基於SpringAOP手寫事務及事務註解

我的博客:zhenganwen.topphp

AOP概述

AOP,即Aspect-Oriented Program面向切面編程,相比較繼承、裝飾者模式等縱向加強對象的方式,AOP是橫向的、無入侵性的、可插拔的、高複用的。所以做爲Spring的核心模塊之一,它普遍應用於日誌記錄、事務管理、權限控制、異常處理等場景。java

代理模式

因爲AOP是基於動態代理的,因此本節先簡單介紹一下代理模式。代理模式分爲靜態代理和動態代理,核心思想是在調用方和被調用方(被代理對象,也稱目標對象)起到一箇中介(代理對象)的做用,這樣可以達到解耦、複用、保護的效果:mysql

  • 全部的調用請求都要先通過代理對象,因而代理對象能夠先對這些請求作一個過濾從而屏蔽非法請求,是爲保護
  • 若是每次調用前都須要記錄一下調用者的信息,那麼能夠將此任務交給代理對象,而無需在不一樣的被調用方都增長此項代碼,是爲複用
  • 對於調用方而言,代理對象和目標對象有一樣的外觀,經過代理對象做爲中介,調用方並不知道實際處理請求的究竟是誰,調用方和和目標對象沒有直接的依賴關係,是爲解耦

代理對象對調用請求是不作實際的業務處理的spring

靜態代理

經過實現和目標對象一樣的接口,以實現和目標對象有相同的外觀。經過保存目標對象的引用來對請求作實際的業務處理。sql

靜態代理示例代碼以下:數據庫

public interface UserService {

    void add();
}

public class UserReadServiceImpl implements UserService {
    public void add() {
        System.out.println("illegal invoke");
    }

    public void query() {
        System.out.println("query user");
    }
}

public class UserWriteServiceImpl implements UserService {
    public void add() {
        System.out.println("insert user");
    }

    public void query() {
        System.out.println("illegal invoke");
    }
}

public class UserServiceProxy implements UserService {

    private UserService userService;

    public UserServiceProxy(UserService userService) {
        this.userService = userService;
    }

    public void add() {
        userService.add();
    }

    public void query() {
        userService.query();
    }
}
複製代碼

測試以下:編程

public class UserServiceProxyTest {

    @Test
    public void add() {
        UserService userService = new UserWriteServiceImpl();
        UserServiceProxy userServiceProxy = new UserServiceProxy(userService);
        userServiceProxy.add();	
    }

    @Test
    public void query() {
        UserService userService = new UserReadServiceImpl();
        UserServiceProxy userServiceProxy = new UserServiceProxy(userService);
        userServiceProxy.query(); 
    }
}

insert user
query user
複製代碼

靜態代理的弊端:api

  • 增對每個業務領域(如這裏的UserService就是一個域),都須要手動編寫一個代理類
  • 代碼冗餘
    • 針對每一個代理類,都須要在每一個代理方法中編寫代理邏輯,即便這些邏輯是同樣的。好比日誌代理類就是在每次調用目標方法前作一個請求信息的日誌記錄,若在每一個代理方法調用目標方法前都加上此項,會顯得重複冗餘。

針對靜態代理的這些缺點,採用動態代理就能很好的規避。工具

動態代理

目前動態代理有兩種方案,即jdk代理和cglib代理。測試

  • jdk代理是指jdk內置的,經過jdk api就能夠實現,只能對目標對象實現的接口方法作加強
  • cglib代理是指使用第三方類庫cglib實現動態代理,cglib還依賴了asm庫,asm是一種字節碼修改技術,而cglib就是經過修改字節碼中的符號引用來生成代理對象的。字節碼文件結構及符號引用可參閱《深刻理解Java虛擬機(第二版)》(周志明)。因爲cglib動態代理是基於繼承的,因此cglib動態代理能夠 加強目標對象全部的可繼承方法

jdk代理的示例代碼以下:

package cn.tuhu.springaop.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class LogProxy {

    private Object target;

    private LogInvocationHandler logInvocationHandler = new LogInvocationHandler();

    public LogProxy(Object target) {
        this.target = target;
    }

    public Object getProxyObject() {
        ClassLoader classLoader = target.getClass().getClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();
        return Proxy.newProxyInstance(classLoader, interfaces, logInvocationHandler);
    }

    private class LogInvocationHandler implements InvocationHandler {

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("log for request");
            method.invoke(target, args);
            return proxy;
        }
    }

}
複製代碼

