简介

AOP 的全称是“Aspect Oriented Programming”,即面向切面编程,和 OOP(面向对象编程)类似,也是一种编程思想

AOP 采取横向抽取机制(动态代理),取代了传统纵向继承机制的重复性代码,其应用主要体现在事务处理、日志管理、权限控制、异常处理等方面。主要作用是分离功能性需求和非功能性需求,使开发人员可以集中处理某一个关注点或者横切逻辑,减少对业务代码的侵入,增强代码的可读性和可维护性。

AOP 的作用就是保证开发者在不修改源代码的前提下,为系统中的业务组件添加某种通用功能AOP 就是代理模式的典型应用。

目前最流行的 AOP 框架有两个,分别为 Spring AOP 和 AspectJ

  • Spring AOP 是基于 AOP 编程模式的一个框架,它能够有效的减少系统间的重复代码,达到松耦合的目的。Spring AOP 使用纯 Java 实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。有两种实现方式:基于接口的 JDK 动态代理和基于继承的 CGLIB 动态代理
  • AspectJ 是一个基于 Java 语言的 AOP 框架,从 Spring 2.0 开始,Spring AOP 引入了对 AspectJ 的支持。AspectJ 扩展了 Java 语言,提供了一个专门的编译器,在编译时提供横向代码的植入

spring4.0执行顺序

正常执行顺序

around开始 – before – 方法调用 – around结束 – after 执行 – afterReturning 执行

异常执行顺序

around开始 – before – 方法调用 – around结束 – after 执行 – afterThrowing 执行

spring5.28执行顺序

正常执行顺序

around开始 – before – 方法调用 – afterReturning 执行 – after 执行 – around结束

异常执行顺序

around开始 – before – 方法调用 – afterThrowing 执行 – after 执行 – around结束

普通使用(直接匹配包)

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

/**
* @Author dw
* @ClassName MyAop
* @Description AOP 各个通知使用示例:
* @EnableAspectJAutoProxy : 默认已开启不需要再添加响应的注解
* @Date 2021/8/15 0:21
* @Version 1.0
*/
@Aspect
@Component
public class MyAop {
private final Logger log = LoggerFactory.getLogger(this.getClass());
/**
* 定义切入点 (一个切面类中可以定义多个切入点)
* 拦截所有 com.dw.study.controller 包以及子包下所有类的所有方案
* 第一个 * 表示所有返回值
* 第二个 * 表示所有类
* 第三个 * 表示所有的方法名
* 两个 .. 表示当前包以及子包
**/
@Pointcut("execution(* com.dw.study.controller..*.*(..))")
public void aspect() {}
/**
* 前置通知,目标方法调用前被调用
* 除@Around外,每个方法里都可以加或者不加参数JoinPoint。
* JoinPoint包含了类名、被切面的方法名、参数等属性。
**/
@Before(value = "aspect()")
public void before(JoinPoint joinPoint) {
log.info("AOP before 执行... :参数类型:{}", joinPoint.getArgs());
}

/**
* 最终通知,目标方法执行完之后执行
**/
@After(value = "aspect()")
public void after(JoinPoint joinPoint) {
log.info("AOP after 执行... :{}", joinPoint.toLongString());
}

/**
* 后置返回通知
* 如果参数中的第一个参数为JoinPoint,则第二个参数为返回值的信息
* 如果参数中的第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
* returning 只有目标方法返回值与通知方法具有相应参数类型时才能执行后置返回通知,否则不执行
* 除了使用上面定义好的切面aspect(), 也可以直接使用表达式。
**/
@AfterReturning(value = "execution(* com.dw.study.controller..*.*(..))", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
log.info("AOP afterReturning 执行... :返回结果:{}", result);
}

/**
* 环绕通知
* 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
* 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
* 2:proceedingJoinPoint.proceed() 执行被代理的方法
**/
@Around(value = "aspect()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) {
try {
log.info("AOP around开始... 执行方法... :{}", proceedingJoinPoint.getSignature().getName());
Object proceed = proceedingJoinPoint.proceed();
log.info("AOP around结束... 执行方法... :{}", proceedingJoinPoint.getSignature().getName());
return proceed;
} catch (Throwable throwable) {
log.info("AOP around 执行错误... error :{}", throwable.getMessage());
throwable.printStackTrace();
return "执行around出错。。。";
}
}

/**
* 后置异常通知
* 定义一个名字,该名字用于匹配通知实现方法的一个参数名,当目标方法抛出异常返回后,将把目标方法抛出的异常传给通知方法;
* throwing 只有目标方法抛出的异常与通知方法相应参数异常类型时才能执行后置异常通知,否则不执行,
**/
@AfterThrowing(value = "aspect()", throwing = "exception")
public void afterThrowing(JoinPoint joinPoint, Throwable exception) {
log.error("AOP afterThrowing 执行... , msg : {}", exception.getMessage());
if (exception instanceof NullPointerException)
log.info("空指针异常");
}
}

