原文地址:Spring学习笔记 - 第三章 - AOP与Spring事务

Spring 学习笔记全系列传送门:

  • Spring学习笔记 - 第一章 - IoC(控制反转)、IoC容器、Bean的实例化与生命周期、DI(依赖注入)

  • Spring学习笔记 - 第二章 - 注解开发、配置管理第三方Bean、注解管理第三方Bean、Spring 整合 MyBatis 和 Junit 案例

  • 【本章】Spring学习笔记 - 第三章 - AOP与Spring事务

目录
  • 1、AOP 简介

    • 1.1 什么是 AOP
    • 1.2 AOP 的作用
    • 1.3 AOP 核心概念
  • 2、AOP 入门案例

    • 2.1 需求分析
    • 2.2 思路分析
    • 2.3 环境准备
    • 2.4 AOP 实现步骤
    • 2.5 相关知识点
  • 3、AOP 工作流程与核心概念

    • 3.1 AOP 工作流程
    • 3.2 AOP 核心概念
  • 4、AOP 配置管理

    • 4.1 AOP 切入点表达式

      • 4.1.1 语法格式
      • 4.1.2 通配符
      • 4.1.3 书写技巧
    • 4.2 AOP 通知类型

      • 4.2.1 类型介绍
      • 4.2.2 环境准备
      • 4.2.3 通知类型的使用

        • 4.2.3.1 通知类型
        • 4.2.3.2 通知类型相关知识点总结
    • 4.3 案例:测试业务层接口万次执行效率

      • 4.3.1 需求分析
      • 4.3.2 环境准备
      • 4.3.3 功能开发
    • 4.4 AOP 通知获取数据

      • 4.4.1 环境准备
      • 4.4.2 获取参数

        • 4.4.2.1 非环绕通知获取方式
        • 4.4.2.2 环绕通知获取方式
      • 4.4.3 获取返回值

        • 4.4.3.1 环绕通知获取返回值
        • 4.4.3.2 非环绕通知获取返回值
      • 4.4.4 获取异常

        • 4.4.4.1 环绕通知获取异常
        • 4.4.4.2 抛出异常后通知获取异常
    • 4.5 百度网盘密码数据兼容处理

      • 4.5.1 需求分析
      • 4.5.2 环境准备
      • 4.5.3 具体实现
  • 5、AOP 总结

    • 5.1 AOP 的核心概念
    • 5.2 切入点表达式
    • 5.3 五种通知类型
    • 5.4 通知中获取参数、返回值以及异常信息
  • 6、AOP 事务管理

    • 6.1 Spring 事务简介及案例

      • 6.1.1 相关概念介绍
      • 6.1.2 转账案例

        • 6.1.2.1 需求分析
        • 6.1.2.2 环境搭建
        • 6.1.2.3 事务管理
        • 6.1.2.4 相关知识点
    • 6.2 Spring 事务角色
    • 6.3 Spring 事务属性及案例

      • 6.3.1 事务配置
      • 6.3.2 转账业务追加日志案例

        • 6.3.2.1 需求分析
        • 6.3.2.2 环境准备
      • 6.3.3 事务传播行为

        • 6.3.3.1 修改 logService 改变事务的传播行为
        • 6.3.3.2 事务传播行为的可选值

1、AOP 简介

AOP是在不改原有代码的前提下对其进行增强

1.1 什么是 AOP

1.2 AOP 的作用

1.3 AOP 核心概念

AOP的核心概念包含:连接点、切入点、通知、通知类、切面

2、AOP 入门案例

2.1 需求分析

在方法执行前输出当前系统时间。(使用 AOP )

2.2 思路分析

  1. 导入坐标(pom.xml)
  2. 制作连接点(原始操作,Dao接口与实现类)
  3. 制作共性功能(通知类与通知)
  4. 定义切入点
  5. 绑定切入点与通知关系(切面)

2.3 环境准备

环境搭建完成后:

  • 打印save方法的时候,因为方法中有打印系统时间,所以运行的时候是可以看到系统时间
  • 对于update方法来说,就没有该功能

