0%

面向切面编程AOP

  • 面向切面编程AOP
    • AOP介绍
    • AOP的七大术语
    • 切点表达式
    • 使用Spring的AOP
    • 通知类型
    • 切面顺序@Order
    • 通用切点表达式@Pointcut
    • 连接点JoinPoint
    • 全注解开发
    • AOP实际案例

十五、★★★面向切面编程AOP

15.1 AOP介绍

  • AOP(Aspect Oriented Programming):面向切面编程,面向方面编程。

    • 切面:业务流程中,与业务逻辑不挂钩的非业务逻辑通用代码,如:事务。
    • AOP 是一种编程技术。
    • AOPOOP 的补充延伸。
  • 面向切面编程:将和业务逻辑不挂钩的通用代码抽取出来,形成一个独立的组件,形成一个横向的切面,在纵向的业务逻辑代码中,如果需要用到切面时,可以将切面以交叉业务的形式切入业务流程中。可以增强代码的复用性、可维护性、使我们更专注于业务逻辑代码。

  • AOP 底层是使用动态代理实现的,动态代理是面向切面编程思想的实现

    • Spring 的 AOP 使用的动态代理是:JDK 动态代理 + CGLIB 动态代理技术。Spring 在这两种动态代理中灵活切换:
      • 如果是代理接口,会默认使用 JDK 动态代理。
      • 如果要代理某个类,这个类没有实现接口,就会切换使用 CGLIB。
      • 当然,可以通过一些配置强制让 Spring 只使用 CGLIB。
  • 一般一个系统当中都会有一些系统服务,例如:日志、事务管理、安全等。这些系统服务被称为:交叉业务

    • 纵向的为业务逻辑代码,横向的为交叉业务

      ![](../../../../../Running Noob/计算机/Typora笔记/笔记-git仓库/Java-SSM-notebook/img/Spring/aop纵向和横向业务.png)

    • 这些交叉业务几乎是通用的,不管你是做银行账户转账,还是删除用户数据。日志、事务管理、安全,这些都是需要做的。

    • 如果在每一个业务处理过程当中,都掺杂这些交叉业务代码进去的话,存在两方面问题:

      • 第一:交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复用。并且修改这些交叉业务代码的话,需要修改多处,维护成本高
      • 第二:程序员无法专注核心业务代码的编写,在编写核心业务代码的同时还需要处理这些交叉业务。

      使用 AOP(动态代理)可以很轻松的解决以上问题。

      • AOP:将与核心业务无关的代码独立的抽取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中的过程被称为 AOP
  • AOP的优点:

    • 第一:代码复用性增强。
    • 第二:代码易维护。
    • 第三:使开发者更关注业务逻辑。

15.2 AOP的七大术语

  1. 通知(Advice:又叫增强,具体增强的代码,如具体的事务代码、日志代码、安全代码等。
    • 通知描述的是代码(增强代码)
    • 通知包括:
      • 前置通知:通知的代码只放在业务方法之前。
      • 后置通知:通知的代码只放在业务方法之后。
      • 环绕通知:通知的代码在业务方法之前和之后都有。
      • 异常通知:通知代码在捕获异常 catch(){} 的代码块之中。
      • 最终终通知:通知代码在 finally{} 的代码块之中。
  2. 织入(Weaving):把通知应用到目标对象上的过程。
  3. 连接点(Joinpoint:在程序的整个执行流程中,可以织入切面的位置。
    • 方法的执行前后、异常抛出之后、finally{} 的代码块等。
    • 连接点描述的是位置,可以织入切面的位置。
  4. 切点(Pointcut:在程序执行流程中,真正织入切面的方法。
    • 切点本质上就是方法,织入切面的方法。
    • 切点:要被 “通知” 增强的方法。
    • 一个切点对应多个连接点。
  5. 切面(Aspect:切点 + 通知。
  6. 代理对象(Proxy):目标对象被织入通知后产生的新对象。
  7. 目标对象(Target):被织入通知的对象。

![](../../../../../Running Noob/计算机/Typora笔记/笔记-git仓库/Java-SSM-notebook/img/Spring/aop术语1.png)

![](../../../../../Running Noob/计算机/Typora笔记/笔记-git仓库/Java-SSM-notebook/img/Spring/aop术语2.png)

15.3 切点表达式

  • 切点是要在其前后织入通知的方法。

  • 切点表达式,可以匹配切点方法的表达式,用于定义通知往哪些方法上切入。

  • 切点表达式语法:execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])

    • 切点表达式以 execution( 开始,以 ) 结束。
    • 访问控制权限修饰符:
      • public | protected | private | default
      • 可选项。没写就是 4 个访问控制权限都包括。
      • public 就表示只包括公开的方法。
    • 返回值类型:
      • 必填项。
      • * 表示返回值类型任意。
      • 如果 String,返回值类型就是 String
    • 全限定类名:
      • 带包名的类名。
      • 可选项。
      • 两个点 .. 代表当前包以及子包下的所有类。com.. 表示 com 包以及子包下的所有类。
      • 全限定类名省略时表示所有的类。
    • 方法名:
      • 必填项。
      • * 表示所有方法。
      • set* 表示所有以 set 开头的方法。
    • 形式参数列表:
      • 必填项。
      • () 表示没有参数的方法。
      • (..) 表示参数类型和个数随意的方法。
      • (*) 表示只有一个参数的方法。
      • (*, String) 表示第一个参数类型随意,第二个参数是 String 的方法。
    • 异常:
      • 可选项。
      • 省略时表示任意异常类型。
  • 切点表达式实例:

    • execution(public * com.powernode.mall.service.*.delete*(..))service 包下所有的类中以 delete 开始的所有公共的任意返回值的方法。
    • execution(* com.powernode.mall..*(..))mall 包下所有的类的所有的方法。
    • execution(* *(..)):所有类的所有方法。

15.4 使用Spring的AOP

  • Spring 对 AOP 的实现包括以下 3 种方式:

    • 第一种方式:Spring 框架结合 AspectJ 框架实现的 AOP,基于注解方式
    • 第二种方式:Spring 框架结合 AspectJ 框架实现的 AOP,基于 XML 方式。
      • AspectJ 框架是专门做 AOP 的框架。
    • 第三种方式:Spring 框架自己实现的 AOP,基于 XML 配置方式。

    实际开发中,都是使用 Spring + AspectJ 来实现 AOP,所以重点学习第一种和第二种方式。

    • 其中最重点的就是第一种方式。我们先学习这种方式。

15.4.1 准备工作

  • 使用 Spring+ AspectJAOP 需要引入的依赖如下:

    • spring context 依赖、spring aop 依赖、spring aspects 依赖。
      • 不需要自己手动引入 spring aop 依赖,因为在 spring context 依赖中已经依赖了 spring aop 依赖。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.f</groupId>
    <artifactId>spring6-008-spring-aspectj-aop-anno</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!--仓库-->
    <repositories>
    <repository>
    <id>repository.spring.milestone</id>
    <name>Spring Milestone Repository</name>
    <url>https://repo.spring.io/milestone</url>
    </repository>
    </repositories>
    <!--依赖-->
    <dependencies>
    <!--spring context依赖-->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.0.8</version>
    </dependency>
    <!--spring aspects依赖-->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.0.8</version>
    </dependency>
    <!--junit依赖-->
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
    </dependency>
    </dependencies>

    <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    </project>
  • 基于注解的方式需要组件扫描,需要 context 命名空间。需要使用 aop,需要 aop 命名空间。

    • 所以在 Spring 配置文件中添加 context 命名空间和 aop 命名空间。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:context="http://www.springframework.org/schema/context"
      xmlns:aop="http://www.springframework.org/schema/aop"
      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
      http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

      </beans>

15.4.2 目标类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.f.spring6.service;

import org.springframework.stereotype.Service;

/**
* @author fzy
* @date 2024/1/25 15:01
*/
@Service
public class UserService { // 目标类
public void login() { // 目标方法
System.out.println("系统正在登录...");
}
}

15.4.3 切面

  • 切面 = 通知 + 切点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.f.spring6.service;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

/**
* @author fzy
* @date 2024/1/25 15:03
*/
@Component
@Aspect // 切面类是需要使用@Aspect注解进行标注的
public class LogAspect { // 切面
// 切面 = 通知 + 切点
// 通知就是具体要编写的增强代码
// @Before注解标注的方法就是一个前置通知
// @Before(切点表达式)
@Before("execution(* com.f.spring6.service..*(..))")
public void beforeAdvice() {
System.out.println("我是一段前置通知...");
}
}

15.4.4 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.f.spring6.service"/>
<!--开启aspectj的自动代理-->
<!--spring容器在扫描类的时候,查看该类上是否有@Aspect注解,如果有,则给这个类生成代理对象-->
<!--
proxy-target-class="true" 表示强制使用CGLIB动态代理
proxy-target-class="false",这是默认值,表示接口使用JDK动态代理,类使用CGLIB动态代理
-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

15.4.5 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.f.spring6.test;

import com.f.spring6.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
* @author fzy
* @date 2024/1/25 15:17
*/
public class SpringAOPTest {
@Test
public void testAdvice() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.login();
}
}
  • 输出结果:

    1
    2
    我是一段前置通知...
    系统正在登录...

15.5 通知类型

  • 通知类型包括:

    • 前置通知:@Before 目标方法执行之前的通知。
    • 后置通知:@AfterReturning 目标方法执行之后的通知。
    • 环绕通知:@Around 目标方法执行之前添加通知,同时目标方法执行之后添加通知。
    • 异常通知:@AfterThrowing 发生异常之后执行的通知。
    • 最终终通知:@After 放在 finally 语句块中的通知。
  • 接下来,编写程序来测试这几个通知的执行顺序:

    • 在前面代码的基础上:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package com.f.spring6.service;

    import org.springframework.stereotype.Service;

    /**
    * @author fzy
    * @date 2024/1/25 15:01
    */
    @Service
    public class UserService { // 目标类
    public void login() { // 目标方法
    System.out.println("系统正在登录...");
    if (true) {
    throw new RuntimeException("运行时异常...");
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    package com.f.spring6.service;

    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;

    /**
    * @author fzy
    * @date 2024/1/25 15:03
    */
    @Component
    @Aspect // 切面类是需要使用@Aspect注解进行标注的
    public class LogAspect { // 切面
    // 切面 = 通知 + 切点
    // 通知就是具体要编写的增强代码
    // @Before注解标注的方法就是一个前置通知
    // @Before(切点表达式)
    // 前置通知
    @Before("execution(* com.f.spring6.service..*(..))")
    public void beforeAdvice() {
    System.out.println("我是一段前置通知...");
    }

    // 后置通知
    @AfterReturning("execution(* com.f.spring6.service..*(..))")
    public void afterReturningAdvice() {
    System.out.println("我是一段后置通知...");
    }

    // 环绕通知(环绕通知是最大的通知,前环绕在前置通知之前,后环绕在所有通知之后)
    @Around("execution(* com.f.spring6.service..*(..))")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    // 前面的增强代码
    // 这段增强代码在前置通知之前执行
    System.out.println("前环绕...");
    // 切点
    joinPoint.proceed();
    // 后面的增强代码
    // 这段增强代码在后置通知之后执行
    System.out.println("后环绕...");
    }

    // 异常通知
    @AfterThrowing("execution(* com.f.spring6.service..*(..))")
    public void afterThrowingAdvice() {
    System.out.println("我是一段异常通知");
    }

    // 最终通知(finally语句块中的通知)
    @After("execution(* com.f.spring6.service..*(..))")
    public void afterAdvice() {
    System.out.println("我是一段最终通知...");
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.f.spring6.test;

    import com.f.spring6.service.UserService;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;

    /**
    * @author fzy
    * @date 2024/1/25 15:17
    */
    public class SpringAOPTest {
    @Test
    public void testAdvice() {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    UserService userService = applicationContext.getBean("userService", UserService.class);
    userService.login();
    }
    }
    • 输出结果:

      1
      2
      3
      4
      5
      6
      7
      8
      前环绕...
      我是一段前置通知...
      系统正在登录...
      我是一段异常通知
      我是一段最终通知...

      java.lang.RuntimeException: 运行时异常...
      ......
    • 当没有异常发生时,输出结果:

      1
      2
      3
      4
      5
      6
      前环绕...
      我是一段前置通知...
      系统正在登录...
      我是一段后置通知...
      我是一段最终通知...
      后环绕...

15.6 切面顺序@Order

  • 使用 @Order 注解可以指定切面的执行顺序。

    • 数字越小,优先级越高。

    下面的代码是别人笔记里的,不是自己写的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    package cw.spring.service;

    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;

    /**
    * ClassName: SecurityAspect
    * Package: cw.spring.service
    * Description:
    *
    * @Author tcw
    * @Create 2023-05-17 9:32
    * @Version 1.0
    */
    @Component
    @Aspect
    @Order(1)
    public class SecurityAspect {
    // 前置通知
    @Before("execution(* cw.spring.service.UserService.*(..))")
    public void beforeAdvice() {
    System.out.println("优先级 1 前置通知...");
    }

    // 后置通知
    @AfterReturning("execution(* cw.spring.service.UserService.*(..))")
    public void afterReturningAdvice() {
    System.out.println("优先级 1 后置通知...");
    }

    /**
    * 环绕通知
    *
    * @param joinPoint 连接点
    */
    @Around("execution(* cw.spring.service.UserService.*(..))")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) {
    // 前环绕
    System.out.println("优先级 1 前环绕...");
    // 调用执行目标
    try {
    joinPoint.proceed();
    } catch (Throwable e) {
    e.printStackTrace();
    }
    // 后环绕
    System.out.println("优先级 1 后环绕...");
    }

    // 最终通知
    @After("execution(* cw.spring.service.UserService.*(..))")
    public void afterAdvice() {
    System.out.println("优先级 1 最终通知...");
    }

    // 异常通知
    @AfterThrowing("execution(* cw.spring.service.UserService.*(..))")
    public void afterThrowingAdvice() {
    System.out.println("优先级 1 异常通知...");
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    package cw.spring.service;

    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;

    /**
    * ClassName: LogAspect
    * Package: cw.spring.service
    * Description:
    * 切面 = 通知 + 切点
    *
    * @Author tcw
    * @Create 2023-05-16 16:03
    * @Version 1.0
    */
    @Component
    @Aspect // 标注为切面类
    @Order(0)
    public class LogAspect {

    // 前置通知
    @Before("execution(* cw.spring.service.UserService.*(..))")
    public void beforeAdvice() {
    System.out.println("优先级 0 前置通知...");
    }

    // 后置通知
    @AfterReturning("execution(* cw.spring.service.UserService.*(..))")
    public void afterReturningAdvice() {
    System.out.println("优先级 0 后置通知...");
    }

    /**
    * 环绕通知
    *
    * @param joinPoint 连接点
    */
    @Around("execution(* cw.spring.service.UserService.*(..))")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) {
    // 前环绕
    System.out.println("优先级 0 前环绕...");
    // 调用执行目标
    try {
    joinPoint.proceed();
    } catch (Throwable e) {
    e.printStackTrace();
    }
    // 后环绕
    System.out.println("优先级 0 后环绕...");
    }

    // 最终通知
    @After("execution(* cw.spring.service.UserService.*(..))")
    public void afterAdvice() {
    System.out.println("优先级 0 最终通知...");
    }

    // 异常通知
    @AfterThrowing("execution(* cw.spring.service.UserService.*(..))")
    public void afterThrowingAdvice() {
    System.out.println("优先级 0 异常通知...");
    }
    }
    • 输出结果:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      优先级 0 前环绕...
      优先级 0 前置通知...
      优先级 1 前环绕...
      优先级 1 前置通知...
      登录中...
      优先级 1 后置通知...
      优先级 1 最终通知...
      优先级 1 后环绕...
      优先级 0 后置通知...
      优先级 0 最终通知...
      优先级 0 后环绕...
    • 前置通知,优先级越高越先执行。

    • 后置通知,优先级越高越后执行。

    • 环绕通知,优先级越高越外层执行。

    • 在切点执行之前执行的通知和在切点执行之后执行的通知,同一优先级执行完,再执行下一级优先级。

15.7 通用切点表达式@Pointcut

  • 使用通用切点表达式可以简化切点表达式,使切点表达式的代码得到复用。

  • 在定义通用切点表达式的切面类中使用 "通用切点表达式对应方法的方法名()" 即可。

    在其他切面类中使用:"包名.定义通用切点表达式的切面类.通用切点表达式对应方法的方法名()" 可以使切点表达式得到复用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    package com.f.spring6.service;

    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;

    /**
    * @author fzy
    * @date 2024/1/25 15:03
    */
    @Component
    @Aspect // 切面类是需要使用@Aspect注解进行标注的
    @Order(2)
    public class LogAspect { // 切面

    ......

    // 最终通知(finally语句块中的通知)
    //@After("execution(* com.f.spring6.service..*(..))")
    @After("commonPointCut()")
    public void afterAdvice() {
    System.out.println("我是一段最终通知...");
    }

    // 通用切点,解决切点表达式复用问题
    // 定义通用的切点表达式
    @Pointcut("execution(* com.f.spring6.service..*(..))")
    public void commonPointCut() {
    // 这个方法只是一个标记,方法名随意,方法体中也不需要写任何代码
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package com.f.spring6.service;

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;

    /**
    * @author fzy
    * @date 2024/1/25 15:47
    */
    @Aspect
    @Component
    @Order(1)
    public class SecurityAspect { // 安全切面
    //@Before("execution(* com.f.spring6.service..*(..))")
    @Before("com.f.spring6.service.LogAspect.commonPointCut()")
    public void beforeAdvice() {
    System.out.println("安全切面的前置通知...");
    }
    }

15.8 连接点JoinPoint

  • 在环绕通知中,Spring 在调用通知方法时,会传入 ProceedingJionPoint 对象(连接点对象)。

  • 除了环绕通知以外的其他通知,Spring 在调用时,会传入 JoinPoint 对象(连接点对象)。

    • 通过 JoinPoint 对象(连接点对象),我们可以获取目标方法的相关信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    package com.f.spring6.service;

    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;

    /**
    * @author fzy
    * @date 2024/1/25 15:47
    */
    @Aspect
    @Component
    @Order(1)
    public class SecurityAspect { // 安全切面
    //@Before("execution(* com.f.spring6.service..*(..))")
    @Before("com.f.spring6.service.LogAspect.commonPointCut()")
    public void beforeAdvice(JoinPoint joinPoint) {
    // 这个JoinPoint joinPoint,在Spring容器调用这个方法的时候会自动传过来。
    // 我们可以用这个joinPoint获取目标方法的相关信息
    // 获取目标方法的签名,即 public void login() 方法的修饰符列表、方法名等
    // 方法的签名:访问权限修饰符开始到方法名
    Signature signature = joinPoint.getSignature();
    // 通过方法的签名可以获取方法的具体信息
    System.out.println("目标方法的方法名:" + signature.getName());
    System.out.println("安全切面的前置通知...");
    }
    }
    • 输出结果:

      1
      2
      3
      4
      5
      6
      7
      8
      目标方法的方法名:login
      安全切面的前置通知...
      前环绕...
      我是一段前置通知...
      系统正在登录...
      我是一段后置通知...
      我是一段最终通知...
      后环绕...

15.9 全注解开发

  • 12.9 小节中已经提到过全注解开发,这里是把 aop 的配置也加进去。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package com.f.spring6.service;

    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;

    /**
    * @author fzy
    * @date 2024/1/25 16:21
    */
    @Configuration // 代替spring.xml文件
    @ComponentScan({"com.f.spring6.service"}) // 组件扫描
    // AOP配置
    @EnableAspectJAutoProxy(proxyTargetClass = true)
    public class Spring6Config {
    }
    1
    2
    3
    4
    5
    6
    @Test
    public void testNoXML() {
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Config.class);
    UserService userService = applicationContext.getBean("userService", UserService.class);
    userService.login();
    }
    • 输出结果:

      1
      2
      3
      4
      5
      6
      7
      8
      目标方法的方法名:login
      安全切面的前置通知...
      前环绕...
      我是一段前置通知...
      系统正在登录...
      我是一段后置通知...
      我是一段最终通知...
      后环绕...

15.10 基于XML配置方式的AOP(了解)

15.11 AOP实际案例:事务处理

  • 项目中的事务控制是在所难免的。在一个业务流程当中,可能需要多条 DML 语句共同完成,为了保证数据的安全,这多条 DML 语句要么同时成功,要么同时失败。这就需要添加事务控制的代码。

  • 在业务类中的每一个业务方法都是需要控制事务的,而控制事务的代码又是固定的格式,都是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    try{
    // 开启事务
    startTransaction();

    // 执行核心业务逻辑
    //......

    // 提交事务
    commitTransaction();
    }catch(Exception e){
    // 回滚事务
    rollbackTransaction();
    }
    • 这个控制事务的代码就是和业务逻辑没有关系的 “交叉业务”。这些交叉业务的代码没有得到复用,在每个业务类的业务方法中,都要开启事务、提交事务以及回滚事务的代码,代码复用低,并且如果这些交叉业务代码需要修改,那必然需要修改多处,难维护,怎么解决?可以采用 AOP 思想解决。

      把以上控制事务的代码作为环绕通知,切入到目标类的方法当中。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      package com.f.spring6.service;

      import org.springframework.stereotype.Service;

      /**
      * @author fzy
      * @date 2024/1/26 10:56
      */
      @Service
      public class AccountService { // 目标对象
      // 目标方法
      // 转账的业务方法
      public void transfer() {
      System.out.println("正在转账...");
      // 故意抛出异常
      throw new RuntimeException();
      }

      // 目标方法
      // 取款的业务方法
      public void withdraw() {
      System.out.println("正在取款...");
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      package com.f.spring6.service;

      import org.springframework.stereotype.Service;

      /**
      * @author fzy
      * @date 2024/1/26 10:57
      */
      @Service
      public class OrderService { // 目标对象
      // 目标方法
      // 生成订单的业务方法
      public void generate() {
      System.out.println("正在生成订单...");
      }

      // 目标方法
      // 取消订单的业务方法
      public void cancel() {
      System.out.println("正在取消订单...");
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      package com.f.spring6.service;

      import org.aspectj.lang.ProceedingJoinPoint;
      import org.aspectj.lang.annotation.Around;
      import org.aspectj.lang.annotation.Aspect;
      import org.springframework.stereotype.Component;

      /**
      * @author fzy
      * @date 2024/1/26 10:58
      */
      @Component
      @Aspect
      public class TransactionAspect {
      @Around("execution(* com.f.spring6.service..*(..))")
      public void aroundAdvice(ProceedingJoinPoint joinPoint) {
      try {
      // 前环绕
      System.out.println("开启事务");
      joinPoint.proceed();
      // 后环绕
      System.out.println("提交事务");
      } catch (Throwable e) {
      System.out.println("回滚事务");
      }
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:context="http://www.springframework.org/schema/context"
      xmlns:aop="http://www.springframework.org/schema/aop"
      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
      http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
      <context:component-scan base-package="com.f.spring6.service"/>
      <aop:aspectj-autoproxy proxy-target-class="true"/>
      </beans>
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      package com.f.spring6.test;

      import com.f.spring6.service.AccountService;
      import com.f.spring6.service.OrderService;
      import org.junit.Test;
      import org.springframework.context.ApplicationContext;
      import org.springframework.context.support.ClassPathXmlApplicationContext;

      /**
      * @author fzy
      * @date 2024/1/26 11:06
      */
      public class AopCaseTest {
      @Test
      public void testTransaction() {
      ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
      AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
      accountService.transfer();
      accountService.withdraw();
      OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
      orderService.generate();
      orderService.cancel();
      }
      }
      • 输出结果:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        开启事务
        正在转账...
        回滚事务 // 这里是回滚事务,因为业务方法中抛出异常了
        开启事务
        正在取款...
        提交事务
        开启事务
        正在生成订单...
        提交事务
        开启事务
        正在取消订单...
        提交事务

15.12 AOP实际案例:安全日志

  • 需求:项目开发结束了,已经上线了,运行正常。客户提出了新的需求:凡是在系统中进行修改操作的,删除操作的,新增操作的,都要把这个人的信息记录下来,因为这几个操作是属于危险行为。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package com.f.spring6.biz;

    import org.springframework.stereotype.Service;

    /**
    * @author fzy
    * @date 2024/1/26 11:18
    */
    @Service
    public class UserService { // 目标对象
    public void saveUser() { // 目标方法
    System.out.println("新增用户信息");
    }

    public void deleteUser() { // 目标方法
    System.out.println("删除用户信息");
    }

    public void modifyUser() { // 目标方法
    System.out.println("修改用户信息");
    }

    public void getUser() {
    System.out.println("获取用户信息");
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package com.f.spring6.biz;

    import org.springframework.stereotype.Service;

    /**
    * @author fzy
    * @date 2024/1/26 11:18
    */
    @Service
    public class VipService { // 目标对象
    public void saveVip() { // 目标方法
    System.out.println("新增会员信息");
    }

    public void deleteVip() { // 目标方法
    System.out.println("删除会员信息");
    }

    public void modifyVip() { // 目标方法
    System.out.println("修改会员信息");
    }

    public void getVip() {
    System.out.println("获取会员信息");
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    package com.f.spring6.biz;

    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.stereotype.Component;

    import java.text.SimpleDateFormat;
    import java.util.Date;

    /**
    * @author fzy
    * @date 2024/1/26 11:20
    */
    @Component
    @Aspect
    public class SecurityAspect {
    // com.f.spring6.biz包下所有以save开头的方法
    @Pointcut("execution(* com.f.spring6.biz..save*(..))")
    public void savePointCut() {
    }

    // com.f.spring6.biz包下所有以delete开头的方法
    @Pointcut("execution(* com.f.spring6.biz..delete*(..))")
    public void deletePointCut() {
    }

    // com.f.spring6.biz包下所有以modify开头的方法
    @Pointcut("execution(* com.f.spring6.biz..modify*(..))")
    public void modifyPointCut() {
    }

    @Before("savePointCut() || deletePointCut() || modifyPointCut()")
    public void beforeAdvice(JoinPoint joinPoint) {
    // 得到当前时间
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String nowTime = sdf.format(new Date());
    // 记录日志信息
    // 如果是WEB项目,用户名字可以从Session中获取
    System.out.println(nowTime + ": " + "张三调用了 " +
    joinPoint.getSignature().getDeclaringTypeName() + " 的 " +
    joinPoint.getSignature().getName() + " 方法");
    }
    }
    • 由于我们的目的只是记录进行增删改操作的相关信息,所以先用通用切点表达式指定增删改方法,然后在通知中使用 @Before("savePointCut() || deletePointCut() || modifyPointCut()") 来选择这些通用切点表达式。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Test
    public void testSercurityLog() {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    UserService userService = applicationContext.getBean("userService", UserService.class);
    userService.saveUser();
    userService.deleteUser();
    userService.modifyUser();
    userService.getUser();
    VipService vipService = applicationContext.getBean("vipService", VipService.class);
    vipService.saveVip();
    vipService.deleteVip();
    vipService.modifyVip();
    vipService.getVip();
    }
    • 输出结果:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      2024-01-26 11:40:43: 张三调用了 com.f.spring6.biz.UserService 的 saveUser 方法
      新增用户信息
      2024-01-26 11:40:43: 张三调用了 com.f.spring6.biz.UserService 的 deleteUser 方法
      删除用户信息
      2024-01-26 11:40:43: 张三调用了 com.f.spring6.biz.UserService 的 modifyUser 方法
      修改用户信息
      获取用户信息
      2024-01-26 11:40:43: 张三调用了 com.f.spring6.biz.VipService 的 saveVip 方法
      新增会员信息
      2024-01-26 11:40:43: 张三调用了 com.f.spring6.biz.VipService 的 deleteVip 方法
      删除会员信息
      2024-01-26 11:40:43: 张三调用了 com.f.spring6.biz.VipService 的 modifyVip 方法
      修改会员信息
      获取会员信息
---------------The End---------------