自定义注解匹配(无参)

1.自定义注解

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodAspect {
}

2.定义切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Aspect
@Component
public class MyAop {
private final Logger log = LoggerFactory.getLogger(this.getClass());/**
* 基于注解定义切入点 2
* 对于注解了 @MethodAspect的方法进行拦截
* 表示标注了特定注解的目标方法链接点。如@annotation(com.dw.study.TestAnnotation)表示任何标注了@TestAnnotation注解的目标类方法。
*/
@Pointcut("@annotation(com.dw.study.customAnnotation.MethodAspect)")
public void testMyAnnotationAspect() {}

/**
* 前置通知,目标方法调用前被调用
* 除@Around外,每个方法里都可以加或者不加参数JoinPoint。
* JoinPoint包含了类名、被切面的方法名、参数等属性。
**/
@Before(value = "testMyAnnotationAspect()")
public void before(JoinPoint joinPoint) {
log.info("AOP before 执行... :参数类型:{}", joinPoint.getArgs());
}
}

3.扫描

1
2
3
4
5
@RequestMapping("hello")
@MethodAspect
public String hello(){
return "SpringBoot-HelloWorld";
}

自定义注解匹配(带参)

1.自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodAspect {
/**
* 传值
* @return
*/
String value();

/**
* 描述
* @return
*/
String description() default "default description";
}

2.定义切面

