Spring(3)Spring从零到入门 - Spring整合技术及AOP事务管理

发布时间:2023年12月24日

Spring(3)Spring从零到入门 - Spring整合技术及AOP事务管理

4 Spring整合技术示例

4.1 Spring整合Mybatis

4.1.1 Mybatis开发回顾

步骤1:准备数据库表

Mybatis是来操作数据库表,所以先创建一个数据库及表

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
);

步骤2:创建项目导入jar包

项目的pom.xml添加相关依赖

<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>
</dependencies>

步骤3:根据表创建模型类

public class Account implements Serializable {

    private Integer id;
    private String name;
    private Double money;
	//setter...getter...toString...方法略    
}

回顾**Serializable**

Serializable 是一个标记接口,继承了这个接口,相关对象可实现序列化

什么场景中会涉及到将对象进行**序列化(Serializable)**

  1. 分布式系统: 在分布式系统中,不同的服务可能运行在不同的物理机器上。为了在这些服务之间传递对象,需要将对象转换成字节序列,以便在网络上传输。这是一种常见的序列化应用场景。
  2. 缓存: 在将对象存储到缓存中时,有些缓存系统要求存储的对象是可序列化的。这样可以在需要时将对象保存到缓存中,以提高数据访问性能。
  3. 消息队列: 在消息队列系统中,消息通常需要被序列化以进行传输。生产者将对象序列化为消息,而消费者则负责将消息反序列化为对象。

步骤4:创建Dao接口

public interface AccountDao {

    @Insert("insert into tbl_account(name,money)values(#{name},#{money})")
    void save(Account account);

    @Delete("delete from tbl_account where id = #{id} ")
    void delete(Integer id);

    @Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ")
    void update(Account account);

    @Select("select * from tbl_account")
    List<Account> findAll();

    @Select("select * from tbl_account where id = #{id} ")
    Account findById(Integer id);
}

步骤5:创建Service接口和实现类

public interface AccountService {

    void save(Account account);

    void delete(Integer id);

    void update(Account account);

    List<Account> findAll();

    Account findById(Integer id);

}

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    public void save(Account account) {
        accountDao.save(account);
    }

    public void update(Account account){
        accountDao.update(account);
    }

    public void delete(Integer id) {
        accountDao.delete(id);
    }

    public Account findById(Integer id) {
        return accountDao.findById(id);
    }

    public List<Account> findAll() {
        return accountDao.findAll();
    }
}

步骤6:添加jdbc.properties文件

resources目录下添加,用于配置数据库连接四要素

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=1234

useSSL:关闭MySQL的SSL连接

在MySQL中,**SSL(Secure Sockets Layer)连接**是通过使用SSL协议来加密和保护数据库连接的一种方式。SSL是一种用于在计算机网络上进行安全通信的协议,它使用加密算法来确保数据在传输过程中的机密性和完整性。对于数据库连接,使用SSL可以有效地保护敏感信息,防止在传输过程中被窃听或篡改。

使用SSL连接MySQL的过程包括以下步骤:

  1. 生成SSL证书和私钥
  2. 配置MySQL服务器
  3. 配置MySQL客户端

步骤7:添加Mybatis核心配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--读取外部properties配置文件-->
    <properties resource="jdbc.properties"></properties>
    <!--别名扫描的包路径-->
    <typeAliases>
        <package name="com.ibaidu.domain"/>
    </typeAliases>
    <!--数据源-->
    <environments default="mysql">
        <environment id="mysql">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"></property>
                <property name="url" value="${jdbc.url}"></property>
                <property name="username" value="${jdbc.username}"></property>
                <property name="password" value="${jdbc.password}"></property>
            </dataSource>
        </environment>
    </environments>
    <!--映射文件扫描包路径-->
    <mappers>
        <package name="com.ibaidu.dao"></package>
    </mappers>
</configuration>
  • 第一行读取外部properties配置文件,Spring有提供具体的解决方案@PropertySource,需要交给Spring

  • 第二行起别名,包扫描,为SqlSessionFactory服务的,需要交给Spring

    别名配置允许你给Java类设置一个别名

    <typeAliases>
     <typeAlias alias="User" type="com.example.User"/>
    </typeAliases>
    

    上述配置将com.example.User类设置别名为User,之后在映射文件中可以使用<resultMap><parameterMap>等元素时,可以使用User代替完整的类名

    包扫描(Type Aliases Package)

    <typeAliases>
     <package name="com.example.domain"/>
    </typeAliases>
    

    包扫描配置允许你指定一个包名,MyBatis将会自动扫描该包下的所有类,并将这些类设置为别名。这样,你在映射文件中引用类时,只需使用类名,而不必配置每个类的别名

  • 第三行主要用于做连接池,Spring之前我们已经整合了Druid连接池,这块也需要交给Spring

  • 前面三行一起都是为了创建SqlSession对象用的,SqlSession是由SqlSessionFactory创建出来的,所以只需要将SqlSessionFactory交给Spring管理即可。

  • 第四行是Mapper接口和映射文件[如果使用注解就没有该映射文件],这个是在获取到SqlSession以后执行具体操作的时候用,所以它和SqlSessionFactory创建的时机都不在同一个时间可能需要单独管理

    这段XML配置是MyBatis中用于配置映射器(mappers)的一部分。具体来说,它通过package元素指定了映射器接口所在的包名,MyBatis将会扫描该包下的所有映射器接口,并自动加载它们

步骤8:编写应用程序

public class App {
    public static void main(String[] args) throws IOException {
        // 1. 创建SqlSessionFactoryBuilder对象
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        // 2. 加载SqlMapConfig.xml配置文件
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        // 3. 创建SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        // 4. 获取SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 5. 执行SqlSession对象执行查询,获取结果User
        AccountDao accountDao = sqlSession.getMapper(AccountDao.class);

        Account ac = accountDao.findById(1);
        System.out.println(ac);

        // 6. 释放资源
        sqlSession.close();
    }
}

SqlSessionFactoryBuilder的主要作用是读取MyBatis的配置信息并构建出一个SqlSessionFactory对象

SqlSessionFactory:是“生产”SqlSession的“工厂”

SqlSession:代表Java程序和数据库之间的会话。(HttpSession是Java程序和浏览器之间的会话)

工厂模式:如果创建某一个对象,使用的过程基本固定,那么我们就可以把创建这个对象的相关代码封装到一个“工厂类”中,以后都使用这个工厂类来“生产”我们需要的对象。

步骤9:运行程序

image-20231221142106523

4.1.2 整合Spring分析

Mybatis的基础环境我们已经准备好了,接下来就得分析下在上述的内容中,哪些对象可以交给Spring来管理?

image-20231221143149306

真正需要交 给Spring管理的是**SqlSessionFactory**

4.1.3 Spring整合Mybatis

前面我们已经分析了Spring与Mybatis的整合,大体需要做两件事,