測試以下:

package cn.tuhu.springaop.proxy;

import cn.tuhu.springaop.service.UserService;
import cn.tuhu.springaop.service.impl.UserReadServiceImpl;
import org.junit.Test;

import static org.junit.Assert.*;

public class LogProxyTest {

    @Test
    public void getProxyObject() {
        UserService userService = new UserReadServiceImpl();
        LogProxy logProxy = new LogProxy(userService);
        UserService userServiceProxy = (UserService) logProxy.getProxyObject();
        userServiceProxy.query();
        userServiceProxy.add();
    }
}


log for request
query user
log for request
illegal invoke
複製代碼

基於SpringAOP手寫事務

AOP編程

上述以講述了動態代理的基本原理,SpringAOP底層用的就是動態代理,它由如下幾個要素組成:

  • 切入點/關注點pointcut/joinpoint
    • 須要被加強的方法
  • 加強/通知advice
    • 在目標方法基礎之上須要加強的邏輯
  • 切面aspect
    • 將通知應用在切入點之上的集合體

引入SpringAOP相關依賴:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>3.0.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>3.0.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>3.0.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.6.1</version>
</dependency>
<dependency>
    <groupId>aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.5.3</version>
</dependency>
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.1_2</version>
</dependency>
複製代碼

添加spring配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:component-scan base-package="cn.tuhu.springaop"></context:component-scan>
	
    <!-- 開啓AOP編程註解,開啓後標識爲@Aspect的bean的AOP纔會生效 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy> 

</beans>

複製代碼

業務接口:

package cn.tuhu.springaop.service;

public interface UserService {

    void add();

    void query();
}
複製代碼

業務實現:

package cn.tuhu.springaop.service.impl;

import cn.tuhu.springaop.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    public void add() {
        System.out.println("insert user");
    }

    public void query() {
        System.out.println("query user");
    }
}
複製代碼

切面類:

package cn.tuhu.springaop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TransactionAspect {

    //前置通知
    @Before(value = "execution(* cn.tuhu.springaop.service.impl.*.*(..))") //切入點:service.impl包下的全部方法
    public void before() {
        System.out.println("Before:最早執行");
    }

    //後置通知
    @After(value = "execution(* cn.tuhu.springaop.service.impl.*.*(..))")
    public void after() {
        System.out.println("After:方法執行以後執行");
    }

    //環繞通知
    @Around(value = "execution(* cn.tuhu.springaop.service.impl.*.*(..))")
    public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("Around Begin:Before執行以後方法執行以前執行");
        proceedingJoinPoint.proceed();
        System.out.println("Around End:After執行以後執行");
    }

    //正常結束通知
    @AfterReturning(value = "execution(* cn.tuhu.springaop.service.impl.*.*(..))")
    public void afterReturning() {
        System.out.println("AfterReturning:最後執行");
    }

    //異常終止通知
    @AfterThrowing(value = "execution(* cn.tuhu.springaop.service.impl.*.*(..))")
    public void afterThrowing() {
        System.out.println("AfterThrowing:拋出異常後執行");
    }

}
複製代碼

測試類:

package cn.tuhu.springaop.service.impl;

import cn.tuhu.springaop.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class UserServiceImplTest {

    ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");

    @Test
    public void add() {
        UserService userService = context.getBean(UserService.class);
        userService.add();
    }
}


Before:最早執行
Around Begin:Before執行以後方法執行以前執行
insert user
After:方法執行以後執行
Around End:After執行以後執行
AfterReturning:最後執行
======================
Before:最早執行
Around Begin:Before執行以後方法執行以前執行
query user
After:方法執行以後執行
Around End:After執行以後執行
AfterReturning:最後執行
複製代碼

可見,AOP的切面機制很靈活,能夠有不一樣方式的加強和靈活的關注點配置(execution表達式)。

編程式事務

編程式事務,即在業務方法中手動編碼開啓事務會話、提交事務、回滾。

環境準備,須要鏈接數據庫以及使用Spring提供的JdbcTemplate

<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.5.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.37</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>3.0.6.RELEASE</version>
</dependency>
複製代碼

在配置文件中配置數據源以及事務管理器:

