在学习Spring Framework的时候接触过AOP的相关内容—>Spring基础学习 - AOP机制 · f10@t's blog (f10at.cn)。
但是当时没有记录使用注解的方式,且仅学习了Advisor没有了解其与Aspect的联系和区别,遂补一个坑。
基于注解编写切面
pom文件依赖如下: 1
2
3
4
5
6<!-- Spring aop支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>${spring-boot.version}</version>
</dependency>aspectj-weaver
,且你无需手动注册切面,只需要使用@Component
将其注册到容器中即可。
需要注意的是,在Spring框架中其实有两套AOP实现方法,一种是基于ASpectJ的AOP,另一种是基于代理的Spring原生AOP。区别如下:
- 基于AspectJ方案拥有完整的AspectJ框架以及更丰富和复杂的切面控制,支持编译时织入(ajc编译器)或加载时织入(Load-Time Weaving)。
- 基于代理的Spring原生AOP:更轻量级,满足大多数需求,具备特殊的bean表达式。但相较于AspectJ支持更少的表达式,如
get
、call
、set
、preinitialization
、if
、cflow
等。
其实整个过程还是比较简单的,可以先入为主参考如下代码,其中定义了所有的五类通知类型。其他代码此处省略。
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
public class PeopleServiceAspect {
/**
* execution开始定义切入点
* 含义:“返回值为任意类型的lzw.spring.springreview.service.impl.PeopleServiceImpl下getPeople方法,参数列表为String,Integer”
* * - 任意返回值
* 类的全路径
* 方法名称及参数
*/
public void pointcutDef() {
}
/**
* 前置通知
*
* @param joinPoint 连接点
*/
public void beforeGetPeople(JoinPoint joinPoint) {
log.info(joinPoint.getSignature().getName() + " 执行前");
}
/**
* 后置通知
*
* @param joinPoint
*/
public void afterGetPeople(JoinPoint joinPoint) {
log.info(joinPoint.getSignature().getName() + " 执行后");
}
/**
* 返回通知
*
* @param joinPoint
* @param peopleToReturn
*/
public void afterReturning(JoinPoint joinPoint, People peopleToReturn) {
log.info(joinPoint.getSignature().getName() + "的返回值是" + peopleToReturn.toString());
}
/**
* 异常通知
*
* @param joinPoint
* @param e
*/
public void afterThrowing(JoinPoint joinPoint, Exception e) {
log.info(joinPoint.getSignature() + "方法抛出异常了,异常是" + e);
}
/**
* 环绕通知
*
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info(proceedingJoinPoint.getSignature().getName() + "环绕通知前");
Object toReturn = proceedingJoinPoint.proceed();
log.info(proceedingJoinPoint.getSignature().getName() + "环绕通知后");
return toReturn;
}
}
直接运行,从输出中就可以看到AOP的效果了:
从运行结果可以看出这五类通知和切入点函数的相对执行顺序:
环绕通知->前置通知->返回通知->后置通知->环绕通知。而异常通知则在切入点函数抛出异常时执行。
上述代码关键点:
- @Aspect:代表这是一个切面定义类;
- @Pointcut:定义一个切点,可以使用
execution
关键字来定义精细化的函数切入点位置; - @Before:前置通知,
value
属性指定切点; - @After:后置通知,
value
属性指定切点; - @Around:环绕通知,
value
属性指定切点; - @AfterReturning:返回通知,
value
属性指定切点,returning
属性指定返回对象变量名称; - @AfterThrowing:异常通知,
value
属性指定切点,throwing
属性指定异常变量名称;
因而在使用Spring
AOP时,我们针对需要切面编程的服务单元编写一个切面,其中可以定义多个@Pointcut
注解的函数并配合execution
表达式实现精细化的切点定义,然后针对这些切点定义不同的通知类型。
exection
表达式基于AspectJ的切点表达式语言(AspectJ
pointcut designators,PCD),其含义及上述例子中的表达式如下:
这里有一个区别,在之前的文章(Spring基础学习 - AOP机制 · f10@t's blog (f10at.cn))中我学习的Spring切面实际上只是Advisor,即由一个切点和一个通知组成,可以理解为一个取了特值的Aspect。如上述代码所示,可以看到实际上对于一个真正的Aspect切面,我们是可以定义多个切点和多个通知的。
但其实Advisor也不是完全没用。个人理解在Spring中,我们也可以通过将不同Advisor注入到容器中,并根据不同需求使用ProxyFactoryBean将多个需要的Advisor组装成一个Aspect,从而提高代码的复用性。
不同表达式和Demo
此外,除了上述的exection
表达式,Spring框架下的PCD还支持如下8个表达式。从类别上可以分为匹配连接点和匹配注解。
- within:用于匹配指定类型内的连接点,如
within(com.example.service.MyService)
会匹配该类下所有的方法执行。 - @within:用于匹配类级别注解,如
@within(com.example.service.ServiceLayer)
会匹配所有带有@ServiceLayer
注解的类中的所有方法。 - this:用于匹配代理对象为指定类型的连接点,如
this (com.example.service.MyService)
会匹配代理对象为MyService
或其子类的所有方法执行。 - target:与this类似,当匹配对象为真实对象而非代理对象的类型。如
target (com.example.service.MyService)
会匹配真实对象为MyService
或其子类的所有方法执行。 - @target:类似
@within
,但匹配范围更小。如@target(com.example.annotation.Transaction)
会匹配所有目标对象类中标记了@Transaction
注解的方法。 - args用于匹配具有特定参数类型或值的方法的调用。如
args(java.lang.String, ..)
会匹配所有第一个参数为字符串类型,其余参数任意的方法执行。 - @args:用于匹配方法参数中具有指定注解的连接点。如
@args(com.example.annotation.Nullable)
会匹配任何方法参数中包含了@Nullable
的执行。 - @annotation:用于匹配带有指定注解的方法执行。如
@annotation(com.example.annotation.MyCustomAnnotation)
会匹配所有带有@MyCustomAnnotation
注解的方法执行。
此外,Spring中还支持一个特殊的注解bean
: -
bean:限制切点的匹配为一个或一系列指定名称的Spring
Bean。如``
下面以@annotation
注解为例,首先自定义一个注解,然后实现匹配该自定义注解的效果:
首先写一个自定义的注解,代表一个自定义的日志注解: 1
2
3
4
5
6
7
8package demo.annotation;
public MyLog {
String logDesc() default "";
}@annotation
定义的切点,以及一个前置通知,并将这个类注册到容器中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyLogAspect {
private static final Logger logger = LoggerFactory.getLogger(MyLogAspect.class);
private void myPointCut() {}
public void beforeLog(JoinPoint point) {
MethodSignature methodSignature = (MethodSignature) point.getSignature();
MyLog myLogAnnotation = methodSignature.getMethod().getAnnotation(MyLog.class);
logger.info("目标日志的内容为:{}", myLogAnnotation.logDesc());
}
}1
2
3
4
5
6
7
8
9
10
11
12
13package demo.service;
public class SimpleTestService {
private static final Logger logger = LoggerFactory.getLogger(SimpleTestService.class);
public void callLog() {
logger.info("服务代码调用");
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package demo.controller;
public class SimpleTestController {
private final SimpleTestService simpleTestService;
public SimpleTestController(SimpleTestService simpleTestService) {
this.simpleTestService = simpleTestService;
}
public void test() {
simpleTestService.callLog();
}
}SimpleTestService
的callLog
方法被调用时,由于该方法上标记了@MyLog
注解,因此会触发切面的通知:
再比如,我们使用Spring特有的bean
表达式写一个切面,实现当我们的服务类加载前织入一个前置通知:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package demo.aspect;
public class BeanLimitedEditionAspect {
private static final Logger logger = Logger.getLogger(BeanLimitedEditionAspect.class.getName());
private void beanPointCut(){}
public void beforeBeanCut() {
logger.info("目标Bean已经加载");
}
}
参考学习
- spring-framework:pointcuts
- spring-framework:aop-pointcuts-designators
- 《Spring Boot进阶 原理、实战与面试题分析》——郑天民