第一件事是:Spring要管理MyBatis中的SqlSessionFactory

第二件事是:Spring要管理Mapper接口的扫描

步骤1:项目中导入整合需要的jar包

<dependency>
    <!--Spring操作数据库需要该jar包-->
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>
<dependency>
    <!--
		Spring与Mybatis整合的jar包
		这个jar包mybatis在前面,是Mybatis提供的
	-->
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.0</version>
</dependency>

步骤2:创建Spring的主配置类

//配置类注解
@Configuration
//包扫描,主要扫描的是项目中的AccountServiceImpl类
@ComponentScan("com.ibaidu")
public class SpringConfig {
}

步骤3:创建数据源的配置类

在配置类中完成数据源的创建

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;
    }
}

步骤4:主配置类中读properties并引入数据源配置类

@Configuration
@ComponentScan("com.ibaidu")
@PropertySource("classpath:jdbc.properties")
@Import(JdbcConfig.class)
public class SpringConfig {
}

步骤5:创建Mybatis配置类并配置SqlSessionFactory

public class MybatisConfig {
    //定义bean,SqlSessionFactoryBean,用于产生SqlSessionFactory对象
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        //设置模型类的别名扫描
        ssfb.setTypeAliasesPackage("com.ibaidu.domain");
        //设置数据源
        ssfb.setDataSource(dataSource);
        return ssfb;
    }
    //定义bean,返回MapperScannerConfigurer对象
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.ibaidu.dao");
        return msc;
    }
}

说明:

  • 使用SqlSessionFactoryBean封装SqlSessionFactory需要的环境信息

    1630138835057

    • SqlSessionFactoryBean是前面我们讲解FactoryBean的一个子类,在该类中将SqlSessionFactory的创建进行了封装,简化对象的创建,我们只需要将其需要的内容设置即可。
  • 使用MapperScannerConfigurer加载Dao接口,创建代理对象保存到IOC容器中

    1630138916939

    • 这个**MapperScannerConfigurer对象也是MyBatis提供的专用于整合的jar包中的类**,用来处理原始配置文件中的mappers相关配置,加载数据层的Mapper接口类
    • MapperScannerConfigurer有一个核心属性basePackage,就是用来设置所扫描的包路径

步骤6:主配置类中引入Mybatis配置类

@Configuration
@ComponentScan("com.ibaidu")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}

Import中的加载顺序

JdbcConfig.classMybatisConfig.class的代码顺序并不会影响它们的加载顺序;Spring容器会根据配置类的依赖关系和其他条件,以一种合适的顺序加载它们。

步骤7:编写运行类

在运行类中,从IOC容器中获取Service对象,调用方法获取结果

public class App2 {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);

        AccountService accountService = ctx.getBean(AccountService.class);

        Account ac = accountService.findById(1);
        System.out.println(ac);
    }
}

步骤8:运行程序

image-20231221150725698

支持Spring与Mybatis的整合就已经完成了,其中主要用到的两个类分别是:

  • SqlSessionFactoryBean
  • MapperScannerConfigurer

4.2 Spring整合Junit

Junit是一个搞单元测试用的工具,它不是我们程序的主体,也不会参加最终程序的运行;从作用上来说就和之前的东西不同,它不是做功能的,更像是一个辅助工具

4.2.1 环境准备

步骤1:引入依赖

pom.xml

<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>
4.2.2 整合Junit步骤

在test\java下创建一个AccountServiceTest,这个名字任意

//设置类运行器
@RunWith(SpringJUnit4ClassRunner.class)
//设置Spring环境对应的配置类
@ContextConfiguration(classes = {SpringConfiguration.class}) //加载配置类
//@ContextConfiguration(locations={"classpath:applicationContext.xml"})//加载配置文件
public class AccountServiceTest {
    //支持自动装配注入bean
    @Autowired
    private AccountService accountService;
    @Test
    public void testFindById(){
        System.out.println(accountService.findById(1));

    }
    @Test
    public void testFindAll(){
        System.out.println(accountService.findAll());
    }
}

注意:

  • 单元测试,如果测试的是注解配置类,则使用@ContextConfiguration(classes = 配置类.class)

  • 单元测试,如果测试的是配置文件,则使用@ContextConfiguration(locations={配置文件名,...})

    虽然配置文件本身不包含业务逻辑,但在测试中仍然有一些可以考虑的方面:

    配置项的正确性测试配置文件的存在性检查环境变量和配置文件的协同测试

  • Junit运行后是基于Spring环境运行的,所以Spring提供了一个专用的类运行器,这个务必要设置,这个类运行器就在Spring的测试专用包中提供的,导入的坐标就是这个东西SpringJUnit4ClassRunner

  • 上面两个配置都是固定格式,当需要测试哪个bean时,使用自动装配加载对应的对象,下面的工作就和以前做Junit单元测试完全一样了

知识点1:@RunWith

名称@RunWith
类型测试类注解
位置测试类定义上方
作用设置JUnit运行器
属性value(默认):运行所使用的运行期

知识点2:@ContextConfiguration

名称@ContextConfiguration
类型测试类注解
位置测试类定义上方
作用设置JUnit加载的Spring核心配置
属性classes:核心配置类,可以使用数组的格式设定加载多个配置类
locations:配置文件,可以使用数组的格式设定加载多个配置文件名称

关于测试类的文件位置

在Java项目中,通常会有一些约定俗成的目录结构,用于存放源代码和测试代码。这种目录结构有助于开发者更容易地组织和管理代码。一般而言,测试类(Test Classes)通常存放在与源代码相对应的测试目录

src
├── main
│   └── java
│       └── com
│           └── example
│               └── MyClass.java
└── test
 └── java
     └── com
         └── example
             └── MyClassTest.java

按照这个结构存放,确实一下就可以识别到相关的测试类,和相关的注解:

image-20231221160124360

运行后报错:Class not found: “com.baidu.test.AccountServiceTest”

image-20231221160152671

原因:pom.xml配置中配置了其他的测试类的地址,注释掉之后就不会出错了

image-20231221160758441

运行结果:

image-20231221160838109

如果想要自定义测试类,那么

① 在 pom.xml 文件中,将 <testSourceDirectory> 标签设置为新的测试目录路径。

<build>
 <testSourceDirectory>src/test2/java</testSourceDirectory>
 <!-- 其他构建配置 -->
</build>

② 确保在你的测试类上使用了适当的测试框架(如 JUnit),并在类上添加 @RunWith 注解。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringConfig.class})
public class AccountServiceTest {

 @Autowired
 private AccountService accountService;

 @Test
 public void test01()
 {
     System.out.println(this.accountService.findById(1));
 }
}

**但是!**识别不到这个文件,无法测试:

image-20231221161532105

建议还是将测试类及其代码放在约定俗成相应的文件夹中