<!-- 1. 數據源對象: C3P0鏈接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>
    <property name="user" value="root"></property>
    <property name="password" value="123456"></property>
</bean>

<!-- 2. JdbcTemplate工具類實例 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"></property>
</bean>

<!-- 3.配置事務 -->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>
複製代碼

Dao

package cn.tuhu.springaop.dao;

import cn.tuhu.springaop.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class UserDao {

    @Autowired
    JdbcTemplate jdbcTemplate;

    public void add(User user) {
        String sql = "insert into user (id,name) values(" + user.getId() + ",'" + user.getName() + "');";
        jdbcTemplate.execute(sql);
    }
}
複製代碼

Service

package cn.tuhu.springaop.service.impl;

import cn.tuhu.springaop.dao.UserDao;
import cn.tuhu.springaop.entity.User;
import cn.tuhu.springaop.service.UserService;
import cn.tuhu.springaop.util.TransactionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserDao userDao;

    @Autowired
    TransactionUtils transactionUtils;

    public void add() {
        TransactionStatus transactionStatus = null;
        try {
            //begin
            transactionStatus = transactionUtils.begin();

            User user = new User(1L, "張三");
            userDao.add(user);
            user = new User(2L, "李四");
            int i = 1 / 0;
            userDao.add(user);

            if (transactionStatus != null) {
                transactionUtils.commit(transactionStatus);
            }
        } catch (Exception e) {
            if (transactionStatus != null) {
                transactionUtils.rollback(transactionStatus);
            }
        }
    }

}
複製代碼

測試事務

public class UserServiceImplTest {

    ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");

    @Test
    public void add() {
        UserService userService = context.getBean(UserService.class);
        userService.add();
    }
}
複製代碼

刷新數據庫user表發現沒有數據插入,而註釋掉i=1/0則插入兩條數據,說明事務生效。

上述事務的開啓、提交、回滾是模板式的代碼,咱們應該抽取出來以供複用,這時AOP就派上用場了。

package cn.tuhu.springaop.proxy;

import cn.tuhu.springaop.util.TransactionUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;

@Component
@Aspect
public class TransactionAop {

    @Autowired
    TransactionUtils transactionUtils;

    @Around(value = "execution(* cn.tuhu.springaop.service.impl.*.add*(..))," +
            "execution(* cn.tuhu.springaop.service.impl.*.update*(..))," +
            "execution(* cn.tuhu.springaop.service.impl.*.delete*(..))")
    public void transactionHandler(ProceedingJoinPoint proceedingJoinPoint) {
        TransactionStatus transactionStatus = transactionUtils.begin();
        try {
            proceedingJoinPoint.proceed();
            transactionUtils.commit(transactionStatus);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            transactionUtils.rollback(transactionStatus);
        }
    }
}
複製代碼

業務類則只需關注業務代碼

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserDao userDao;

    public void add() {
        User user = new User(1L, "張三");
        userDao.add(user);
        user = new User(2L, "李四");
        int i = 1 / 0;
        userDao.add(user);
    }

}
複製代碼

聲明式事務

Spring的聲明式事務是經過@Transactional註解來實現的,首先咱們先屏蔽上節寫的TransactionAop

//@Component
//@Aspect
public class TransactionAop {
複製代碼

spring.xml中開啓聲明式事務註解(注意引入tx名稱空間):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    <context:component-scan base-package="cn.tuhu.springaop"></context:component-scan>
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy> <!-- 開啓事物註解 -->

    <!-- 1. 數據源對象: C3P0鏈接池 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>

    <!-- 2. JdbcTemplate工具類實例 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 3.配置事務 -->
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
</beans>
複製代碼

只需在方法和類上添加@Transactional註解便可添加事務控制(若在類上添加則至關於在每一個方法上都加了@Transactional):

package cn.tuhu.springaop.service.impl;

import cn.tuhu.springaop.dao.UserDao;
import cn.tuhu.springaop.entity.User;
import cn.tuhu.springaop.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserDao userDao;

    @Transactional(rollbackFor = Exception.class)
    public void add() {
        User user = new User(1L, "張三");
        userDao.add(user);
        user = new User(2L, "李四");
        int i = 1 / 0;  //不註釋測一次,註釋起來再測一次
        userDao.add(user);
    }

}
複製代碼

實現原理

Spring經過掃包發現被註解爲@Transactional的方法,再經過AOP的方法進行編程式事務控制。

