山一程--软件开发-Spring transaction

目的:整理掌握基于Spring的企业级技术体系之事务管理.


索引:

1.transaction


1. Spring transaction

背景:数据安全. 

refer:

1.<Spring5 recipes> chapter10  Spring transaction management.  p294   ( 2023.4.1 - 2023.4.8) 第一次


基于 <Spring5 recipes> 实践

1.mysql DDL

CREATE USER 'PRACTICE_TRANSACTION'@'localhost' identified BY 'admin';

CREATE SCHEMA PRACTICE_TRANSACTION;
GRANT ALL PRIVILEGES ON PRACTICE_TRANSACTION .* TO 'PRACTICE_TRANSACTION'@'localhost';
FLUSH PRIVILEGES;

SET GLOBAL time_zone = "+3:00";
1.sql ddl-user schema
create table BOOK(
ISBN varchar(50) PRIMARY KEY,
BOOK_NAME VARCHAR(100) NOT NULL,
PRICE INTEGER
);

create table BOOK_STOCK(
ISBN varchar(50) PRIMARY KEY,
STOCK int NOT NULL CHECK(STOCK >=0)
);

create table ACCOUNT(
USERNAME VARCHAR(50) PRIMARY KEY,
BALANCE int not null CHECK(BALANCE >=0)
)
create table
insert into practice_transaction.book (`ISBN`,`BOOK_NAME`,`PRICE`)
VALUES('0001','The first book',30);

insert into practice_transaction.book_stock (`USERNAME`,`BALANCE`)
VALUES('user1',10);

insert into practice_transaction.account (`USERNAME`,`BALANCE`)
VALUES('user1',20);
insert data

 2.spring pom

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>2.7.10</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
            <version>8.0.21</version>
        </dependency>
View Code

spring application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/practice_transaction?userUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=PRACTICE_TRANSACTION
spring.datasource.password=admin

java code

1.interface BookShop , purse(xx) method

public interface BookShop {
    void purchase(String isbn,String userName);
}

实现

@Component("bookShop")
public class JdbcBookshop implements BookShop {

  @Autowired
  private DataSource dataSource;

  public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
  }

  @Override
  public void purchase(String isbn, String userName) {
    Connection connection = null;
    try {
      connection = dataSource.getConnection();
      PreparedStatement statement_query =
          connection.prepareStatement("SELECT PRICE FROM BOOK WHERE ISBN = ?");
      statement_query.setString(1, isbn);
      ResultSet rs = statement_query.executeQuery();
      rs.next();
      int price = rs.getInt("PRICE");
      statement_query.close();

      PreparedStatement statement_decrease_stock =
          connection.prepareStatement(
              "UPDATE BOOK_STOCK SET STOCK = STOCK - 1 " + "WHERE ISBN = ?");
      statement_decrease_stock.setString(1, isbn);
      statement_decrease_stock.executeUpdate();
      statement_decrease_stock.close();

      PreparedStatement statement_up_account =
          connection.prepareStatement(
              "UPDATE ACCOUNT SET BALANCE = BALANCE - ? " + "where USERNAME = ? ");
      statement_up_account.setInt(1, price);
      statement_up_account.setString(2, userName);
      statement_up_account.executeUpdate();
      statement_up_account.close();

    } catch (SQLException e) {
      throw new RuntimeException(e);
    } finally {
      try {
        if (connection != null) {
          connection.close();
        }
      } catch (SQLException e) {
        e.printStackTrace();
      }
    }
  }
}
Bookshop impl

main

@SpringBootApplication
public class SpringTransactionApplication {

  public static void main(String[] args) {
    SpringApplication.run(SpringTransactionApplication.class, args);
  }

  @Component
  class App implements ApplicationRunner{
    @Autowired
    BookShop bookShop;
    @Override
    public void run(ApplicationArguments args) throws Exception {
      bookShop.purchase("0001","user1");
    }
  }
}
main

问题:

三段 SQL ,第一个查询正常,第二个会库存扣减一本书,

第三个模拟扣减用户金额,但此时违反了数据库check 约束,20 少于 30,交易失败,抛出 SQLException.

结果是库存减少了1,交易失败。数据处于不一致的状态。