对于接口的注入报错

接口AccountDao采用注解的方式实现了具体的方法,所以没有AccountDaoImpl这样的实现类:

image-20231223211718155

所以,相关注入会报错:

image-20231223211810593

但这种情况下,不用管这个报错,程序能够正常执行,因为MybatisConfig中扫描了相关文件,加载Dao接口,创建代理对象保存到IOC容器中

public class MybatisConfig {

 @Bean
 public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
     SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
     ssfb.setTypeAliasesPackage("com.baidu.domain");
     ssfb.setDataSource(dataSource);
     return ssfb;
 }

 @Bean
 public MapperScannerConfigurer mapperScannerConfigurer(){
     MapperScannerConfigurer msc = new MapperScannerConfigurer();
     msc.setBasePackage("com.baidu.dao");
     return msc;
 }
}

5 AOP

Spring有两个核心的概念,一个是IOC/DI,一个是AOP

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

5.1 AOP简介

  • AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构

    OOP(Object Oriented Programming)面向对象编程

  • OOP是一种编程思想,那么AOP也是一种编程思想,编程思想主要的内容就是指导程序员该如何编写程序,所以它们两个是不同的编程范式(Programming paradigm)

  • 作用: 在不惊动原始设计的基础上为其进行功能增强,前面咱们有技术就可以实现这样的功能即代理模式

    如何理解这里的**功能增强**呢?

    @Repository
    public class BookDaoImpl implements BookDao {
        public void save() {
            //记录程序当前执行执行(开始时间)
            Long startTime = System.currentTimeMillis();
            //业务执行万次
            for (int i = 0;i<10000;i++) {
                System.out.println("book dao save ...");
            }
            //记录程序当前执行时间(结束时间)
            Long endTime = System.currentTimeMillis();
            //计算时间差
            Long totalTime = endTime-startTime;
            //输出信息
            System.out.println("执行万次消耗时间:" + totalTime + "ms");
        }
        public void update(){
            System.out.println("book dao update ...");
        }
        public void delete(){
            System.out.println("book dao delete ...");
        }
        public void select(){
            System.out.println("book dao select ...");
        }
    }
    

    于计算万次执行消耗的时间,只有save方法有,可不可以让delete和update方法也有呢?可不可以让select方法保持原来的效果即没有呢

    用Spring的AOP就可以实现。

    image-20231222145926287

    不惊动(改动)原有设计(代码)的前提下,想给谁添加功能就给谁添加。

    这个也就是Spring的理念:无入侵式/无侵入式。前面的注入也具有这样的思想,提供了一个变量,相关的对象就有值了

  • 背后的原理是什么样的呢?(核心概念)

    image-20231222150610654

    连接点:类里面哪些方法可以被增强,这些方法称为连接点

    连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等

    • 在SpringAOP中,理解为方法的执行

    切入点:实际被真正增强的方法,称为切入点

    切入点(Pointcut): 匹配连接点的式子

    • 在SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
      • 一个具体的方法:如com.baidu.dao包下的BookDao接口中的无形参无返回值的save方法
      • 匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
    • 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。

    通知(增强):实际增强的逻辑部分称为通知(增强);通知有多种类型:前置通知,后置通知,环绕通知,异常通知,最终通知

    通知(Advice): 在切入点处执行的操作,也就是共性功能

    • 在SpringAOP中,功能最终以方法的形式呈现

    通知类:通知是一个方法,方法不能独立存在需要被写在一个类中,即通知类

    切面:是动作,把通知应用到切入点的过程,个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚

5.2 AOP入门案例

使用SpringAOP的注解方式完成在方法执行前添加打印出当前系统时间的功能

5.2.1 环境准备
  • pom.xml添加Spring依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
    </dependencies>
    
  • 添加BookDao和BookDaoImpl类

    public interface BookDao {
        public void save();
        public void update();
    }
    
    @Repository
    public class BookDaoImpl implements BookDao {
    
        public void save() {
            System.out.println(System.currentTimeMillis());
            System.out.println("book dao save ...");
        }
    
        public void update(){
            System.out.println("book dao update ...");
        }
    }
    

    目前打印save方法的时候,因为方法中有打印系统时间,所以运行的时候是可以看到系统时间

    对于update方法来说,就没有该功能,我们要使用SpringAOP的方式在不改变update方法的前提下让其具有打印系统时间的功能。

  • 创建Spring的配置类

    @Configuration
    @ComponentScan("com.baidu")
    public class SpringConfig {
    }
    
  • 编写App运行类

    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
            BookDao bookDao = ctx.getBean(BookDao.class);
            bookDao.save();
        }
    }
    
5.2.2 实现步骤

步骤1:添加依赖

pom.xml

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>

1630146885493

  • 因为spring-context中已经导入了spring-aop,所以不需要再单独导入spring-aop
  • 导入AspectJ的jar包;AspectJ是AOP思想的一个具体实现,Spring有自己的AOP实现,但是相比于AspectJ来说比较麻烦,所以我们直接采用Spring整合ApsectJ的方式进行AOP开发。

步骤2:定义接口与实现类

public interface BookDao {
    public void save();
    public void update();
}

@Repository
public class BookDaoImpl implements BookDao {

    public void save() {
        System.out.println(System.currentTimeMillis());
        System.out.println("book dao save ...");
    }

    public void update(){
        System.out.println("book dao update ...");
    }
}

步骤3:定义通知类和通知

通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。

public class MyAdvice {
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

类名和方法名没有要求,可以任意。

步骤4:定义切入点

BookDaoImpl中有两个方法,分别是save和update,我们要增强的是update方法,该如何定义呢?

public class MyAdvice {
    @Pointcut("execution(void com.baidu.dao.BookDao.update())")
    private void pt(){}
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

说明:

  • 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑
  • execution及后面编写的内容,指定了一个方法的签名,规定了哪些方法会被选定作为切入点。(后面会详细讲解)

步骤5:制作切面

切面是用来描述通知和切入点之间的关系,如何进行关系的绑定?

public class MyAdvice {
    @Pointcut("execution(void com.baidu.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置

1630148447689

说明:@Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行,除此之前还有其他四种类型

切入点的定义切面的制作,都是在通知类中进行的。(因为不能改动原有设计代码

步骤6:将通知类配给容器并标识其为切面类

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.baidu.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

步骤7:开启注解格式AOP功能

@Configuration
@ComponentScan("com.baidu")
@EnableAspectJAutoProxy
public class SpringConfig {
}

步骤8:运行程序

public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        bookDao.update();
    }
}

执行结果:

image-20231222153732825

知识点1:@EnableAspectJAutoProxy

名称@EnableAspectJAutoProxy
类型配置类注解
位置配置类定义上方
作用开启注解格式AOP功能

知识点2:@Aspect

名称@Aspect
类型类注解
位置切面类定义上方
作用设置当前类为AOP切面类

知识点3:@Pointcut

名称@Pointcut
类型方法注解
位置切入点方法定义上方
作用设置切入点方法
属性value(默认):切入点表达式
定义切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑

知识点4:@Before

名称@Before
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行

5.3 AOP工作流程

5.3.1 AOP工作流程

核心:代理模式

这得从Spring加载bean说起…

流程1: Spring容器启动