手寫一個事務註解

  1. 定義事務註解

    package cn.tuhu.springaop.annotation;
    
    public @interface MyTransactional {
        
    }
    複製代碼
  2. 編寫切面類,加強帶有事務註解的方法

    package cn.tuhu.springaop.proxy;
    
    import cn.tuhu.springaop.annotation.MyTransactional;
    import cn.tuhu.springaop.util.TransactionUtils;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.transaction.TransactionStatus;
    import org.springframework.transaction.interceptor.TransactionAspectSupport;
    
    import java.lang.reflect.Method;
    
    @Component
    @Aspect
    public class TransactionAop {
    
        @Autowired
        TransactionUtils transactionUtils;
    
        private ProceedingJoinPoint proceedingJoinPoint;
    
        @Around(value = "execution(* cn.tuhu.springaop.service.impl.*.*(..))")
        public void transactionHandler(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            TransactionStatus transactionStatus = null;
    
            if (hasTransaction(proceedingJoinPoint)) {
                transactionStatus = transactionUtils.begin();
            }
    
            proceedingJoinPoint.proceed();
    
            // 若hasTransaction(proceedingJoinPoint)判斷經過,則transactionStatus不爲null
            if (transactionStatus != null) {
                transactionUtils.commit(transactionStatus);
            }
        }
    
        /** * 判斷切入點是否標註了@MyTransactional註解 * * @param proceedingJoinPoint * @return */
        private boolean hasTransaction(ProceedingJoinPoint proceedingJoinPoint) throws NoSuchMethodException {
            this.proceedingJoinPoint = proceedingJoinPoint;
            //獲取方法名
            String methodName = proceedingJoinPoint.getSignature().getName();
            //獲取方法所在類的class對象
            Class clazz = proceedingJoinPoint.getSignature().getDeclaringType();
            //獲取參數列表類型
            Class[] parameterTypes = ((MethodSignature) proceedingJoinPoint.getSignature()).getParameterTypes();
            //根據方法名和方法參列各參數類型可定位類中惟一方法
            Method method = clazz.getMethod(methodName, parameterTypes);
            //根據方法對象獲取方法上的註解信息
            MyTransactional myTransactional = method.getAnnotation(MyTransactional.class);
            return myTransactional == null ? false : true;
        }
    
        @AfterThrowing(value = "execution(* cn.tuhu.springaop.service.impl.*.*(..))")
        public void handleTransactionRollback() throws NoSuchMethodException {
            if (hasTransaction(proceedingJoinPoint)) {
                //獲取當前事務並回滾
                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            }
        }
    }
    複製代碼

    測試經過!

事務傳播行爲

Spring事務註解@Transactional中有一個屬性propagation表示當前事務的傳播行爲,可選值以下:

事務的傳播行爲發生在多個事務之間,默認值爲REQUIRED

Propagation propagation() default Propagation.REQUIRED;
複製代碼

最經常使用的也只有REQUIREDREQUIRED_NEW兩個選項,下面僅介紹這二者的含義,其餘的可本身嘗試。

假設有這樣一個場景,對於訂單表order有一張訂單日誌表order_log專門用來記錄生成訂單的請求。要求每次請求生成訂單時不管訂單是否生成成功,該請求都應該被記錄下來,即請求信息始終會插入order_log而不該該受OrderService事務控制的影響。

以下,咱們能夠爲插入日誌的操做指定REQUIRED_NEW,這樣若是在調用addOrder中調用addLog時會由於addOrder已開啓了事務因而將該事務掛起,併爲addLog新建一個事務,這樣addLog獨立於addOrder的事務以外天然不會受其回滾的影響了。

@Service
public class OrderLogService{
    
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
    public void addLog(){
        // recode request info
    }
}

@Service
public class OrderService{
    
    @Autowire
    OrderLogService orderLogService;
    
    @Transactional(rollbackFor = Exception.class)
    public void addOrder(){
        orderLogService.addLog();
        // generate order
        // ...
        int i = 1 / 0 ;
    }
}
複製代碼

不然,若是不爲addLog添加事務或者將其事務傳播行爲採用默認的REQUIRED的話,addLog中的邏輯就會與addOrder中的邏輯處於同一事務中,一旦生成訂單過程當中出現異常,那麼插入日誌也會一塊兒被回滾。

相關文章
相關標籤/搜索