解决:使用事务,将三个SQL 归为一个事务。

JDBC 默认是在每条SQL执行后立刻提交,自动提交。该行为不允许你管理操作的事务.

JDBC支持的方式:显示调用连接的 commit() 与 rollback() 来实现,先关闭自动提交,默认是开启的。setAutoCommit(false)


这些样式代码, 特定于JDBC, Spring 事务支持不依赖于特定技术的设施,

包括事务管理:PlaformTransactionManager, 

事务模板:Transaction   Template

事务声明支持。


使用事务管理 API 以编程方式管理事务 

Spring 核心事务管理管理抽象接口 PlatformTransaction,封装了一套独立于技术的方法.

 其实现

1.应用只使用了单个数据源并使用 JDBC 访问,采用 DataSourceTransactionManager

2.在Java EE 服务中使用 JTA 进行事务管理, 使用 JtaTransactionManager 从应用服务器查找事务.,而且其适用分布式事务。经常使用 JTA 事务管理器来

集成应用服务器的事务管理器.  也可使用独立的 JTA事务管理器,如 Atomikos.

3.如果使用对象关系映射框架访问数据库,则应采用 HibernateTransactionManager 或 JpaTransactionManager.

事务管理器是作为一个普通的 bean 声明在 Spring IoC 容器中,bean 配置声明了一个 DataSourceTransactionManager 实例.设置它的 dataSource 属性,

就可以管理该数据源锁创建的连接事务了.

@Configuration
@EnableAutoConfiguration
public class BookstoreConfig {

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager =
                new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}

示例采用上面示例的基础 对象:

注意报错

以构造器方式注入 dataSource, 不能以 Setter方式。 

 

TransactionDefinition def = new DefaultTransactionDefinition();  默认的事务配置

TransactionStatus status = transactionManager.getTransacction ( def );  此处 事务管理器使用该定义开启一个新事务了.

Spring JDBC 模板抛出的所有异常都是 DataAccessException 子类.

@Component("bookShop")
@EnableAutoConfiguration
public class TransactionJdbcBookshop extends JdbcDaoSupport implements BookShop {

    @Autowired
    private PlatformTransactionManager transactionManager;

    @Autowired
    TransactionJdbcBookshop(DataSource dataSource){
        setDataSource(dataSource);
    }

    @Override
    public void purchase(String isbn, String userName) {

        TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
        TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);

        try{
            int price = getJdbcTemplate().queryForObject(
                    "SELECT PRICE FROM BOOK WHERE ISBN = ?",Integer.class,isbn);

            getJdbcTemplate().update(
                    "UPDATE BOOK_STOCK SET STOCK = STOCK - 1 WHERE ISBN = ?",isbn            );

            getJdbcTemplate().update(                    "UPDATE ACCOUNT SET BALANCE = BALANCE - ?",price);

            transactionManager.commit(transactionStatus);
        }catch (DataAccessException e){
            transactionManager.rollback(transactionStatus);
        }
    }
}
View Code
@SpringBootApplication
public class SpringTransactionApplication {

  public static void main(String[] args) {
    SpringApplication.run(SpringTransactionApplication.class, args);
  }

  @Component
  class App implements ApplicationRunner{
    @Autowired
    BookShop bookShop;
    @Override
    public void run(ApplicationArguments args) throws Exception {
      bookShop.purchase("0001","user1");
    }
  }
}
View Code

以上的样板代码,替换为 TransactionTemplate 来控制整个事务管理过程和事务异常处理.

将代码块封装到一个实现了  TransactionCallback<T> 接口的回调类中( 内部类实现需要将方法参数标记为 final )并将其传递给 TransactionTemplate 的 execute 方法执行即可.

轻量级。丢弃或重新创建不会产生性能影响.

1. 通过 DataSource 引用来创建 JDBC 模板

2. 事务管理其引用来创建 TransactionTemplate , 或 Spring 应用上下文创建.

TransactionCallback<T> 有返回值,通过 T execute 返回. 好处在于剥离了 开启,回滚与 提交事务的职责.

子接口 TransactionCallBackWithoutResult

回调对象执行过程中,如出现