  • 容器启动就需要去加载bean,哪些类会被加载呢?
  • 需要被增强的类,如:BookServiceImpl
  • 通知类,如:MyAdvice
  • 注意此时bean对象还没有创建成功

流程2: 读取所有切面配置中的切入点

image-20231222163925610

  • 上面这个例子中有两个切入点的配置,但是第一个切入点定义时依托的方法ptx()并没有被使用,所以不会被读取。
  • 如何看有没有被使用,就看切面注解上有没有这个方法即可

流程3:初始化bean

判定bean对应的类中的方法是否匹配到任意切入点(流程2中已经读取了切面中所有的切入点)

  • 注意第1步在容器启动的时候,bean对象还没有被创建成功

  • 要对实例化bean对象的类中的方法和切入点进行匹配

    1630152538083

    • 匹配失败,创建原始对象, 如UserDao
      • 匹配失败说明不需要增强,直接调用原始对象的方法即可。
    • 匹配成功,创建原始对象(目标对象)的代理对象,如:BookDao
      • 匹配成功说明需要对其进行增强
      • 对哪个类做增强,这个类对应的对象就叫做目标对象
      • 因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
      • 最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强

流程4:获取bean执行方法

  • 获取的bean是原始对象时,调用方法并执行,完成操作
  • 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
5.3.2 验证容器中是否为代理对象
  • 如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身
  • 如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
5.3.2.1 不被增强
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.baidu.dao.BookDao.update1())")
    private void pt(){}
    
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

切入点中update1这个方法是不存在的

运行代码:

public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        System.out.println(bookDao);
        System.out.println(bookDao.getClass());
    }
}

运行结果:

image-20231222170324805

5.3.2.2 被增强
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.baidu.dao.BookDao.update1())")
    private void pt(){}
    
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

同样的运行代码运行出的结果:

image-20231222170438494

类型是代理类。

为什么打印出来的对象仍是BookDaoImpl?

因为Spring的AOP对其toString方法进行了重写,所以打印出来的对象会感觉是BookDaoImpl类型

5.3.3 AOP核心概念

在上面介绍AOP的工作流程中,我们提到了两个核心概念,分别是:

  • 目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
  • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现

简单来说,

目标对象就是要增强的类[如:BookServiceImpl类]对应的对象,也叫原始对象,不能说它不能运行,只能说它在运行的过程中对于要增强的内容是缺失的。

SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知[如:MyAdvice中的method方法]内容加进去,就实现了增强,这就是我们所说的代理(Proxy)

5.4 AOP配置管理

5.4.1 AOP切入点表达式

image-20231222193450619

形如execution(void com.baidu.dao.BookDao.update())即为切入点表达式

对于切入点表达式,重点关注其语法格式通配符书写技巧

5.4.1.1 语法格式
  • 切入点: 要进行增强的方法

    因为调用接口方法的时候最终运行的还是其实现类的方法,所以有两种描述方式

    image-20231222194937691

    描述方式一:执行com.baidu.dao包下的BookDao接口中的无参数update方法

    execution(void com.baidu.dao.BookDao.update())
    

    描述方式二:执行com.baidu.dao.impl包下的BookDaoImpl类中的无参数update方法

    execution(void com.baidu.dao.impl.BookDaoImpl.update())
    
  • 切入点表达式:要进行增强的方法的描述方式

    对于**切入点表达式的语法**为:

    • 切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)

    对于这个格式,我们不需要硬记,通过一个例子,理解它:

    execution(public User com.baidu.service.UserService.findById(int))
    
    • execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
    • public: 访问修饰符,还可以是public,private等,可以省略
    • User:返回值,写返回值类型
    • com.baidu.service:包名,多级包使用点连接
    • UserService: 类/接口名称
    • findById:方法名
    • int: 参数,直接写参数的类型,多个类型用逗号隔开
    • 异常名:方法定义中抛出指定异常,可以省略

    但如果每一个方法对应一个切入点表达式,编写起来会比较麻烦,有没有更简单的方式呢?

    就需要用到下面的通配符

5.4.1.2 通配符

我们使用通配符描述切入点,主要的目的就是简化之前的配置

  • *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

    execution(public * com.baidu.*.UserService.find*(*))
    

    匹配com.baidu包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法

  • ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

    execution(public User com..UserService.findById(..))
    

    匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法

  • +:专用于匹配子类类型

    execution(* *..*Service+.*(..))
    

    这个使用率较低,描述子类的,咱们做JavaEE开发,继承机会就一次,使用都很慎重,所以很少用它。*Service+,表示所有以Service结尾的接口的子类。

  • 实际案例分析

    image-20231222195748944

    execution(void com.baidu.dao.BookDao.update())
    匹配接口,能匹配到
    execution(void com.baidu.dao.impl.BookDaoImpl.update())
    匹配实现类,能匹配到
    execution(* com.baidu.dao.impl.BookDaoImpl.update())
    返回值任意,能匹配到
    execution(* com.baidu.dao.impl.BookDaoImpl.update(*))
    返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加参数
    execution(void com.*.*.*.*.update())
    返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配
    execution(void com.*.*.*.update())
    返回值为void,com包下的任意两层包下的任意类的update方法,匹配到的是接口,能匹配
    execution(void *..update())
    返回值为void,方法名是update的任意包下的任意类,能匹配
    execution(* *..*(..))
    匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
    execution(* *..u*(..))
    匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配
    execution(* *..*e(..))
    匹配项目中任意包任意类下只要以e结尾的方法,update和save方法能满足,能匹配
    execution(void com..*())
    返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法
    execution(* com.baidu.*.*Service.find*(..))
    将项目中所有业务层方法的以find开头的方法匹配
    execution(* com.baidu.*.*Service.save*(..))
    将项目中所有业务层方法的以save开头的方法匹配
    

    后面两种更符合我们平常切入点表达式的编写规则

5.4.1.3 书写技巧