2.4 AOP 实现步骤

  1. 添加依赖

    • AspectJ 是 AOP 思想的一个具体实现

    • Spring 有自己的 AOP 实现,但是相比于 AspectJ 来说比较麻烦,所以我们直接采用Spring 整合 ApsectJ 的方式进行 AOP 开发

    <!-- 因为spring-context依赖aop,aop的包已经导入 -->
    <!-- 此处我们额外导入aspectjweaver包 -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.4</version>
    </dependency>
    
  2. 定义接口与实现类

    环境准备已经完成

  3. 定义通知类和通知

    package priv.dandelion.aop;
    public class MyAdvice {
        public void method(){
            // 获取系统时间
            System.out.println(System.currentTimeMillis());
        }
    }
    
  4. 定义切入点

    • 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑
    • execution及后面编写的内容,将在后续的章节中详细介绍
    public class MyAdvice {
        // 定义切入点,注解中execution后面的内容即为对切入点的描述(即当执行到这个方法的时候)
        @Pointcut("execution(void priv.dandelion.dao.BookDao.update())")
        private void pt(){}
        public void method(){
            System.out.println(System.currentTimeMillis());
        }
    }
    
  5. 制作切面

    绑定切入点与通知关系

    public class MyAdvice {
        @Pointcut("execution(void priv.dandelion.dao.BookDao.update())")
        private void pt(){}
        // 定义执行时间(在运行到哪一个切入点的什么时间执行,如此处使用Before就是在运行pt()切入点前执行)
        @Before("pt()")
        public void method(){
            System.out.println(System.currentTimeMillis());
        }
    }
    
  6. 将通知类配给容器并标识其为切面类

    // 告诉Spring,这是一个Bean,需要被管理
    @Component
    // 告诉Spring,扫描到该Bean时,当作AOP处理
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(void priv.dandelion.dao.BookDao.update())")
        private void pt(){}
        @Before("pt()")
        public void method(){
            System.out.println(System.currentTimeMillis());
        }
    }
    
  7. 开启注解格式 AOP 功能

    @Configuration
    @ComponentScan("priv.dandelion")
    // 告诉Spring,包含注解开发的AOP,即启动了@Aspect注解
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
    

2.5 相关知识点

3、AOP 工作流程与核心概念

SpringAOP的本质:代理模式

3.1 AOP 工作流程

AOP 是基于 Spring 容器管理的 bean 做的增强,所以整个工作过程需要从 Spring 加载 bean 说起

3.2 AOP 核心概念

4、AOP 配置管理

4.1 AOP 切入点表达式

  • 切入点表达式在上面的章节出现过,即@Pointcut中的内容

    @Pointcut("execution(void priv.dandelion.dao.BookDao.update())")
    private void pt(){}
    
  • 对于AOP中的切入点表达式,本文将详细说明三个内容,分别是语法格式、通配符和书写技巧

4.1.1 语法格式

4.1.2 通配符

4.1.3 书写技巧

4.2 AOP 通知类型

前面的案例中,涉及到如下内容:

@Before("pt()")

其代表的含义是将通知添加到切入点方法执行的前面

4.2.1 类型介绍

4.2.2 环境准备

4.2.3 通知类型的使用

4.2.3.1 通知类型
4.2.3.2 通知类型相关知识点总结

4.3 案例:测试业务层接口万次执行效率

4.3.1 需求分析

4.3.2 环境准备

4.3.3 功能开发

4.4 AOP 通知获取数据

  • 获取参数(所有的通知类型都可以获取参数)
    • JoinPoint:适用于前置、后置、返回后、抛出异常后通知
    • ProceedingJoinPoint:适用于环绕通知
  • 获取返回值(前置和抛出异常后通知没有返回值,后置通知可有可无,所以不做研究)
    • 返回后通知
    • 环绕通知
  • 获取异常信息(获取切入点方法运行异常信息,前置和返回后通知不会有,后置通知可有可无,所以不做研究)
    • 抛出异常后通知
    • 环绕通知

4.4.1 环境准备

4.4.2 获取参数

4.4.2.1 非环绕通知获取方式
@Before("pt()")
public void before(JoinPoint jp) {
    Object[] args = jp.getArgs();
    System.out.println(Arrays.toString(args));
    System.out.println("before advice ..." );
}
@After("pt()")
public void after() {
    Object[] args = jp.getArgs();
    System.out.println(Arrays.toString(args));
    System.out.println("after advice ...");
}
4.4.2.2 环绕通知获取方式

4.4.3 获取返回值

只有返回后AfterReturing和环绕Around这两个通知类型可以获取返回值

4.4.3.1 环绕通知获取返回值

环绕通知获取返回值时,在调用原始方法时接收返回值,并且为通知方法定义返回值类型 Object,如果有需要可以进行修改

@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
    Object[] args = pjp.getArgs();
    System.out.println(Arrays.toString(args));
    // 假设原始方法被调用时传入的参数有问题,此处进行处理
    args[0] = 666;
    // 手动传入参数,执行原始方法时将使用被手动传入的参数
    Object ret = pjp.proceed(args);
    return ret;
}
4.4.3.2 非环绕通知获取返回值

4.4.4 获取异常

  • 对于获取抛出的异常,只有抛出异常后AfterThrowing和环绕Around这两个通知类型可以获取

  • 假设 Dao 层代码中存在异常

    @Repository
    public class BookDaoImpl implements BookDao {
        public String findName(int id, String password) {
            System.out.println("id:"+id);
            if(true){
                throw new NullPointerException();
            }
            return "dandelion";
        }
    }
    