1.未检查异常  RuntimeException 和  DataAccessException 

2.在 doInTransactionWithoutResult 方法中显示调用了 TransactionStatus 参数的 setRollbackOnly() 

事务回滚。否则,对象执行完毕后就提交.

@Configuration
public class BookshopConfig {

@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}

@Bean
public TransactionTemplate transactionTemplate(DataSource dataSource){
TransactionTemplate transactionTemplate = new TransactionTemplate();
transactionTemplate.setTransactionManager(transactionManager(dataSource));
return transactionTemplate;
}
}
@Component("bookShop")
@EnableAutoConfiguration
public class TransactionJdbcBookshop extends JdbcDaoSupport implements BookShop {

    @Autowired
    TransactionTemplate transactionTemplate;

    TransactionJdbcBookshop(DataSource dataSource){
        setDataSource(dataSource);
    }

    @Override
    public void purchase(final String isbn,final String userName) {
        this.transactionTemplate.execute(
                new TransactionCallbackWithoutResult() {
                    @Override
                    protected void doInTransactionWithoutResult(TransactionStatus status) {
                        int price = getJdbcTemplate().queryForObject(
                                "SELECT PRICE FROM BOOK WHERE ISBN = ?",Integer.class,isbn);

                        getJdbcTemplate().update(
                                "UPDATE BOOK_STOCK SET STOCK = STOCK - 1 WHERE ISBN = ?",isbn);

                        getJdbcTemplate().update(
                                "UPDATE ACCOUNT SET BALANCE = BALANCE - ? WHERE USERNAME = ?",price,userName);
                    }
                });
    }
}

 @Transaction ,  @EnableTransactionManagement 注解以声明的方式管理事务


事务传播

发生在 一个事务性方法被另一个方法调用的时候。另一个方法可是可不是事务性方法。

 

 Spring 定义了7中传播行为,定义在 TransactionDefinition 接口中,并非所有类型的事务管理器都支持。页与数据库支持不同的隔离级别,约束了

事务管理器所能支持的传播行为:

1. REQUIRED : 如果过程中存在事务,则当前方法应该运行在该事务中,否则应该开启一个新事务并运行在自己的事务中。(默认)

2.REQUIRED_NEW : 当前方法必须要开启一个新事务,且运行在自己的事务中,如果过程中存在事务,则将其挂起.

3.SUPPORTS : 如果过程中存在事务,当前方法可运行在该事务中. 否则,没必要运行在事务中.

4.NOT_SUPPORTED : 当前方法不该运行在事务中,如果过程中存在事务,就应该将其挂起.

5.MANDATORY : 当前方法必须运行在事务中,如果过程中没有事务,则抛出异常.

6.NEVER: 当前方法不能运行在事务中,如果过程中存在事务, 就会抛出异常.

7.NESTED : 如果过程存在事务, 则当前方法运行在该事务的嵌套事务中(JDBC 3.0 安全特性),否则,开启一个新事务并运行在自己的事务中,

      该特性为 Spring 特有. ( 对于长时间运行的可分段处理的场景,批处理) 100W 数据,截断,每1W条记录提交一次。如果出错, 可以回滚嵌套事务,

      只会丢失这1W条记录,而不是 100W.


示例:用户金额 40, 买两本书,1本 30, 第二本 50, 

1. 事务传播的默认, REQUIRED, 将导致整个购买失败。

2.事务传播 REQUIRED_NEW, 会导致购买第一本成功,第二本失败。

 

 示例1:整个结算事务失败,数据集不变

 

purse 自身是 transaction, propagation 默认是REQUIRED. 其被check() 调用

1. check()  是个事务方法,因此 purse() 行为表现为自身不启用事务,事务的边界便由check() 决定.

2.如果check() 不是事务方法,且过程中不存在既有事务,则purse() 则会开启事务,第一个购买成功,第二个余额不足会失败.

将 check() 变成非事务性方法。

结果是 用户第一本书购买成功,第二本失败. 


示例2: propagation.REQUIRED_NEW

 

check() 默认传播机制,purchase 设定 REQUIRED_NEW. 将会在过程中存在事务时,开启自己的独立事务,并将过程事务挂起。