切入点表达式的编写其实是很灵活的,常用的书写技巧:(所有代码按照标准规范开发,否则以下技巧全部失效)

  • 描述切入点通常描述接口,而不描述实现类, 如果描述到实现类,就出现紧耦合了

  • 访问控制修饰符针对接口开发均采用public描述可省略访问控制修饰符描述

  • 返回值类型对于**增删改类使用精准类型加速匹配**,对于**查询类使用*通配快速描述**

  • 包名书写尽量不使用…匹配,效率过低,常用*做单个包描述匹配,或精准匹配

  • 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名

  • 方法名书写以动词进行精准匹配,名词采用匹配,例如getById书写成getBy,selectAll书写成selectAll

  • 参数规则较为复杂,根据业务方法灵活调整

  • 通常不使用异常作为匹配规则

    指定在目标方法抛出异常时执行的通知(advice)

    @Aspect
    @Component
    public class ExceptionAspect {
    
     @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "exception")
     public void handleException(Exception exception) {
         // 在方法抛出异常时执行此通知
         System.out.println("Exception caught: " + exception.getMessage());
         // 可以在这里执行异常处理逻辑,例如记录日志、发送通知等
     }
    }
    
5.4.2 AOP通知(增强)类型

回顾:

image-20231222201032116

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

除了加在前面的类型,还有没有加在其他地方的类型?

5.4.2.1 类型介绍
  • 前置通知
  • 后置通知
  • 环绕通知(重点)
  • 返回后通知 (了解)
  • 抛出异常后通知 (了解)
image-20231222201320598

(1) 前置通知,追加功能到方法执行前,类似于在代码1或者代码2添加内容

(2) 后置通知, 追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代码5添加内容

(3) 返回后通知, 追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,如果方法执行抛出异常,返回后通知将不会被添加

(4) 抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内容,只有方法抛出异常后才会被添加

(5) 环绕通知, 环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式它可以实现其他四种通知类型的功能

环境准备

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.9.4</version>
    </dependency>
</dependencies>
public interface BookDao {
    public void update();
    public int select();
}

@Repository
public class BookDaoImpl implements BookDao {
    public void update(){
        System.out.println("book dao update ...");
    }
    public int select() {
        System.out.println("book dao select is running ...");
        return 100;
    }
}
@Configuration
@ComponentScan("com.baidu")
@EnableAspectJAutoProxy
public class SpringConfig {
}
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.baidu.dao.BookDao.update())")
    private void pt(){}

    public void before() {
        System.out.println("before advice ...");
    }

    public void after() {
        System.out.println("after advice ...");
    }

    public void around(){
        System.out.println("around before advice ...");
        System.out.println("around after advice ...");
    }

    public void afterReturning() {
        System.out.println("afterReturning advice ...");
    }
    
    public void afterThrowing() {
        System.out.println("afterThrowing advice ...");
    }
}
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        bookDao.update();
    }
}

通知类型的使用

前置通知

修改MyAdvice,在before方法上添加@Before注解

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.baidu.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    //此处也可以写成 @Before("MyAdvice.pt()"),不建议
    public void before() {
        System.out.println("before advice ...");
    }
}
后置通知
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.baidu.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    public void before() {
        System.out.println("before advice ...");
    }
    @After("pt()")
    public void after() {
        System.out.println("after advice ...");
    }
}

运行结果:

image-20231222203222198

环绕通知

试着编写这样的通知并调用:

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.baidu.dao.BookDao.update())")
    private void pt(){}
    
    @Around("pt()")
    public void around(){
        System.out.println("around before advice ...");
        System.out.println("around after advice ...");
    }
}

运行结果:

image-20231222203451723

结果:通知的内容打印出来,但是原始方法的内容却没有被执行,并且之前的前置,后置通知也没显示结果。

因为环绕通知需要在原始方法的前后进行增强,所以环绕通知就必须要能对原始操作进行调用

环绕通知示例如下:

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.baidu.dao.BookDao.update())")
    private void pt(){}
    
    @Around("pt()")
    public void around(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println("around before advice ...");
        //表示对原始操作的调用
        pjp.proceed();
        System.out.println("around after advice ...");
    }
}

提示:proceed()为什么要抛出异常?

原因:查看源码

image-20231222203803058

运行结果:

image-20231222203829712

注意事项:

原始方法有返回值的处理

select方法带有一个int的返回值

public interface BookDao {
    public void update();
    public int select();
}

对select方法添加环绕通知

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.baidu.dao.BookDao.update())")
    private void pt(){}
    
    @Pointcut("execution(int com.baidu.dao.BookDao.select())")
    private void pt2(){}
    
    @Around("pt2()")
    public void aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        //表示对原始操作的调用
        pjp.proceed();
        System.out.println("around after advice ...");
    }
}

修改App类,调用select方法

public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        int num = bookDao.select();
        System.out.println(num);
    }
}

会报错:

image-20231222204501715

错误大概的意思是:空的返回不匹配原始方法的int返回

  • void就是返回Null
  • 原始方法就是BookDao下的select方法

所以如果我们使用环绕通知的话,要根据原始方法的返回值来设置环绕通知的返回值,具体解决方案为:

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.baidu.dao.BookDao.update())")
    private void pt(){}
    
    @Pointcut("execution(int com.baidu.dao.BookDao.select())")
    private void pt2(){}
    
    @Around("pt2()")
    public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        //表示对原始操作的调用
        Object ret = pjp.proceed();
        System.out.println("around after advice ...");
        return ret;
    }
}

说明:

  • 为什么返回的是Object而不是int的主要原因是Object类型更通用。

  • 在环绕通知中是可以对原始方法返回值就行修改的。

    如何修改?

    @Aspect
    @Component
    public class ModifyIntReturnValueAspect {
    
     @Around("execution(* com.example.service.*.*(..))")
     public Object modifyIntReturnValue(ProceedingJoinPoint joinPoint) throws Throwable {
         // 调用目标方法,并获取原始的返回值
         int originalReturnValue = (int) joinPoint.proceed();
    
         // 修改返回值,这里演示简单地加上一个固定的值
         int modifiedReturnValue = originalReturnValue + 10;
    
         // 返回修改后的返回值
         return modifiedReturnValue;
     }
    }
    

    Object是所有类的根类,因此它可以接受任何非基本数据类型(比如int)的对象。这是由于自动装箱(Autoboxing)的特性

返回后通知
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.baidu.dao.BookDao.update())")
    private void pt(){}
    
    @Pointcut("execution(int com.baidu.dao.BookDao.select())")
    private void pt2(){}
    
    @AfterReturning("pt2()")
    public void afterReturning() {
        System.out.println("afterReturning advice ...");
    }
}

运行结果:(这里注释掉了around,但保留了Before和After)

image-20231222205343169

注意:返回后通知是需要在原始方法select正常执行后才会被执行,如果select()方法执行的过程中出现了异常,那么返回后通知是不会被执行。后置通知是不管原始方法有没有抛出异常都会被执行。

异常后通知
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.baidu.dao.BookDao.update())")
    private void pt(){}
    
    @Pointcut("execution(int com.baidu.dao.BookDao.select())")
    private void pt2(){}
    
    @AfterThrowing("pt2()")
    public void afterThrowing() {
        System.out.println("afterThrowing advice ...");
    }
}
//可以在`select()`方法中添加一行代码`int i = 1/0`来产生异常