4.4.4.1 环绕通知获取异常
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
    Object[] args = pjp.getArgs();
    System.out.println(Arrays.toString(args));
    // 假设原始方法被调用时传入的参数有问题,此处进行处理
    args[0] = 666;
    Object ret = null;
    try {
        // 手动传入参数,执行原始方法时将使用被手动传入的参数
        ret = pjp.proceed(args);
    } catch (Throwable t) {
        // 捕获异常
        t.printStackTrace();
    }
    return ret;
}
4.4.4.2 抛出异常后通知获取异常
@AfterThrowing(value = "pt()", throwing = "t")
public void afterThrowing(Throwable t) {
    System.out.println("afterThrowing advice ..." + t);
}

4.5 百度网盘密码数据兼容处理

4.5.1 需求分析

百度网盘选取分享密码时会多带一个空格,影响正常使用

4.5.2 环境准备

4.5.3 具体实现

  1. 开启 SpringAOP 的注解功能

    @EnableAspectJAutoProxy
    
  2. 编写通知类

    @Component
    @Aspect
    public class DataAdvice {
    }
    
  3. 定义切入点

    // 定义切入点,假设对业务层任意包含字符串的参数都进行处理
    @Pointcut("execution(boolean priv.dandelion.service.*Service.*(..))")
    private void servicePt(){}
    
  4. 添加环绕通知

    @Around("servicePt()")
    public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
        // 获取参数,若参数为字符串类型,则去除空格
        Object[] args = pjp.getArgs();
        for (int i = 0; i < args.length; i++) {
            if (args[i].getClass().equals(String.class))
                args[i] = args[i].toString().trim();
        }
        // 调用原始方法,手动传入修改后的参数
        Object proceed = pjp.proceed(args);
        return proceed;
    }
    

5、AOP 总结

5.1 AOP 的核心概念

5.2 切入点表达式

5.3 五种通知类型

5.4 通知中获取参数、返回值以及异常信息

6、AOP 事务管理

6.1 Spring 事务简介及案例

6.1.1 相关概念介绍

6.1.2 转账案例

6.1.2.1 需求分析
6.1.2.2 环境搭建
  1. 数据库

    create database spring_db character set utf8;
    use spring_db;
    create table tbl_account(
        id int primary key auto_increment,
        name varchar(35),
        money double
    );
    insert into tbl_account values(1,'Tom',1000);
    insert into tbl_account values(2,'Jerry',1000);
    
  2. 创建项目导入 jar 包

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
    </dependencies>
    
  3. 根据表创建模型类

    public class Account implements Serializable {
        private Integer id;
        private String name;
        private Double money;
    	// setter
        // getter
        // toString    
    }
    
  4. Dao接口

    public interface AccountDao {
        @Update("update tbl_account set money = money + #{money} where name = #{name}")
        void inMoney(@Param("name") String name, @Param("money") Double money);
        @Update("update tbl_account set money = money - #{money} where name = #{name}")
        void outMoney(@Param("name") String name, @Param("money") Double money);
    }
    
  5. Service 接口和实现类(接口不表)

    @Service
    public class AccountServiceImpl implements AccountService {
        @Autowired
        private AccountDao accountDao;
        @Override
        public void transfer(String out,String in ,Double money) {
            accountDao.outMoney(out,money);
            accountDao.inMoney(in,money);
        }
    }
    
  6. jdbc.properties 配置文件

    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
    jdbc.username=root
    jdbc.password=123456
    
  7. JDBCConfig 配置类

    public class JdbcConfig {
        @Value("${jdbc.driver}")
        private String driver;
        @Value("${jdbc.url}")
        private String url;
        @Value("${jdbc.username}")
        private String userName;
        @Value("${jdbc.password}")
        private String password;
        @Bean
        public DataSource dataSource(){
            DruidDataSource ds = new DruidDataSource();
            ds.setDriverClassName(driver);
            ds.setUrl(url);
            ds.setUsername(userName);
            ds.setPassword(password);
            return ds;
        }
    }
    
  8. MybatisConfig 配置类

    public class MybatisConfig {
        @Bean
        public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
            SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
            ssfb.setTypeAliasesPackage("priv.dandelion.entity");
            ssfb.setDataSource(dataSource);
            return ssfb;
        }
        @Bean
        public MapperScannerConfigurer mapperScannerConfigurer(){
            MapperScannerConfigurer msc = new MapperScannerConfigurer();
            msc.setBasePackage("priv.dandelion.dao");
            return msc;
        }
    }
    
  9. SpringConfig 配置类

    @Configuration
    @ComponentScan("priv.dandelion")
    @PropertySource("classpath:jdbc.properties")
    @Import({JdbcConfig.class,MybatisConfig.class})
    public class SpringConfig {
    }
    
  10. Service 测试类

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = SpringConfig.class)
    public class AccountServiceTest {
        @Autowired
        private AccountService accountService;
        @Test
        public void testTransfer() throws IOException {
            accountService.transfer("Tom","Jerry",100D);
        }
    }
    
