Transactional是spring中定義的事務注解,在方法或類上加該注解開啟事務。主要是通過反射獲取bean的注解信息,利用AOP對編程式事務進行封裝實現。AOP對事務的封裝可以看我的這篇文章的介紹。
我們先寫個demo,感受它的加載過程。
spring事務注解:
1. 自定義一個注解
/** * @Target 作用域(作用在方法上,類上,或是屬性上) * @Retention 運行周期 * @interface 定義注解 */ @Target(value = ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { //自定義注解的屬性 int id() default 0; String name() default "默認名稱"; String[]arrays(); String title() default "默認標題"; }
2. 測試
import java.lang.reflect.Method; public class User { @MyAnnotation(name="吳磊",arrays = {"2","3"}) public void aMethod () {} public void bMethod () {} public static void main(String[] args) throws ClassNotFoundException { //1. 反射獲取到類信息 Class<?> forName = Class.forName("com.demo.User"); //2. 獲取到當前類(不包含繼承)所有的方法 Method[] declaredMethods = forName.getDeclaredMethods(); //3. 遍歷每個方法的信息 for (Method method : declaredMethods) { System.out.println("方法名稱:" + method.getName()); //4. 獲取方法上面的注解 MyAnnotation annotation = method.getDeclaredAnnotation(MyAnnotation.class); if(annotation == null) { System.out.println("該方法上沒有加注解...."); }else { System.out.println("Id:" + annotation.id()); System.out.println("Name:" + annotation.name()); System.out.println("Arrays:" + annotation.arrays()); System.out.println("Title:" + annotation.title()); } System.out.println("--------------------------"); } } }
============================= 【控制台輸出】 方法名稱:main 該方法上沒有加注解.... -------------------------- 方法名稱:aMethod Id:0 Name:吳磊 Arrays:[Ljava.lang.String;@74a14482 Title:默認標題 -------------------------- 方法名稱:bMethod 該方法上沒有加注解.... --------------------------
總結:通過上面這么一個小demo我們就能發現,反射獲取到每一個方法的注解信息然后進行判斷,如果這是@Transactional注解,spring就會開啟事務。接下來我們可以按照這種思路基於上一篇博客的編程式事務自己實現一個事務注解。
手寫注解事務:
1. 導包
<dependencies> <!-- 引入Spring-AOP等相關Jar --> <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.springframework</groupId> <artifactId>spring-orm</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> <!-- https://mvnrepository.com/artifact/com.mchange/c3p0 --> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.2</version> </dependency> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.37</version> </dependency> </dependencies>
2. 配置spring.xml文件
<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" 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="com.wulei"/> <!-- 開啟切面代理 --> <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="root"></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> </beans>
3.1 自定義事務注解 (通過反射解析方法上的注解,如果有這個注解就執行事務邏輯)
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; //@Transaction可以作用在類和方法上, 我們這里只作用在方法上。 @Target(value = ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) /** * 自定義事務注解 */ public @interface MyAnnotation { }
3.2 封裝編程式事務
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.stereotype.Component; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.interceptor.DefaultTransactionAttribute; @Component @Scope("prototype") // 申明為多例,解決線程安全問題。 /** * 手寫編程式事務 */ public class TransactionUtil { // 全局接受事務狀態 private TransactionStatus transactionStatus; // 獲取事務源 @Autowired private DataSourceTransactionManager dataSourceTransactionManager; // 開啟事務 public TransactionStatus begin() { System.out.println("開啟事務"); transactionStatus = dataSourceTransactionManager.getTransaction(new DefaultTransactionAttribute()); return transactionStatus; } // 提交事務 public void commit(TransactionStatus transaction) { System.out.println("提交事務"); if(dataSourceTransactionManager != null) dataSourceTransactionManager.commit(transaction); } // 回滾事務 public void rollback(TransactionStatus transaction) { System.out.println("回滾事務..."); if(dataSourceTransactionManager != null) dataSourceTransactionManager.rollback(transaction); } }
3.3 通過AOP封裝事務工具類, 基於環繞通知和異常通知來觸發事務。
import java.lang.reflect.Method; 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 com.wulei.transaction.TransactionUtil; @Aspect// 申明為切面 @Component /** * 切面類封裝事務邏輯 */ public class AopTransaction { @Autowired private TransactionUtil transactionUtil; private TransactionStatus transactionStatus; /** * 環繞通知 在方法之前和之后處理事情 * @param pjp 切入點 */ @Around("execution(* com.wulei.service.*.*(..))") public void around(ProceedingJoinPoint pjp) throws Throwable { // 1.獲取方法的注解 MyAnnotation annotation = this.getMethodMyAnnotation(pjp); // 2.判斷是否需要開啟事務 transactionStatus = begin(annotation); // 3.調用目標代理對象方法 pjp.proceed(); // 4.判斷關閉事務 commit(transactionStatus); } /** * 獲取代理方法上的事務注解 * @param pjp 切入點 */ private MyAnnotation getMethodMyAnnotation(ProceedingJoinPoint pjp) throws Exception { //1. 獲取代理對對象的方法 String methodName = pjp.getSignature().getName(); //2. 獲取目標對象 Class<?> classTarget = pjp.getTarget().getClass(); //3. 獲取目標對象類型 Class<?>[] par = ((MethodSignature) pjp.getSignature()).getParameterTypes(); //4. 獲取目標對象方法 Method objMethod = classTarget.getMethod(methodName, par); //5. 獲取該方法上的事務注解 MyAnnotation annotation = objMethod.getDeclaredAnnotation(MyAnnotation.class); return annotation; } /** 開啟事務 */ private TransactionStatus begin(MyAnnotation annotation) { if(annotation == null) return null; return transactionUtil.begin(); } /** 關閉事務 */ private void commit(TransactionStatus transactionStatus) { if(transactionStatus != null) transactionUtil.commit(transactionStatus); } /** * 異常通知進行 回滾事務 */ @AfterThrowing("execution(* com.wulei.service.*.*(..))") public void afterThrowing() { // 獲取當前事務 直接回滾 //TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); if(transactionStatus != null) transactionUtil.rollback(transactionStatus); } }
4. 編寫dao層
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; /* CREATE TABLE `t_users` ( `name` varchar(20) NOT NULL, `age` int(5) DEFAULT NULL, PRIMARY KEY (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; */ @Repository public class UserDao { @Autowired private JdbcTemplate jdbcTemplate; public int add(String name, Integer age) { String sql = "INSERT INTO t_users(NAME, age) VALUES(?,?);"; int result = jdbcTemplate.update(sql, name, age); System.out.println("插入成功"); return result; } }
5. 編寫service
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.wulei.dao.UserDao; import com.wulei.transaction.MyAnnotation; @Service public class UserService { @Autowired private UserDao userDao; // 加上事務注解 @MyAnnotation public void add() { userDao.add("test001", 20); int i = 1 / 0; userDao.add("test002", 21); // // 如果非要try,那么出現異常不會被aop的異常通知監測到,必須手動在catch里面回滾事務。 // try { // userDao.add("test001", 20); // int i = 1 / 0; // userDao.add("test002", 21); // } catch (Exception e) { // // 回滾當前事務 // System.out.println("回滾"); // TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); // } } public void del() { System.out.println("del..."); } }
6. 測試
import org.springframework.context.support.ClassPathXmlApplicationContext; import com.wulei.service.UserService; public class Main { public static void main(String[] args) { ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); UserService userService = (UserService) applicationContext.getBean("userService"); // aop()對userService類進行了攔截,添加自定義事務注解的方法會觸發事務邏輯 userService.add(); // del()方法沒有加注解,則什么也不會觸發。 //userService.del(); } }
@Transactional 自調用事務失效分析
出處:在同一個類中,一個方法調用另外一個有注解(比如@Async,@Transational)的方法,注解失效的原因和解決方法
在同一個類中,一個方法調用另外一個有注解(比如@Async,@Transational)的方法,注解是不會生效的。
比如,下面代碼例子中,有兩方法,一個有@Transational注解,一個沒有。如果調用了有注解的addPerson()方法,會啟動一個Transaction;如果調用updatePersonByPhoneNo(),因為它內部調用了有注解的addPerson(),如果你以為系統也會為它啟動一個Transaction,那就錯了,實際上是沒有的。
@Service public class PersonServiceImpl implements PersonService { @Autowired PersonDao personDao; @Override @Transactional public boolean addPerson(Person person) { boolean result = personDao.insertPerson(person)>0 ? true : false; return result; } @Override //@Transactional public boolean updatePersonByPhoneNo(Person person) { boolean result = personDao.updatePersonByPhoneNo(person)>0 ? true : false; addPerson(person); //測試同一個類中@Transactional是否起作用 return result; } }
如何查看是否啟動了Transaction?
設置log leve為debug,可以查看是否有下面這個log,判斷是否啟動了Transaction:
DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Creating new transaction with name...
同樣地,@Async等其他注解也有這樣的問題。
(關於@Async的用法,請參考:http://blog.csdn.net/clementad/article/details/47403185)
原因:
spring 在掃描bean的時候會掃描方法上是否包含@Transactional注解,如果包含,spring會為這個bean動態地生成一個子類(即代理類,proxy),代理類是繼承原來那個bean的。此時,當這個有注解的方法被調用的時候,實際上是由代理類來調用的,代理類在調用之前就會啟動transaction。然而,如果這個有注解的方法是被同一個類中的其他方法調用的,那么該方法的調用並沒有通過代理類,而是直接通過原來的那個bean,所以就不會啟動transaction,我們看到的現象就是@Transactional注解無效。
為什么一個方法a()調用同一個類中另外一個方法b()的時候,b()不是通過代理類來調用的呢?可以看下面的例子(為了簡化,用偽代碼表示):
@Service class A{ @Transactinal method b(){...} method a(){ //標記1 b(); } } //Spring掃描注解后,創建了另外一個代理類,並為有注解的方法插入一個startTransaction()方法: class proxy$A{ A objectA = new A(); method b(){ //標記2 startTransaction(); objectA.b(); } method a(){ //標記3 objectA.a(); //由於a()沒有注解,所以不會啟動transaction,而是直接調用A的實例的a()方法 } }
當我們調用A的bean的a()方法的時候,也是被proxy$A攔截,執行proxy$A.a()(標記3),然而,由以上代碼可知,這時候它調用的是objectA.a(),也就是由原來的bean來調用a()方法了,所以代碼跑到了“標記1”。由此可見,“標記2”並沒有被執行到,所以startTransaction()方法也沒有運行。
了解了失效的原因,解決的方法就簡單了(兩種):
- 把這兩個方法分開到不同的類中;
- 把注解加到類名上面;