运行结果:

image-20231222205630860

环绕通知扩展

思考下环绕通知是如何实现其他通知类型的功能的:

因为环绕通知是**可以控制原始方法执行( pjp.proceed();)**的,所以我们把增强的代码写在调用原始方法的不同位置就可以实现不同的通知类型的功能

image-20231222210336665

通知类型总结

知识点1:@After

名称@After
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行

知识点2:@AfterReturning

名称@AfterReturning
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法正常执行完毕后执行

知识点3:@AfterThrowing

名称@AfterThrowing
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行

知识点4:@Around

名称@Around
类型方法注解
位置通知方法定义上方
作用设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行

环绕通知注意事项

  1. 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
  2. 通知中如果未使用ProceedingJoinPoint对原始方法进行调用的话,将跳过原始方法的执行
  3. 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
  4. 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
  5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常

介绍完这么多种通知类型,具体该选哪一种呢?

我们可以通过一些案例加深下对通知类型的学习。

5.4.3 AOP获取数据

通知(增强)中获取切入点的数据,如获取参数,获取返回值,获取异常

  • 获取切入点方法的参数,所有的通知类型都可以获取参数

    • JoinPoint类ProceedingJoinPoint类都是通知中函数参数的数据类型

      如:

      image-20231223133419121

    • JoinPoint:适用于前置、后置、返回后、抛出异常后通知

    • ProceedingJoinPoint:适用于环绕通知

  • 获取切入点方法返回值,前置和抛出异常后通知是没有返回值后置通知可有可无,所以不做研究

    • 回顾:为什么要获取返回值

      如果使用**返回后通知或者环绕通知,要根据原始方法的返回值来设置通知的返回值**,否则会报错

    • 返回后通知

    • 环绕通知

  • 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究

    • 抛出异常后通知
    • 环绕通知
5.4.3.1 获取参数

非环绕通知获取方式

在方法上添加JoinPoint,通过JoinPoint来获取参数

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.baidu.dao.BookDao.findName(..))")
    private void pt(){}

    @Before("pt()")
    public void before(JoinPoint jp) 
        Object[] args = jp.getArgs();
        System.out.println(Arrays.toString(args));
        System.out.println("before advice ..." );
    }
	//...其他的略
}

运行方法:

public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        String name = bookDao.findName(100,"baidu");
        System.out.println(name);
    }
}

运行结果:

image-20231223135619147

说明:

使用JoinPoint的方式获取参数适用于前置后置返回后抛出异常后通知

环绕通知获取方式

环绕通知使用的是ProceedingJoinPoint,因为ProceedingJoinPoint是JoinPoint类的子类,所以对于ProceedingJoinPoint类中应该也会有对应的getArgs()方法

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.baidu.dao.BookDao.findName(..))")
    private void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp)throws Throwable {
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        Object ret = pjp.proceed();
        return ret;
    }
	//其他的略
}

运行结果:

image-20231223135811314

注意:

  • pjp.proceed()方法是有两个构造方法,分别是:

    image-20231223140047684
    • 调用无参数的proceed,当原始方法有参数,会在调用的过程中自动传入参数

    • 所以调用这两个方法的任意一个都可以完成功能

    • 但是当需要修改原始方法的参数时,就只能采用带有参数的方法, 如下:

      @Component
      @Aspect
      public class MyAdvice {
          @Pointcut("execution(* com.baidu.dao.BookDao.findName(..))")
          private void pt(){}
      
          @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;
          }
      	//其他的略
      }
      

      有了这个特性后,我们就可以在环绕通知中对原始方法的参数进行拦截过滤,避免由于参数的问题导致程序无法正确运行,保证代码的健壮性。

      例如,百度网盘在获取提取码时,会将传入的参数去掉一些空格之后再将其传入

5.4.3.2 获取返回值

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

返回后通知获取返回值

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.baidu.dao.BookDao.findName(..))")
    private void pt(){}

    @AfterReturning(value = "pt()",returning = "ret")
    public void afterReturning(Object ret) {
        System.out.println("afterReturning advice ..."+ret);
    }
	//其他的略
}

注意 :

(1) 参数名的问题

image-20231223140953860

(2) afterReturning方法参数类型的问题

参数类型可以写成String,但是为了能匹配更多的参数类型,建议写成Object类型

(3) afterReturning方法参数的顺序问题

image-20231223141114180

运行App后查看运行结果,说明返回值已经被获取到

image-20231223141343313

环绕通知获取返回值

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.baidu.dao.BookDao.findName(..))")
    private void pt(){}

    @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;
    }
	//其他的略
}

上述代码中,ret就是方法的返回值,我们是可以直接获取不但可以获取,如果需要还可以进行修改

5.4.3.3 获取异常

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

环绕通知获取异常

catch方法中就可以获取到异常,至于获取到异常以后该如何处理,和业务需求有关

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.baidu.dao.BookDao.findName(..))")
    private void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp){
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = null;
        try{
            ret = pjp.proceed(args);
        }catch(Throwable throwable){
            t.printStackTrace();
        }
        return ret;
    }
	//其他的略
}

抛出异常后通知获取异常

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.baidu.dao.BookDao.findName(..))")
    private void pt(){}

    @AfterThrowing(value = "pt()",throwing = "t")
    public void afterThrowing(Throwable t) {
        System.out.println("afterThrowing advice ..."+t);
    }
	//其他的略
}

注意:

image-20231223141834429

运行结果:

image-20231223141936247

5.5 AOP总结

AOP的知识就已经讲解完了,接下来对于AOP的知识进行一个总结:

5.5.1 AOP的核心概念
  • 概念:AOP(Aspect Oriented Programming)面向切面编程,一种编程范式
  • 作用:在不惊动原始设计的基础上为方法进行功能增强
  • 核心概念
    • 代理(Proxy):SpringAOP的核心本质是采用代理模式实现的
    • 连接点(JoinPoint):在SpringAOP中,理解为任意方法的执行
    • 切入点(Pointcut):匹配连接点的式子,切入点一定是连接点
    • 通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
    • 切面(Aspect):描述通知与切入点的对应关系
    • 目标对象(Target):被代理的原始对象成为目标对象
5.5.2 切入点表达式
  • 切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名)

    execution(* com.baidu.service.*Service.*(..))
    
  • 切入点表达式描述通配符:

    • 作用:用于快速描述,范围描述
    • *:匹配任意符号(常用)
    • .. :匹配多个连续的任意符号(常用)
    • +:匹配子类类型
  • 切入点表达式书写技巧

    1. 标准规范开发

    2. 查询操作的返回值建议使用*匹配

    3. 减少使用…的形式描述包

    4. 对接口进行描述,使用*表示模块名,例如UserService的匹配描述为*Service

    5. 方法名书写保留动词,例如get,使用*表示名词,例如getById匹配描述为getBy*

    6. 参数根据实际情况灵活调整

