詳解 spring 聲明式事務(@Transactional)


spring事務有2種用法:編程式事務和聲明式事務

編程式事務上一篇文章中已經介紹了,不熟悉的建議先看一下編程式事務的用法。

這篇主要介紹聲明式事務的用法,我們在工作中基本上用的都是聲明式事務,所以這篇文章是比較重要的,建議各位打起精神,正式開始。

什么是聲明式事務?

所謂聲明式事務,就是通過配置的方式,比如通過配置文件(xml)或者注解的方式,告訴spring,哪些方法需要spring幫忙管理事務,然后開發者只用關注業務代碼,而事務的事情spring自動幫我們控制。

比如注解的方式,只需在方法上面加一個@Transaction注解,那么方法執行之前spring會自動開啟一個事務,方法執行完畢之后,會自動提交或者回滾事務,而方法內部沒有任何事務相關代碼,用起來特別的方法。

@Transaction
public void insert(String userName){
    this.jdbcTemplate.update("insert into t_user (name) values (?)", userName);
}

聲明式事務的2種實現方式

  1. 配置文件的方式,即在spring xml文件中進行統一配置,開發者基本上就不用關注事務的事情了,代碼中無需關心任何和事務相關的代碼,一切交給spring處理。
  2. 注解的方式,只需在需要spring來幫忙管理事務的方法上加上@Transaction注解就可以了,注解的方式相對來說更簡潔一些,都需要開發者自己去進行配置,可能有些同學對spring不是太熟悉,所以配置這個有一定的風險,做好代碼review就可以了。

配置文件的方式這里就不講了,用的相對比較少,我們主要掌握注解的方式如何使用,就可以了。

聲明式事務注解方式5個步驟

1、啟用Spring的注釋驅動事務管理功能

在spring配置類上加上@EnableTransactionManagement注解

@EnableTransactionManagement
public class MainConfig4 {
}

簡要介紹一下原理:當spring容器啟動的時候,發現有@EnableTransactionManagement注解,此時會攔截所有bean的創建,掃描看一下bean上是否有@Transaction注解(類、或者父類、或者接口、或者方法中有這個注解都可以),如果有這個注解,spring會通過aop的方式給bean生成代理對象,代理對象中會增加一個攔截器,攔截器會攔截bean中public方法執行,會在方法執行之前啟動事務,方法執行完畢之后提交或者回滾事務。稍后會專門有一篇文章帶大家看這塊的源碼。

如果有興趣的可以自己先去讀一下源碼,主要是下面這個這方法會

org.springframework.transaction.interceptor.TransactionInterceptor#invoke

再來看看 EnableTransactionManagement 的源碼

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

 /**
  * spring是通過aop的方式對bean創建代理對象來實現事務管理的
  * 創建代理對象有2種方式,jdk動態代理和cglib代理
  * proxyTargetClass:為true的時候,就是強制使用cglib來創建代理
  */

 boolean proxyTargetClass() default false;

 /**
  * 用來指定事務攔截器的順序
  * 我們知道一個方法上可以添加很多攔截器,攔截器是可以指定順序的
  * 比如你可以自定義一些攔截器,放在事務攔截器之前或者之后執行,就可以通過order來控制
  */

 int order() default Ordered.LOWEST_PRECEDENCE;
}

2、定義事務管理器

事務交給spring管理,那么你肯定要創建一個或者多個事務管理者,有這些管理者來管理具體的事務,比如啟動事務、提交事務、回滾事務,這些都是管理者來負責的。

spring中使用PlatformTransactionManager這個接口來表示事務管理者。

PlatformTransactionManager多個實現類,用來應對不同的環境

圖片

JpaTransactionManager:如果你用jpa來操作db,那么需要用這個管理器來幫你控制事務。

DataSourceTransactionManager:如果你用是指定數據源的方式,比如操作數據庫用的是:JdbcTemplate、mybatis、ibatis,那么需要用這個管理器來幫你控制事務。

HibernateTransactionManager:如果你用hibernate來操作db,那么需要用這個管理器來幫你控制事務。

JtaTransactionManager:如果你用的是java中的jta來操作db,這種通常是分布式事務,此時需要用這種管理器來控制事務。

比如:我們用的是mybatis或者jdbctemplate,那么通過下面方式定義一個事務管理器。

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

