1. 程式人生 > 其它 >spring成神之路第四十四篇:詳解 spring 宣告式事務(@Transactional)

spring成神之路第四十四篇:詳解 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