5.5.3 五种通知类型
  • 前置通知
  • 后置通知
  • 环绕通知(重点)
    • 环绕通知依赖形参ProceedingJoinPoint才能实现对原始方法的调用
    • 环绕通知可以隔离原始方法的调用执行(即不使用ProceedingJoinPoint进行对原始方法的调用)
    • 环绕通知返回值设置为Object类型
    • 环绕通知中可以对原始方法调用过程中出现的异常进行处理
  • 返回后通知
  • 抛出异常后通知
5.5.4 通知中获取参数
  • 获取切入点方法的参数,所有的通知类型都可以获取参数
    • JoinPoint:适用于前置、后置、返回后、抛出异常后通知
    • ProceedingJoinPoint:适用于环绕通知
  • 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
    • 返回后通知
    • 环绕通知
  • 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
    • 抛出异常后通知
    • 环绕通知

5.6 AOP事务管理

5.6.1 Spring事务简介
  • 事务作用:在数据层保障一系列的数据库操作同成功同失败

  • Spring事务作用:在**数据层业务层保障一系列的数据库操作同成功同失败**

    数据层有事务我们可以理解,为什么业务层也需要处理事务呢?

    因为会出现这种情况**:业务层中有事务,事务是在数据层中,不同事务对应着各自的数据操作,要保障一系列的数据库操作同成功同失败,最终也就是要保障业务层一系列的数据库操作同成功同失败**

  • Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager

    image-20231223204510766

    commit是用来提交事务,rollback是用来回滚事务。

    PlatformTransactionManager只是一个接口,Spring还为其提供了一个具体的实现:

    image-20231223204544532

    从名称上可以看出,我们只需要给它一个DataSource对象,它就可以帮你去在业务层管理事务。

    其内部采用的是JDBC的事务

    所以说如果你持久层采用的是JDBC相关的技术就可以采用这个事务管理器来管理你的事务

    Mybatis内部采用的就是JDBC的事务,所以后期我们Spring整合Mybatis就采用的这个DataSourceTransactionManager事务管理器。

    持久层(Persistence Layer)是指应用程序中负责处理数据持久化(即数据在应用程序数据库之间的存储和检索)的部分

    即常见的Dao层

5.6.1.1 转账案例

需求: 实现任意两个账户间转账操作

需求微缩: A账户减钱,B账户加钱

实现步骤:

①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)

②:业务层提供转账操作(transfer),调用减钱与加钱的操作

③:提供2个账号和操作金额执行转账操作

④:基于Spring整合MyBatis环境搭建上述操作

image-20231223205707827

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);
}

正常情况:程序正常执行时,账户金额A减B加。

引入:如果在转账的过程中出现了异常,如:

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(out,money);
        int i = 1/0;
        accountDao.inMoney(in,money);
    }

}

出现的问题金额减少之后并没有相关账户金额的增加,(如果交换执行顺序,相关账户金额的增加但没有账户金额减少)

程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败

当程序出问题后,我们需要让事务进行回滚,而且这个事务应该是加在业务层Spring的事务管理就是用来解决这类问题的

Spring事务管理具体的实现步骤为:

步骤1:在需要被事务管理的方法上添加注解 @Transactional

public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    //配置当前接口方法具有事务
    public void transfer(String out,String in ,Double money) ;
}

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;
	@Transactional
    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(out,money);
        int i = 1/0;
        accountDao.inMoney(in,money);
    }

}

注意:

@Transactional可以写在接口类上、接口方法上、实现类上和实现类方法

  • 写在接口类上,该接口的**所有实现类的所有方法**都会有事务
  • 写在接口方法上,该接口的**所有实现类的该方法**都会有事务
  • 写在实现类上,该**类中的所有方法**都会有事务
  • 写在实现类方法上,该方法上有事务
  • 建议写在实现类或实现类的方法上

步骤2:在JdbcConfig类中配置事务管理器 (并注入Spring容器中@Bean)

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;
    }

    //配置事务管理器,mybatis使用的是jdbc事务
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
}

注意:事务管理器要根据使用技术进行选择,Mybatis框架使用的是JDBC事务,可以直接使用DataSourceTransactionManager

步骤3:开启事务注解 @EnableTransactionManagement

在SpringConfig的配置类中开启

@Configuration
@ComponentScan("com.baidu")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}

步骤4:运行测试类

在测试类中测试:

@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);
    }

}

会发现在转换的业务出现错误后,事务就可以控制回滚,保证数据的正确性

知识点1:@EnableTransactionManagement

名称@EnableTransactionManagement
类型配置类注解
位置配置类定义上方
作用设置当前Spring环境中开启注解式事务支持

知识点2:@Transactional

名称@Transactional
类型接口注解 类注解 方法注解
位置业务层接口上方 业务层实现类上方 业务方法上方
作用为当前业务层方法添加事务(如果设置在类或接口上方则类或接口中所有方法均添加事务)
5.6.2 Spring事务角色

两个角色:分别是事务管理员事务协调员

  • 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
  • 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法
5.6.2.1 未开启Spring事务之前

未开启Spring事务之前(也就是如果在转账的过程中出现了异常,会导致数据不一致时):

image-20231223214718930

  • AccountDao的outMoney因为是修改操作,会开启一个事务T1
  • AccountDao的inMoney因为是修改操作,会开启一个事务T2
  • AccountService的transfer没有事务
    • 运行过程中如果没有抛出异常,则T1和T2都正常提交,数据正确
    • 如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行
    • 就会导致数据出现错误
5.6.2.2 开启了Spring事务之后

image-20231223214921431

  • transfer上添加了@Transactional注解,在该方法上就会有一个事务T
  • AccountDao的outMoney方法的**事务T1加入到transfer的事务T中**
  • AccountDao的inMoney方法的**事务T2加入到transfer的事务T中**
  • 这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性。

注意:

目前的事务管理是基于DataSourceTransactionManager(用于管理数据库事务)SqlSessionFactoryBean(配置和创建 MyBatis 的 SqlSessionFactory 的工厂Bean)使用的是同一个数据源

回顾,数据源在Spring中的创建过程

@Bean
public DataSource dataSource(){
 DruidDataSource ds = new DruidDataSource();
 ds.setDriverClassName(driver);
 ds.setUrl(url);
 ds.setUsername(userName);
 ds.setPassword(password);
 return ds;
}
5.6.3 Spring事务属性
  • 事务的管理员和事务的协同员,这两个概念具体做什么?
  • 除了这两个概念,事务的其他相关配置都有哪些?
5.6.3.1 事务配置

image-20231224140916112