此结果会使第一本成功扣减,第二本失败.

 一共三个事务

1.checkout() 开启事务

2.遇到第一个 purse() 时, checkOut() 事务挂起, purse() _1 新事务开启,顺利完成提交.此时,purse_1() 事务结束,checkOut() 事务恢复

3.遇到第二个purse() 时, checkout() 事务挂起, purse()_2 新事务开启,不满足check, 事务失败并回滚, checkout()事务恢复,随后结束. 


 隔离事务

同一个应用或不同应用的多个事务同时操纵同一个数据集时,数据不安全。要设置事务隔离方式

并发事务问题4类

1.脏读。事务T1,T2, T2读取了T1更新但未提交的字段,T1 回滚,则T2 读取的字段变成了临时且无效的.

2.不可重复读,T2读取了一个字段,随后T1更新了这个字段,值不同,(值相同是不可预测的),T2如果再次读取该值,则是不同的。

3.幻读 phantom read,T2 从表中读取一些行,然后T1 向该表插入一些行, T2再次读取该表,则会新行. 表的数据改变,

       幻读类似不可重复读,不过它涉及多行.

4.丢失更新: T2 更新表中的一行数据,随后T1也更新了该一行数据,数据覆盖。

 


事务隔离要由底层数据库引擎支持, 不应由应用或框架支持.

Java.sql.connection   setTransactionIsolation() 方法修改 JDBC 连接的隔离级别.

Spring 支持的5中隔离级别

1.Default ,由底层数据库默认的隔离级别。大多数默认是 READ_COMMITTED.

2.READ_UNCOMMITED: 一个事务可读取其他事务尚提交的更改;

            可能出现脏读,不可重复读,幻读问题.

3.READ_COMMITED:      一个事务只能读到其他事务已经提交的更改,

            可避免脏读,不能避免 不可重复读 和 幻读

4.REPEATABLE_READ:   一个事务能多次从一个字段读到相同的值。在该事务期间禁止其他事务对该字段进行更新.

            可避免脏读,不可重复读,不可避免幻读.

5.SERIALIZABLE:        可避免所有并发问题, 性能最差.

               确保一个事务能够多次从一张表中读到相同的行,在该事务期间禁止其他事务对该表进行插入,更新和删除操作。


示例1: Isolation READ_UNCOMMIT , 未提交读,会导致 脏读,不可重复读,幻读,更新丢失.

 

 

@Component("bookShop")
@EnableAutoConfiguration
public class JdbcBookShop extends JdbcDaoSupport implements BookShop {

  public JdbcBookShop(DataSource dataSource) {
    setDataSource(dataSource);
  }

  @Transactional
  @Override
  public void purchase(final String isbn, final String userName) {
    int price =
        getJdbcTemplate()
            .queryForObject("SELECT PRICE FROM BOOK WHERE ISBN = ?", Integer.class, isbn);

    getJdbcTemplate().update("UPDATE BOOK_STOCK SET STOCK = STOCK - 1 WHERE ISBN = ?", isbn);

    getJdbcTemplate()
        .update("UPDATE ACCOUNT SET BALANCE = BALANCE - ? WHERE USERNAME = ?", price, userName);
  }

  @Transactional
  @Override
  public void increaseStock(String isbn, int stock) {
    String threadName = Thread.currentThread().getName();
    System.out.println((threadName + " - Prepare to increase stock"));

    getJdbcTemplate().update(
            "UPDATE BOOK_STOCK SET STOCK = STOCK + ? WHERE ISBN = ?",
            stock,isbn);

    System.out.println((threadName + " - Book stock increased by " + stock));
    sleep(threadName);

    System.out.println((threadName + " - Book stock transaction rolled back"));
    throw new RuntimeException("Increased by mistake");
  }

  @Transactional(isolation = Isolation.READ_UNCOMMITTED)
  @Override
  public int checkShock(String isbn) {
    String threadName = Thread.currentThread().getName();
    System.out.println((threadName + " - Prepare to check book stock"));

    int stock = getJdbcTemplate().queryForObject(
            "SELECT STOCK FROM BOOK_STOCK WHERE ISBN = ?",
            Integer.class,isbn);

    System.out.println((threadName + " - Book stock is " + stock));
    sleep(threadName);
    return stock;
  }

