f10@t's blog

Spring AOP再探

字数统计: 2k阅读时长: 8 min
2023/10/22

在学习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>
如果是在spring环境下使用,那么推荐使用上述依赖,其中已经包含了基础的aspectj-weaver,且你无需手动注册切面,只需要使用@Component将其注册到容器中即可。

需要注意的是,在Spring框架中其实有两套AOP实现方法,一种是基于ASpectJ的AOP,另一种是基于代理的Spring原生AOP。区别如下:

  • 基于AspectJ方案拥有完整的AspectJ框架以及更丰富和复杂的切面控制,支持编译时织入(ajc编译器)或加载时织入(Load-Time Weaving)。
  • 基于代理的Spring原生AOP:更轻量级,满足大多数需求,具备特殊的bean表达式。但相较于AspectJ支持更少的表达式,如getcallsetpreinitializationifcflow等。

其实整个过程还是比较简单的,可以先入为主参考如下代码,其中定义了所有的五类通知类型。其他代码此处省略。

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
@Aspect
@Log4j2
@Component
public class PeopleServiceAspect {
/**
* execution开始定义切入点
* 含义:“返回值为任意类型的lzw.spring.springreview.service.impl.PeopleServiceImpl下getPeople方法,参数列表为String,Integer”
* * - 任意返回值
* 类的全路径
* 方法名称及参数
*/
@Pointcut(value = "execution(* lzw.spring.springreview.service.impl.PeopleServiceImpl.getPeople(String, Integer))")
public void pointcutDef() {
}

/**
* 前置通知
*
* @param joinPoint 连接点
*/
@Before(value = "pointcutDef()")
public void beforeGetPeople(JoinPoint joinPoint) {
log.info(joinPoint.getSignature().getName() + " 执行前");
}

/**
* 后置通知
*
* @param joinPoint
*/
@After(value = "pointcutDef()")
public void afterGetPeople(JoinPoint joinPoint) {
log.info(joinPoint.getSignature().getName() + " 执行后");
}

/**
* 返回通知
*
* @param joinPoint
* @param peopleToReturn
*/
@AfterReturning(value = "pointcutDef()", returning = "peopleToReturn")
public void afterReturning(JoinPoint joinPoint, People peopleToReturn) {
log.info(joinPoint.getSignature().getName() + "的返回值是" + peopleToReturn.toString());
}

/**
* 异常通知
*
* @param joinPoint
* @param e
*/
@AfterThrowing(value = "pointcutDef()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
log.info(joinPoint.getSignature() + "方法抛出异常了,异常是" + e);
}

/**
* 环绕通知
*
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around(value = "pointcutDef()")
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
8
package demo.annotation;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {
String logDesc() default "";
}
定义一个切面,其中包含一个使用@annotation定义的切点,以及一个前置通知,并将这个类注册到容器中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Aspect
@Component
public class MyLogAspect {

private static final Logger logger = LoggerFactory.getLogger(MyLogAspect.class);

@Pointcut("@annotation(demo.annotation.MyLog)")
private void myPointCut() {}

@Before("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
package demo.service;

public class SimpleTestService {

private static final Logger logger = LoggerFactory.getLogger(SimpleTestService.class);

@MyLog(logDesc = "插播一条新闻哥们")
public void callLog() {
logger.info("服务代码调用");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package demo.controller;

@RestController
public class SimpleTestController {
private final SimpleTestService simpleTestService;

@Autowired
public SimpleTestController(SimpleTestService simpleTestService) {
this.simpleTestService = simpleTestService;
}

@GetMapping(path = "/babyAnnotationLog")
public void test() {
simpleTestService.callLog();
}
}
SimpleTestServicecallLog方法被调用时,由于该方法上标记了@MyLog注解,因此会触发切面的通知:

再比如,我们使用Spring特有的bean表达式写一个切面,实现当我们的服务类加载前织入一个前置通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package demo.aspect;

@Aspect
@Component
public class BeanLimitedEditionAspect {

private static final Logger logger = Logger.getLogger(BeanLimitedEditionAspect.class.getName());

@Pointcut("bean(simpleTestService)")
private void beanPointCut(){}

@Before("beanPointCut()")
public void beforeBeanCut() {
logger.info("目标Bean已经加载");
}
}
启动后,当我们请求接口时,会触发该bean实例的使用,从而触发我们的通知:

参考学习

CATALOG
  1. 1. 基于注解编写切面
  2. 2. 不同表达式和Demo
  3. 3. 参考学习