3、需使用事務的目標上加@Transaction注解

  • @Transaction放在接口上,那么接口的實現類中所有public都被spring自動加上事務
  • @Transaction放在類上,那么當前類以及其下無限級子類中所有pubilc方法將被spring自動加上事務
  • @Transaction放在public方法上,那么該方法將被spring自動加上事務
  • 注意:@Transaction只對public方法有效

下面我們看一下@Transactional源碼:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

    /**
     * 指定事務管理器的bean名稱,如果容器中有多事務管理器PlatformTransactionManager,
     * 那么你得告訴spring,當前配置需要使用哪個事務管理器
     */

    @AliasFor("transactionManager")
    String value() default "";

    /**
     * 同value,value和transactionManager選配一個就行,也可以為空,如果為空,默認會從容器中按照類型查找一個事務管理器bean
     */

    @AliasFor("value")
    String transactionManager() default "";

    /**
     * 事務的傳播屬性
     */

    Propagation propagation() default Propagation.REQUIRED;

    /**
     * 事務的隔離級別,就是制定數據庫的隔離級別,數據庫隔離級別大家知道么?不知道的可以去補一下
     */

    Isolation isolation() default Isolation.DEFAULT;

    /**
     * 事務執行的超時時間(秒),執行一個方法,比如有問題,那我不可能等你一天吧,可能最多我只能等你10秒
     * 10秒后,還沒有執行完畢,就彈出一個超時異常吧
     */

    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

    /**
     * 是否是只讀事務,比如某個方法中只有查詢操作,我們可以指定事務是只讀的
     * 設置了這個參數,可能數據庫會做一些性能優化,提升查詢速度
     */

    boolean readOnly() default false;

    /**
     * 定義零(0)個或更多異常類,這些異常類必須是Throwable的子類,當方法拋出這些異常及其子類異常的時候,spring會讓事務回滾
     * 如果不配做,那么默認會在 RuntimeException 或者 Error 情況下,事務才會回滾 
     */

    Class<? extends Throwable>[] rollbackFor() default {};

    /**
     * 和 rollbackFor 作用一樣,只是這個地方使用的是類名
     */

    String[] rollbackForClassName() default {};

    /**
     * 定義零(0)個或更多異常類,這些異常類必須是Throwable的子類,當方法拋出這些異常的時候,事務不會回滾
     */

    Class<? extends Throwable>[] noRollbackFor() default {};

    /**
     * 和 noRollbackFor 作用一樣,只是這個地方使用的是類名
     */

    String[] noRollbackForClassName() default {};

}

參數介紹

參數 描述
value 指定事務管理器的bean名稱,如果容器中有多事務管理器PlatformTransactionManager,那么你得告訴spring,當前配置需要使用哪個事務管理器
transactionManager 同value,value和transactionManager選配一個就行,也可以為空,如果為空,默認會從容器中按照類型查找一個事務管理器bean
propagation 事務的傳播屬性,下篇文章詳細介紹
isolation 事務的隔離級別,就是制定數據庫的隔離級別,數據庫隔離級別大家知道么?不知道的可以去補一下
timeout 事務執行的超時時間(秒),執行一個方法,比如有問題,那我不可能等你一天吧,可能最多我只能等你10秒 10秒后,還沒有執行完畢,就彈出一個超時異常吧
readOnly 是否是只讀事務,比如某個方法中只有查詢操作,我們可以指定事務是只讀的 設置了這個參數,可能數據庫會做一些性能優化,提升查詢速度
rollbackFor 定義零(0)個或更多異常類,這些異常類必須是Throwable的子類,當方法拋出這些異常及其子類異常的時候,spring會讓事務回滾 如果不配做,那么默認會在 RuntimeException 或者 Error 情況下,事務才會回滾
rollbackForClassName 同 rollbackFor,只是這個地方使用的是類名
noRollbackFor 定義零(0)個或更多異常類,這些異常類必須是Throwable的子類,當方法拋出這些異常的時候,事務不會回滾
noRollbackForClassName 同 noRollbackFor,只是這個地方使用的是類名

4、執行db業務操作

在@Transaction標注類或者目標方法上執行業務操作,此時這些方法會自動被spring進行事務管理。

如,下面的insertBatch操作,先刪除數據,然后批量插入數據,方法上加上了@Transactional注解,此時這個方法會自動受spring事務控制,要么都成功,要么都失敗。

@Component
public class UserService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    //先清空表中數據,然后批量插入數據,要么都成功要么都失敗
    @Transactional
    public void insertBatch(String... names) {
        jdbcTemplate.update("truncate table t_user");
        for (String name : names) {
            jdbcTemplate.update("INSERT INTO t_user(name) VALUES (?)", name);
        }
    }
}