  private void sleep(String threadName){
    System.out.println((threadName + " - Sleeping"));

    try{
      Thread.sleep(10000);
    }catch (InterruptedException e){
    }
    System.out.println(threadName+" - Wake up");
  }
}
JdbcShopImpl

并发访问:

@SpringBootApplication
@EnableTransactionManagement
public class SpringTransactionApplication {

  public static void main(String[] args) {
    SpringApplication.run(SpringTransactionApplication.class, args);
  }

  @Component
  class App implements ApplicationRunner{
    @Autowired
    BookShop bookShop;

    @Override
    public void run(ApplicationArguments args) throws Exception {
      ExecutorService executorService = Executors.newFixedThreadPool(3);

      Runnable increaseTask = new Runnable() {
        @Override
        public void run() {
          try{
            Thread.currentThread().setName("Thread 1");
            bookShop.increaseStock("0001",5);
          }catch (RuntimeException e){}
        };
      };

      Runnable checkStockTask =new Runnable() {
        @Override
        public void run() {
          Thread.currentThread().setName("Thread 2");
          bookShop.checkShock("0001");
        }
      };

      executorService.execute(increaseTask);
      try{
        Thread.sleep(5000);
      }catch (InterruptedException e){
      }
      executorService.execute(checkStockTask);
    }
  }
}
View Code

 

thread2 脏读 thread1 的临时数据15,实际回滚后仍然为10.

解决办法是:  Isolation.READ_COMMITTED

对于支持该隔离级别的数据库来说:需要获取到已更新但尚未提交的行的更新锁,

其他事务在读取该行时需要等待锁释放。

更新锁的释放出现在加锁的事务提交或回滚时。

 

再次运行,Thread2 会在 Thread1 回滚事务,事务完成后再读取.


示例2: 不可重复读:

 

线程1读取库存,得到数据10, 然后休眠,

这时,线程1的事务尚未提交,当线程1休眠时,线程2启动事务增加了库存。READ_COMMITTED 级别下,

线程2 更新了尚未提交的事务1所读取的库存值。

如果线程1再次读取库存,与第一次读取的值是不同的,这就是 non-repeatable read. 因为事务会读取到同一个字段的不同值。 

解决办法:Isolation.REPEATBALE_READ

底层数据对 Repeatable_READ 的实现:获取已读但尚未提交的行的读锁

其他的事务在更新该行时需要等待,直到读锁释放.

读锁的释放出现在 加锁的事务提交或回滚时。 


示例3: 幻读:一个事务从一张表读取几行后,另一个事务向同一张表插入几个新行.

第一个事务随后再此读取相同表,则发现数据变化. 

解决:隔离级别最高 Serializable。. 在整张表上获取读锁. 

 

 

public interface BookShop {
    void purchase(String isbn,String userName);

    void increaseStock(String isbn,int stock);
    public int checkShock(String isbn);
}
Bookshop interface

 

@Component("bookShop")
@EnableAutoConfiguration
public class JdbcBookShop extends JdbcDaoSupport implements BookShop {

  public JdbcBookShop(DataSource dataSource) {
    setDataSource(dataSource);
  }

  @Transactional
  @Override
  public void purchase(final String isbn, final String userName) {
    int price =
        getJdbcTemplate()
            .queryForObject("SELECT PRICE FROM BOOK WHERE ISBN = ?", Integer.class, isbn);

    getJdbcTemplate().update("UPDATE BOOK_STOCK SET STOCK = STOCK - 1 WHERE ISBN = ?", isbn);

    getJdbcTemplate()
        .update("UPDATE ACCOUNT SET BALANCE = BALANCE - ? WHERE USERNAME = ?", price, userName);
  }