如果需要获取@annotation中的值,需要和方法参数名相同

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
@Aspect
@Component
public class MyAop {
private final Logger log = LoggerFactory.getLogger(this.getClass());

/**
* 基于注解定义切入点 2
* 对于注解了 @MethodAspect的方法进行拦截
* 表示标注了特定注解的目标方法链接点。如@annotation(com.dw.study.TestAnnotation)表示任何标注了@TestAnnotation注解的目标类方法。
*/
@Pointcut("@annotation(com.dw.study.customAnnotation.MethodAspect)")
public void testMyAnnotationAspect() {}

/**
* 前置通知,目标方法调用前被调用
* 除@Around外,每个方法里都可以加或者不加参数JoinPoint。
* JoinPoint包含了类名、被切面的方法名、参数等属性。
* @annotation中的值,需要和方法参数名相同(重要)
**/
@Before(value = "testMyAnnotationAspect() && @annotation(methodAspect)")
public void before(JoinPoint joinPoint, MethodAspect methodAspect) {
log.info("AOP before 执行... :参数类型:{}", joinPoint.getArgs());
log.info("before-value== "+ methodAspect.value() + "before-description== " + methodAspect.description());
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
System.out.println("URL : " + request.getRequestURL().toString());
System.out.println("HTTP_METHOD : " + request.getMethod());
System.out.println("IP : " + request.getRemoteAddr());
System.out.println("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
System.out.println("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}

/**
* 环绕通知
* 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
* 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
* 2:proceedingJoinPoint.proceed() 执行被代理的方法
* @annotation中的值,需要和方法参数名相同
**/
@Around(value = "testMyAnnotationAspect() && @annotation(methodAspect)")
public Object around(ProceedingJoinPoint proceedingJoinPoint, MethodAspect methodAspect) {
try {
log.info("AOP around开始... 执行方法... :{}", proceedingJoinPoint.getSignature().getName());
log.info("around-value== "+ methodAspect.value() + "around-description== " + methodAspect.description());
Object proceed = proceedingJoinPoint.proceed();
log.info("AOP around结束... 执行方法... :{}", proceedingJoinPoint.getSignature().getName());
return proceed;
} catch (Throwable throwable) {
log.info("AOP around 执行错误... error :{}", throwable.getMessage());
throwable.printStackTrace();
return "执行around出错。。。";
}
}
}

附文

为了更好地理解 AOP,我们需要了解一些它的相关术语。这些专业术语并不是 Spring 特有的,有些也同样适用于其它 AOP 框架,如 AspectJ。

它们的含义如下表所示:

名称 说明
Joinpoint(连接点) 指那些被拦截到的点,在 Spring 中,指可以被动态代理拦截目标类的方法。
Pointcut(切入点) 指要对哪些 Joinpoint 进行拦截,即被拦截的连接点。
Advice(通知) 指拦截到 Joinpoint 之后要做的事情,即对切入点增强的内容。
Target(目标) 指代理的目标对象。
Weaving(植入) 指把增强代码应用到目标上,生成代理对象的过程。
Proxy(代理) 指生成的代理对象。
Aspect(切面) 切入点和通知的结合。

Advice 直译为通知,也有的资料翻译为“增强处理”,共有 5 种类型,如下表所示:

通知 说明
before(前置通知) 通知方法在目标方法调用之前执行
after(后置通知) 通知方法在目标方法返回或异常后调用
after-returning(返回后通知) 通知方法会在目标方法返回后调用
after-throwing(抛出异常通知) 通知方法会在目标方法抛出异常后调用
around(环绕通知) 通知方法会将目标方法封装起来

execution表达式:

用于匹配方法执行的连接点,属于方法级别, 语法: execution(修饰符 返回值类型 方法名(参数)异常)

语法参数 描述
修饰符 可选,如public,protected,写在返回值前,任意修饰符填*号就可以
返回值类型 必选,可以使用*来代表任意返回值
方法名 必选,可以用*来代表任意方法
参数 必选, ()代表是没有参数,(..)代表是匹配任意数量,任意类型的参数,当然也可以指定类型的参数进行匹配,如要接受一个String类型的参数,则(java.lang.String), 任意数量的String类型参数:(java.lang.String..)等等。。。
异常 可选,语法:throws 异常,异常是完整带包名,可以是多个,用逗号分隔

execution()表达式案例

  • 拦截com.dw.study包下的所有子包里的任意类的任意方法
    execution(* com.dw.study..*.*(..))
  • 拦截com.dw.study.Test2Controller下的任意方法
    execution(* **com.dw.study**.Test2Controller.*(..))
  • 拦截任何修饰符为public的方法
    execution(public * * (..))
  • 拦截com.dw.study下的所有子包里的以ok开头的方法
    execution(* **com.dw.study**..*.ok* (..))

JoinPoint

JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象。 除 @Around 外,每个通知的方法里都可以加或者不加参数JoinPoint。JoinPoint包含了类名、被切面的方法名、参数等属性。@Around 参数必须为 ProceedingJoinPoint

方法名 功能
Signature getSignature(); 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
Object[] getArgs(); 获取传入目标方法的参数对象
Object getTarget(); 获取被代理的对象
Object getThis(); 获取代理对象

ProceedingJoinPoint

ProceedingJoinPoint对象是JoinPoint 的子接口, 该对象只用在@Around的切面方法中:

方法名 功能
Object proceed() throws Throwable 执行目标方法
Object proceed(Object[] var1) throws Throwable 传入的新的参数去执行目标方法

小结

AOP 是 Spring 的核心之一,在 Spring 中经常会使用 AOP 来简化编程。

在 Spring 框架中使用 AOP 主要有以下优势:

  • 提供声明式企业服务,特别是作为 EJB 声明式服务的替代品。最重要的是,这种服务是声明式事务管理。
  • 允许用户实现自定义切面。在某些不适合用 OOP 编程的场景中,采用 AOP 来补充。
  • 可以对业务逻辑的各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时也提高了开发效率。

参考