5、啟動spring容器,使用bean執行業務操作

@Test
public void test1() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(MainConfig4.class);
    context.refresh();

    UserService userService = context.getBean(UserService.class);
    userService.insertBatch("java高並發系列""mysql系列""maven系列""mybatis系列");
}

案例1

准備數據庫

DROP DATABASE IF EXISTS javacode2018;
CREATE DATABASE if NOT EXISTS javacode2018;

USE javacode2018;
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user(
  id int PRIMARY KEY AUTO_INCREMENT,
  name varchar(256)
 NOT NULL DEFAULT '' COMMENT '姓名'
)
;

spring配置類

package com.javacode2018.tx.demo4;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.support.TransactionTemplate;

import javax.sql.DataSource;

@EnableTransactionManagement //@1
@Configuration
@ComponentScan
public class MainConfig4 {
    //定義一個數據源
    @Bean
    public DataSource dataSource() {
        org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8");
        dataSource.setUsername("root");
        dataSource.setPassword("root123");
        dataSource.setInitialSize(5);
        return dataSource;
    }

    //定義一個JdbcTemplate,用來執行db操作
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    //定義我一個事物管理器
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) //@2
        return new DataSourceTransactionManager(dataSource);
    }
}

@1:使用@EnableTransactionManagement注解開啟spring事務管理

@2:定義事務管理器

來個業務類

package com.javacode2018.tx.demo4;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;

@Component
public class UserService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    //先清空表中數據,然后批量插入數據,要么都成功要么都失敗
    @Transactional //@1
    public int insertBatch(String... names) {
        int result = 0;
        jdbcTemplate.update("truncate table t_user");
        for (String name : names) {
            result += jdbcTemplate.update("INSERT INTO t_user(name) VALUES (?)", name);
        }
        return result;
    }

    //獲取所有用戶信息
    public List<Map<String, Object>> userList() {
        return jdbcTemplate.queryForList("SELECT * FROM t_user");
    }
}

@1:insertBatch方法上加上了@Transactional注解,讓spring來自動為這個方法加上事務

測試類

package com.javacode2018.tx.demo4;

import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Demo4Test {
    @Test
    public void test1() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(MainConfig4.class);
        context.refresh();

        UserService userService = context.getBean(UserService.class);
        //先執行插入操作
        int count = userService.insertBatch(
                "java高並發系列",
                "mysql系列",
                "maven系列",
                "mybatis系列");
        System.out.println("插入成功(條):" + count);
        //然后查詢一下
        System.out.println(userService.userList());
    }
}

運行輸出

插入成功(條):4
[{id=1, name=java高並發系列}, {id=2, name=mysql系列}, {id=3, name=maven系列}, {id=4, name=mybatis系列}]

有些朋友可能會問,如何知道這個被調用的方法有沒有使用事務? 下面我們就來看一下。

如何確定方法有沒有用到spring事務

方式1:斷點調試

spring事務是由TransactionInterceptor攔截器處理的,最后會調用下面這個方法,設置個斷點就可以看到詳細過程了。

org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction

圖片

方式2:看日志

spring處理事務的過程,有詳細的日志輸出,開啟日志,控制台就可以看到事務的詳細過程了。

添加maven配置

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

src\main\resources新建logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>[%d{MM-dd HH:mm:ss.SSS}][%thread{20}:${PID:- }][%X{trace_id}][%level][%logger{56}:%line:%method\(\)]:%msg%n##########**********##########%n</pattern>
        </encoder>
    </appender>

    <logger name="org.springframework" level="debug">
        <appender-ref ref="STDOUT" />
    </logger>

</configuration>

再來運行一下案例1