  @Transactional
  @Override
  public void increaseStock(String isbn, int stock) {
    String threadName = Thread.currentThread().getName();
    System.out.println((threadName + " - Prepare to increase stock"));

    getJdbcTemplate().update(
            "UPDATE BOOK_STOCK SET STOCK = STOCK + ? WHERE ISBN = ?",
            stock,isbn);

    System.out.println((threadName + " - Book stock increased by " + stock));
    sleep(threadName);

/*    System.out.println((threadName + " - Book stock transaction rolled back"));
    throw new RuntimeException("Increased by mistake");*/
    int currentStock = getJdbcTemplate().queryForObject(
            "SELECT STOCK FROM BOOK_STOCK WHERE ISBN = ?",
            Integer.class,isbn);
    System.out.println((threadName + " - current stock is " + currentStock));
  }

  @Transactional(isolation = Isolation.SERIALIZABLE)
  @Override
  public int checkShock(String isbn) {
    String threadName = Thread.currentThread().getName();
    System.out.println((threadName + " - Prepare to check book stock"));

    int stock = getJdbcTemplate().queryForObject(
            "SELECT STOCK FROM BOOK_STOCK WHERE ISBN = ?",
            Integer.class,isbn);

    System.out.println((threadName + " - Book stock is " + stock));
    sleep(threadName);

    return stock;
  }

  private void sleep(String threadName){
    System.out.println((threadName + " - Sleeping"));

    try{
      Thread.sleep(10000);
    }catch (InterruptedException e){
    }
    System.out.println(threadName+" - Wake up");
  }
}
JdbcShopImpl
@SpringBootApplication
@EnableTransactionManagement
public class SpringTransactionApplication {

  public static void main(String[] args) {
    SpringApplication.run(SpringTransactionApplication.class, args);
  }

  @Component
  class App implements ApplicationRunner{
    @Autowired
    BookShop bookShop;

    @Override
    public void run(ApplicationArguments args) throws Exception {
      ExecutorService executorService = Executors.newFixedThreadPool(3);

      Runnable increaseTask = new Runnable() {
        @Override
        public void run() {
          try{
            Thread.currentThread().setName("Thread 1");
            bookShop.increaseStock("0001",5);
          }catch (RuntimeException e){}
        };
      };

      Runnable checkStockTask =new Runnable() {
        @Override
        public void run() {
          Thread.currentThread().setName("Thread 2");
          bookShop.checkShock("0001");
        }
      };

      executorService.execute(checkStockTask);
      try{
        Thread.sleep(5000);
      }catch (InterruptedException e){
      }
      executorService.execute(increaseTask);
    }
  }
}
View Code

 

 


回滚事务属性:

默认只有未检查异常 ( 即 RuntimeException 或 Error ) 会导致事务回滚,

而检查异常则不会.

更改设置:自定义哪些异常会导致事务回滚,未显式指定的任何异常都会按照默认规则处理.


 

超时 和 只读

事务需要获取行和表的锁,因此长时间事务会占用资源并对整体性能产生影响.

只读属性会让数据库引擎优化事务。

timeout 事务 整数,单位秒, 表示事务在强制回滚前会持续多久.防止长事务.

read-only 只读不更新数据,如果出现了写操作,资源不一定会失败。


 

加载期编织 管理事务

默认 Spring 声明式事务时通过 AOP 框架开启。限制: Ioc容器中的bean的 public 方法添加通知,使用 Spring AOP 只能在这个范围内管理事务.

如果要管理 非 public 方法,或在 Spring IoC 容器外创建的对象的事务。

则: AnnotationTransactionAspect 的 Aspect 切面. 可管理任何对象的任何方法事务。该切面会管理使用了 @Transactional 注解的任何方法的事务。

可选择 AspectJ 的编译期编织或加载期编织来启用该切面.

要将该切面在加载期织入到领域类中,配置类上 @EnableLoadTimeWeaving 注解

@EnableTransactionManagement注解 model 两个值

ASPECTJ : 容器应该使用加载期或编译期编织来启用事务通知,要求 spring-instrument JAR 来类路径上,同时在加载器和编译期配置。

PROXY: 容器要使用 Spring AOP 机制。

为切面提供一个事务管理器。 默认寻找 transactionManager 名的

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.7</version>
</dependency>

<!-- 启用加载期编织,需引入一个Java代理,位于以下模块-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-instrument</artifactId>
<version>6.0.7</version>
</dependency>


 

posted @ 2023-04-05 09:54  君子之行  阅读(17)  评论(0)    收藏  举报