在学习Spring Framework的时候接触过AOP的相关内容—>Spring基础学习 - AOP机制
· f10@t's blog (f10at.cn)。
但是当时没有记录使用注解的方式,且仅学习了Advisor没有了解其与Aspect的联系和区别,遂补一个坑。
喜欢我昆明池吗?
基于注解编写切面
pom文件依赖如下:
1 2 3 4 5 6
| <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支持更少的表达式,如
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
| @Aspect @Log4j2 @Component public class PeopleServiceAspect {
@Pointcut(value = "execution(* lzw.spring.springreview.service.impl.PeopleServiceImpl.getPeople(String, Integer))") public void pointcutDef() { }
@Before(value = "pointcutDef()") public void beforeGetPeople(JoinPoint joinPoint) { log.info(joinPoint.getSignature().getName() + " 执行前"); }
@After(value = "pointcutDef()") public void afterGetPeople(JoinPoint joinPoint) { log.info(joinPoint.getSignature().getName() + " 执行后"); }
@AfterReturning(value = "pointcutDef()", returning = "peopleToReturn") public void afterReturning(JoinPoint joinPoint, People peopleToReturn) { log.info(joinPoint.getSignature().getName() + "的返回值是" + peopleToReturn.toString()); }
@AfterThrowing(value = "pointcutDef()", throwing = "e") public void afterThrowing(JoinPoint joinPoint, Exception e) { log.info(joinPoint.getSignature() + "方法抛出异常了,异常是" + e); }
@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(); } }
|
当SimpleTestService
的callLog
方法被调用时,由于该方法上标记了@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实例的使用,从而触发我们的通知:
参考学习
v1.5.2