6.1.2.3 事务管理
  1. 再需要被事务管理的方法上添加@Transactional注解(一般加在接口而非实现类中)

    public interface AccountService {
        /**
         * 转账操作
         * @param out 传出方
         * @param in 转入方
         * @param money 金额
         */
        @Transactional
        public void transfer(String out,String in ,Double money);
    }
    
  2. 在 JDBCConfig 类中配置事务管理器

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        // 需要其他的Bean,直接写参数,详见Bean的注入
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
    
  3. 开启事务注解 @EnableTransactionManagement

    @Configuration
    @ComponentScan("priv.dandelion")
    @PropertySource("classpath:jdbc.properties")
    @Import({JdbcConfig.class,MybatisConfig.class})
    @EnableTransactionManagement
    public class SpringConfig {
    }
    
6.1.2.4 相关知识点

6.2 Spring 事务角色

6.3 Spring 事务属性及案例

6.3.1 事务配置

补充:

  • 事务仅在发生 Error运行时异常 时才能正常回滚
  • 此外的异常若想正常回滚需要设置 rollbackFor 属性
属性 作用 示例
readOnly 设置是否为只读事务 readOnly=true只读,默认非只读
timeOut 设置事务超时时间 值为 -1 表示永不超时
rollbackFor 设置遇到异常时回滚事务 rollbackFor={异常1.class, 异常2.class}
rollbackForClassName 设置遇到异常时回滚事务 参数为字符串列表,是异常的名称
noRollbackFor 设置不回滚的异常 参数为.class
noRollbackForClassName 设置不回滚的异常 参数为字符串,异常名称
propagation 设置事务传播行为 (详见6.3.2)

6.3.2 转账业务追加日志案例

6.3.2.1 需求分析

禁止事务协调员的事务加入事务管理者的事务即可

6.3.2.2 环境准备
  1. 创建日志表

    create table tbl_log(
       id int primary key auto_increment,
       info varchar(255),
       createDate datetime
    )
    
  2. 添加 LogDao 接口

    public interface LogDao {
        @Insert("insert into tbl_log (info,createDate) values(#{info},now())")
        void log(String info);
    }
    
  3. 添加 LogService 接口与实现类

    @Service
    public class LogServiceImpl implements LogService {
        @Autowired
        private LogDao logDao;
    	@Transactional
        public void log(String out,String in,Double money ) {
            logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
        }
    }
    
  4. 在转账的业务中添加记录日志

    @Service
    public class AccountServiceImpl implements AccountService {
        @Autowired
        private AccountDao accountDao;
        @Autowired
        private LogService logService;
    	@Transactional
        public void transfer(String out,String in ,Double money) {
            try{
                accountDao.outMoney(out,money);
                accountDao.inMoney(in,money);
            }finally {
                logService.log(out,in,money);
            }
        }
    }
    

6.3.3 事务传播行为

  • 代码中,outMoney()inMoney()、三个为独立事务,但是都加入了事务管理者transfer()开启的事务中,合并为一个整体,出现异常时全部回滚,无法记录日志

    @Transactional
    public void transfer(String out,String in ,Double money) {
        try{
            accountDao.outMoney(out,money);
            accountDao.inMoney(in,money);
        }finally {
            logService.log(out,in,money);
        }
    }
    
  • 可以将log()对应的事务独立出来,不参与与事务管理者transfer()开启的的事务合并,自然可以不随之而回滚,可在任何时候完成事务

6.3.3.1 修改 logService 改变事务的传播行为
@Service
public class LogServiceImpl implements LogService {
    @Autowired
    private LogDao logDao;
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(String out,String in,Double money ) {
        logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
    }
}
6.3.3.2 事务传播行为的可选值
传播属性 事务管理员 事务协调员
REQUIRED(默认) 开启事务T 加入事务T
:: 新建事务T2
REQUIRES_NEW 开启事务T 另新建事务T2
:: 自主新建事务T2
SUPPORTS 开启事务T 加入事务T
:: 跟随事务管理员,不创建事务
NOT_SUPPORTED 开启事务T 无视事务管理员,不加入事务
::
MANDATORY 开启事务T 加入事务T
:: ERROR,事务管理员必须有事务
NEVER 开启事务T ERROR,事务管理员不可有事务
::
NESTED 设置savePoint,一旦回滚将回滚到存档点,有客户响应提交 / 回滚

发表回复