上面这些属性都可以在@Transactional注解的参数上进行设置。

  • readOnly:true只读事务,false读写事务增删改要设为false, 查询设为true。

  • timeout: 设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间。

  • rollbackFor: 当出现指定异常进行事务回滚(即也侧面说明了并不是所有的异常都会回滚事务)

    Spring的事务只会对Error异常和RuntimeException异常及其子类进行事务回顾,其他的异常类型是不会回滚的;如IOException不符合条件,不会回滚

    @Service
    public class AccountServiceImpl implements AccountService {
    
     @Autowired
     private AccountDao accountDao;
    	@Transactional
     public void transfer(String out,String in ,Double money) throws IOException{
         accountDao.outMoney(out,money);
         //int i = 1/0; //这个异常事务会回滚
         if(true){
             throw new IOException(); //这个异常事务就不会回滚
         }
         accountDao.inMoney(in,money);
     }
    }
    

    运行上面的事务,会发现,虽然抛出了错误,但是数据库的数据仍然发生了变化

    此时就可以使用rollbackFor属性来设置出现IOException异常产生回滚操作

    @Service
    public class AccountServiceImpl implements AccountService {
    
     @Autowired
     private AccountDao accountDao;
    	 @Transactional(rollbackFor = {IOException.class})
     public void transfer(String out,String in ,Double money) throws IOException{
         accountDao.outMoney(out,money);
         //int i = 1/0; //这个异常事务会回滚
         if(true){
             throw new IOException(); //这个异常事务就不会回滚
         }
         accountDao.inMoney(in,money);
     }
    }
    

    结果为:抛出了IOException的异常,并且数据库的数据没有发生变化

  • noRollbackFor:当出现指定异常进行事务回滚

  • rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串

  • noRollbackForClassName等同于noRollbackFor,只不过属性为异常的类全名字符串

  • isolation设置事务的隔离级别(一个事务的执行不受其他事务的影响程度)

    • DEFAULT: 默认隔离级别, 会采用数据库的隔离级别

    • READ_UNCOMMITTED : 读未提交(允许一个事务读取另一个事务未提交的数据)

    • READ_COMMITTED : 读已提交(保证一个事务不会读取到另一个事务未提交的数据)

    • REPEATABLE_READ : 重复读取(保证一个事务在执行期间多次读取相同的数据时,会得到相同的结果)

    • SERIALIZABLE: 串行化

      串行化:

      • 最高的隔离级别,确保事务之间的完全隔离
      • 避免了脏读、不可重复读和幻读,但可能导致性能下降,因为事务需要等待锁的释放。
5.6.3.2 事务传播行为

需求引入:

在前面的转案例的基础上添加新的需求,完成转账后记录日志。

  • 需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
  • 需求微缩:A账户减钱,B账户加钱,数据库记录日志,无论转账操作是否成功,均进行转账操作的日志留痕

分析:

在业务层转账操作(transfer),调用减钱、加钱与记录日志功能

环境准备:

步骤1: 创建日志表

create table tbl_log(
   id int primary key auto_increment,
   info varchar(255),
   createDate datetime
)

步骤2: 添加LogDao接口

now() 是一个数据库函数,用于获取当前的日期和时间

public interface LogDao {
    @Insert("insert into tbl_log (info,createDate) values(#{info},now())")
    void log(String info);
}

步骤3: 添加LogService接口与实现类

public interface LogService {
    void log(String out, String in, Double money);
}
@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: 在转账的业务中添加记录日志

public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    //配置当前接口方法具有事务
    public void transfer(String out,String in ,Double money)throws IOException ;
}
@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);
        }
    }
}

步骤5: 运行程序,发现问题

  • 当转账业务之间出现异常(int i =1/0),转账失败,tbl_account成功回滚,但是tbl_log表未添加数据
  • 失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败

  • log方法inMoney方法和outMoney方法都属于增删改,分别有事务T1,T2,T3
  • transfer因为加了@Transactional注解,也开启了事务T
  • 前面我们讲过Spring事务会把T1,T2,T3都加入到事务T中
  • 所以当转账失败后,所有的事务都回滚,导致日志没有记录下来
  • 这和我们的需求不符,我们希望log方法单独是一个事务
5.6.3.2.1 事务传播行为

事务传播行为:事务协调员事务管理员所携带事务的处理态度。需要用到之前我们没有说的propagation属性

image-20231224162126444

代码修改:

@Service
public class LogServiceImpl implements LogService {

    @Autowired
    private LogDao logDao;
	//propagation设置事务属性:传播行为设置为当前操作需要新事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(String out,String in,Double money ) {
        logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
    }
}

运行后,就能实现我们想要的结果,不管转账是否成功,都会记录日志。

image-20231224163428804

5.6.3.2.2 事务传播行为属性说明

image-20231224163445617

对于我们开发实际中使用的话,因为默认值需要事务是常态的。

根据开发过程选择其他的就可以了,例如案例中需要新事务就需要手工配置。

其实入账和出账操作上也有事务,采用的就是默认值。

image-20231224163538834

获取当前的日期和时间**

public interface LogDao {
    @Insert("insert into tbl_log (info,createDate) values(#{info},now())")
    void log(String info);
}

步骤3: 添加LogService接口与实现类

public interface LogService {
    void log(String out, String in, Double money);
}
@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: 在转账的业务中添加记录日志

public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    //配置当前接口方法具有事务
    public void transfer(String out,String in ,Double money)throws IOException ;
}
@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);
        }
    }
}

步骤5: 运行程序,发现问题

  • 当转账业务之间出现异常(int i =1/0),转账失败,tbl_account成功回滚,但是tbl_log表未添加数据
  • 失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败

  • log方法inMoney方法和outMoney方法都属于增删改,分别有事务T1,T2,T3
  • transfer因为加了@Transactional注解,也开启了事务T
  • 前面我们讲过Spring事务会把T1,T2,T3都加入到事务T中
  • 所以当转账失败后,所有的事务都回滚,导致日志没有记录下来
  • 这和我们的需求不符,我们希望log方法单独是一个事务
5.6.3.2.1 事务传播行为

事务传播行为:事务协调员事务管理员所携带事务的处理态度。需要用到之前我们没有说的propagation属性

image-20231224162126444

代码修改:

@Service
public class LogServiceImpl implements LogService {

    @Autowired
    private LogDao logDao;
	//propagation设置事务属性:传播行为设置为当前操作需要新事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(String out,String in,Double money ) {
        logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
    }
}

运行后,就能实现我们想要的结果,不管转账是否成功,都会记录日志。

[外链图片转存中…(img-wVRbTG7l-1703410601946)]

5.6.3.2.2 事务传播行为属性说明

image-20231224163445617

对于我们开发实际中使用的话,因为默认值需要事务是常态的。

根据开发过程选择其他的就可以了,例如案例中需要新事务就需要手工配置。

其实入账和出账操作上也有事务,采用的就是默认值。

在这里插入图片描述

image-20231224163658888

文章来源:https://blog.csdn.net/qq_55626883/article/details/135184257
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。