[09-10 11:20:38.830][main: ][][DEBUG][o.s.jdbc.datasource.DataSourceTransactionManager:370:getTransaction()]:Creating new transaction with name [com.javacode2018.tx.demo4.UserService.insertBatch]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
##########**********##########
[09-10 11:20:39.120][main: ][][DEBUG][o.s.jdbc.datasource.DataSourceTransactionManager:265:doBegin()]:Acquired Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@65fe9e33]]] for JDBC transaction
##########**********##########
[09-10 11:20:39.125][main: ][][DEBUG][o.s.jdbc.datasource.DataSourceTransactionManager:283:doBegin()]:Switching JDBC Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@65fe9e33]]] to manual commit
##########**********##########
[09-10 11:20:39.139][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:502:update()]:Executing SQL update [truncate table t_user]
##########**********##########
[09-10 11:20:39.169][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:860:update()]:Executing prepared SQL update
##########**********##########
[09-10 11:20:39.169][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:609:execute()]:Executing prepared SQL statement [INSERT INTO t_user(name) VALUES (?)]
##########**********##########
[09-10 11:20:39.234][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:860:update()]:Executing prepared SQL update
##########**********##########
[09-10 11:20:39.235][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:609:execute()]:Executing prepared SQL statement [INSERT INTO t_user(name) VALUES (?)]
##########**********##########
[09-10 11:20:39.236][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:860:update()]:Executing prepared SQL update
##########**********##########
[09-10 11:20:39.237][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:609:execute()]:Executing prepared SQL statement [INSERT INTO t_user(name) VALUES (?)]
##########**********##########
[09-10 11:20:39.238][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:860:update()]:Executing prepared SQL update
##########**********##########
[09-10 11:20:39.239][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:609:execute()]:Executing prepared SQL statement [INSERT INTO t_user(name) VALUES (?)]
##########**********##########
[09-10 11:20:39.241][main: ][][DEBUG][o.s.jdbc.datasource.DataSourceTransactionManager:741:processCommit()]:Initiating transaction commit
##########**********##########
[09-10 11:20:39.241][main: ][][DEBUG][o.s.jdbc.datasource.DataSourceTransactionManager:328:doCommit()]:Committing JDBC transaction on Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@65fe9e33]]]
##########**********##########
[09-10 11:20:39.244][main: ][][DEBUG][o.s.jdbc.datasource.DataSourceTransactionManager:387:doCleanupAfterCompletion()]:Releasing JDBC Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@65fe9e33]]] after transaction
##########**********##########
插入成功(條):4
[09-10 11:20:39.246][main: ][][DEBUG][org.springframework.jdbc.core.JdbcTemplate:427:query()]:Executing SQL query [SELECT * FROM t_user]
##########**********##########
[09-10 11:20:39.247][main: ][][DEBUG][org.springframework.jdbc.datasource.DataSourceUtils:115:doGetConnection()]:Fetching JDBC Connection from DataSource
##########**********##########
[
{id=1, name=java高並發系列}, {id=2, name=mysql系列}, {id=3, name=maven系列}, {id=4, name=mybatis系列}]

來理解一下日志

insertBatch方法上有@Transaction注解,所以會被攔截器攔截,下面是在insertBatch方法調用之前,創建了一個事務。

圖片

insertBatch方法上@Transaction注解參數都是默認值,@Transaction注解中可以通過value或者transactionManager來指定事務管理器,但是沒有指定,此時spring會在容器中按照事務管理器類型找一個默認的,剛好我們在spring容器中定義了一個,所以直接拿來用了。事務管理器我們用的是new DataSourceTransactionManager(dataSource),從事務管理器的datasource中獲取一個數據庫連接,然后通過連接設置事務為手動提交,然后將(datasource->這個連接)丟到ThreadLocal中了,具體為什么,可以看上一篇文章。

圖片

下面就正是進入insertBatch方法內部了,通過jdbctemplate執行一些db操作,jdbctemplate內部會通過datasource到上面的threadlocal中拿到spring事務那個連接,然后執行db操作。

圖片

最后insertBatch方法執行完畢之后,沒有任何異常,那么spring就開始通過數據庫連接提交事務了。

圖片

總結

本文講解了一下spring中編程式事務的使用步驟。

主要涉及到了2個注解:

@EnableTransactionManagement:開啟spring事務管理功能

@Transaction:將其加在需要spring管理事務的類、方法、接口上,只會對public方法有效。

大家再消化一下,有問題,歡迎留言交流。

下篇文章將詳細介紹事務的傳播屬性,敬請期待。

案例源碼

git地址:
https://gitee.com/javacode2018/spring-series

本文案例對應源碼:spring-series\lesson-002-tx\src\main\java\com\javacode2018\tx\demo4

路人甲java所有案例代碼以后都會放到這個上面,大家watch一下,可以持續關注動態。

參考:https://mp.weixin.qq.com/s?__biz=MzA5MTkxMDQ4MQ==&mid=2648936892&idx=2&sn=473a156dc141a2efc0580f93567f0630&scene=21#wechat_redirect


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM