【笔记】Spring实现AOP

前言

Spring实现AOP学习笔记

AOP介绍

  • 面向切面的程序设计(Aspect-oriented programming,AOP,又译作面向方面的程序设计、剖面导向程序设计)是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与装饰,如“对所有方法名以‘set*’开头的方法添加后台日志”。该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。面向切面的程序设计思想也是面向切面软件开发的基础。(维基百科

AOP的核心概念

  • 连接点(JoinPoint):任何核心业务代码
  • 切入点(Pointcut):需要被扩展功能的核心业务代码
  • 通知(Advice):扩展功能代码,通常是共性功能
  • 切面(Aspect):通知(扩展功能代码)与切入点(需要被扩展功能的核心业务代码)的映射关系

Spring中AOP的核心概念

  • 连接点(JoinPoint):方法的执行

  • 切入点(Pointcut):可以只描述一个方法,也可以描述多个方法

  • 通知(Advice):功能最终以方法的形式呈现

  • 切面(Aspect):描述通知与切入点的对应关系

  • 通知类:用于定义通知的类

  • 目标对象和代理

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

引入依赖

  • 导入spring-context包后,会自动导入spring-aop包,因为spring-context依赖于spring-aop
pom.xml
1
2
3
4
5
6
7
8
9
10
<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>

启动AOP

  • 在Spring核心配置类上使用@EnableAspectJAutoProxy启动使用注解定义通知类,让Spring扫描包含@Aspect的类,让这种类作为通知类而不是普通的bean
1
2
3
@EnableAspectJAutoProxy
public class SpringConfig {
}

创建通知类(入门案例)

  • 举例:在所有dao包下任何类中的任何方法执行之前输出系统当前时间
  1. 创建一个通知类。要求:类名自定义
  2. 创建一个通知方法。要求:方法名自定义
  3. 创建一个切入点方法。要求:私有的,无返回值,方法名自定义,无参数,有方法体但是无内容
  4. 在切入点方法上,利用注解描述映射的切入点
  5. 在通知方法上,利用通知类型的注解描述通知与切入点的映射关系以和通知切入时机

@Pointcut("execution()"):描述切入点

* *..*(..)切入点表达式
@Before(""):通过通知类型注解,指定切入点方法。可以直接指定切入点方法,表示在当前类中,也可以指定类名.方法名()

src/main/java/com/aop/MyAdvice.java
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
package com.aop;

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

@Component
@Aspect
// 1. 创建一个通知类
public class MyAdvice {

// 4. 利用注解描述切入点
@Pointcut("execution(* *..*(..))")
// 3. 创建一个切入点方法
private void forMethod() {}

// 5. 映射通知和切入点的关系
@Before("forMethod()")
// 2. 创建一个通知方法
public void method() {
// 在方法中定义通知的业务代码
System.out.println(System.currentTimeMillis());
}
}

Spring中AOP的工作流程

  • Spring容器启动
  • 读取所有切面配置的切入点
    • 如果某些切入点被定义了,但是没有被通知映射,那么不会读取
  • 初始化bean,并判定bean中的方法是有匹配到了已经读取的切入点
    • 如果匹配失败,创建bean对象
      • 获取bean,执行方法
    • 如果匹配成功,创建原始bean对象(又称目标对象)的代理对象
      • 获取的bean是代理对象时,根据代理对象的运行模式,运行原始方法与增强的内容,完成操作

切入点表达式

  • 切入点:需要被增强的方法

  • 切入点表达式:需要被增强的方法的描述

  • 书写技巧(示例:* com.*.*Service.find*(..)

    • 对于访问权限修饰符
      • 访问权限修饰符针对接口开发均采用public描述,可以省略访问权限修饰符
    • 对于返回值类型
      • 返回值类型对于增删改操作使用精准匹配,对于查询操作使用模糊匹配
    • 对于包名
      • 包名书写尽量不使用..通配符,而是使用*通配符描述单个包,或精准匹配
    • 对于类名和接口名
      • 描述切入点时通常描述接口,而不描述实现类
        • 原因是为了降低耦合
      • 类名和接口名采用*通配符
        • 例如使用*Service绑定业务层接口
    • 对于方法名
      • 方法名中动词采用精准匹配,名词采用*通配符
    • 对于异常
      • 通常不使用异常作为匹配规则

语法格式

  • 动作关键字(访问修饰符 返回值类型 包名.类名.方法名(参数)异常名)

动作关键字

exection:执行时
访问控制修饰符:可以省略
异常名:可以省略

通配符

*:匹配一个任意东西
..:匹配多个任意东西
+:匹配所有子类,写在类名结尾

通知类型

@Before():前置通知
@After():后置通知
@Around():环绕通知
@AfterReturning():返回后通知,在方法成功运行结束后执行
@AfterThrowing():抛出异常后通知,在方法抛出异常之前运行

环绕通知

  • 如果原始方法有返回值,环绕通知方法的返回值类型必须是Object,如果为void,那么执行结果为空值
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.aop;

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

@Component
@Aspect
public class MyAdvice {

@Pointcut("execution(* com.dao.UserDao.selectAll())")
private void pt() {}

@Around("pt()")
public Object method(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 切入点之前的通知
...
// 执行切入点
Object obj = proceedingJoinPoint.proceed();
// 切入点之后的通知
...
// 将返回值返回
return obj;
}
}

获取签名信息

获取类的全局限定名
  • 返回Class类型
1
signature.getDeclaringType();
获取字符串类型的类的全局限定名
  • 返回字符串类型
1
signature.getDeclaringTypeName();
获取字符串类型的方法名
1
signature.getName();

获取参数

通过JoinPoint

  • 适用于:前置通知、后置通知、返回后通知、抛出异常后通知
1
2
3
private void method(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
}

通过ProceedingJoinPoint

  • 适用于:环绕通知
1
2
3
4
5
6
7
public Object method(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object[] args = proceedingJoinPoint.getArgs();
// 处理参数数据
...
// 执行切入点,传递新的参数
Object obj = proceedingJoinPoint.proceed(args);
}

获取返回值

通过JoinPoint

  • 适用于:返回后通知
  • 利用注解的returning属性将返回值传递给形参
  • 如果形参有JoinPoint类型的参数,必须是第一个形参

returning = "obj":将返回值传递给形参

1
2
3
4
@AfterReturning(value = "", returning = "obj")
private void method(JoinPoint joinPoint, Object obj) {
...
}

通过ProceedingJoinPoint

  • 适用于:环绕通知
  • 执行切入点,直接获取返回值
1
2
3
public Object method(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object obj = proceedingJoinPoint.proceed(args);
}

获取异常

通过JoinPoint

  • 适用于:抛出异常后通知
  • 利用注解的throwing属性将异常传递给形参
  • 如果形参有JoinPoint类型的参数,必须是第一个形参
1
2
3
4
@AfterThrowing(value = "", throwing = "throwable")
private void method(Throwable throwable) {
...
}

通过ProceedingJoinPoint

  • 适用于:环绕通知
  • 直接通过try…catch代码块获取异常
1
2
3
4
5
6
7
8
public Object method(ProceedingJoinPoint proceedingJoinPoint) {
Object obj = null;
try {
obj = proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}

完成

参考文献

哔哩哔哩——黑马程序员