装配AOP

在AOP编程中,我们经常会遇到下面的概念:

  • Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
  • Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
  • Pointcut:切入点,即一组连接点的集合;
  • Advice:增强,指特定连接点上执行的动作;
  • Introduction:引介,指为一个已有的Java对象动态地增加新的接口;
  • Weaving:织入,指将切面整合到程序的执行流程中;
  • Interceptor:拦截器,是一种实现增强的方式;
  • Target Object:目标对象,即真正执行业务的核心逻辑对象;
  • AOP Proxy:AOP代理,是客户端持有的增强后的对象引用。

看完上述术语,是不是感觉对AOP有了进一步的困惑?其实,我们不用关心AOP创造的“术语”,只需要理解AOP本质上只是一种代理模式的实现方式,在Spring的容器中实现AOP特别方便。

我们以UserServiceMailService为例,这两个属于核心业务逻辑,现在,我们准备给UserService的每个业务方法执行前添加日志,给MailService的每个业务方法执行前后添加日志,在Spring中,需要以下步骤:

首先,我们通过Maven引入Spring对AOP的支持:

  1. <dependency>
  2. <groupId>org.springframework</groupId>
  3. <artifactId>spring-aspects</artifactId>
  4. <version>${spring.version}</version>
  5. </dependency>

上述依赖会自动引入AspectJ,使用AspectJ实现AOP比较方便,因为它的定义比较简单。

然后,我们定义一个LoggingAspect

  1. @Aspect
  2. @Component
  3. public class LoggingAspect {
  4. // 在执行UserService的每个方法前执行:
  5. @Before("execution(public * com.itranswarp.learnjava.service.UserService.*(..))")
  6. public void doAccessCheck() {
  7. System.err.println("[Before] do access check...");
  8. }
  9. // 在执行MailService的每个方法前后执行:
  10. @Around("execution(public * com.itranswarp.learnjava.service.MailService.*(..))")
  11. public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
  12. System.err.println("[Around] start " + pjp.getSignature());
  13. Object retVal = pjp.proceed();
  14. System.err.println("[Around] done " + pjp.getSignature());
  15. return retVal;
  16. }
  17. }

观察doAccessCheck()方法,我们定义了一个@Before注解,后面的字符串是告诉AspectJ应该在何处执行该方法,这里写的意思是:执行UserService的每个public方法前执行doAccessCheck()代码。

再观察doLogging()方法,我们定义了一个@Around注解,它和@Before不同,@Around可以决定是否执行目标方法,因此,我们在doLogging()内部先打印日志,再调用方法,最后打印日志后返回结果。

LoggingAspect类的声明处,除了用@Component表示它本身也是一个Bean外,我们再加上@Aspect注解,表示它的@Before标注的方法需要注入到UserService的每个public方法执行前,@Around标注的方法需要注入到MailService的每个public方法执行前后。

紧接着,我们需要给@Configuration类加上一个@EnableAspectJAutoProxy注解:

  1. @Configuration
  2. @ComponentScan
  3. @EnableAspectJAutoProxy
  4. public class AppConfig {
  5. ...
  6. }

Spring的IoC容器看到这个注解,就会自动查找带有@Aspect的Bean,然后根据每个方法的@Before@Around等注解把AOP注入到特定的Bean中。执行代码,我们可以看到以下输出:

  1. [Before] do access check...
  2. [Around] start void com.itranswarp.learnjava.service.MailService.sendRegistrationMail(User)
  3. Welcome, test!
  4. [Around] done void com.itranswarp.learnjava.service.MailService.sendRegistrationMail(User)
  5. [Before] do access check...
  6. [Around] start void com.itranswarp.learnjava.service.MailService.sendLoginMail(User)
  7. Hi, Bob! You are logged in at 2020-02-14T23:13:52.167996+08:00[Asia/Shanghai]
  8. [Around] done void com.itranswarp.learnjava.service.MailService.sendLoginMail(User)

这说明执行业务逻辑前后,确实执行了我们定义的Aspect(即LoggingAspect的方法)。

有些童鞋会问,LoggingAspect定义的方法,是如何注入到其他Bean的呢?

其实AOP的原理非常简单。我们以LoggingAspect.doAccessCheck()为例,要把它注入到UserService的每个public方法中,最简单的方法是编写一个子类,并持有原始实例的引用:

  1. public UserServiceAopProxy extends UserService {
  2. private UserService target;
  3. private LoggingAspect aspect;
  4. public UserServiceAopProxy(UserService target, LoggingAspect aspect) {
  5. this.target = target;
  6. this.aspect = aspect;
  7. }
  8. public User login(String email, String password) {
  9. // 先执行Aspect的代码:
  10. aspect.doAccessCheck();
  11. // 再执行UserService的逻辑:
  12. return target.login(email, password);
  13. }
  14. public User register(String email, String password, String name) {
  15. aspect.doAccessCheck();
  16. return target.register(email, password, name);
  17. }
  18. ...
  19. }

这些都是Spring容器启动时为我们自动创建的注入了Aspect的子类,它取代了原始的UserService(原始的UserService实例作为内部变量隐藏在UserServiceAopProxy中)。如果我们打印从Spring容器获取的UserService实例类型,它类似UserServiceEnhancerBySpringCGLIBEnhancerBySpringCGLIB1f44e01c,实际上是Spring使用CGLIB动态创建的子类,但对于调用方来说,感觉不到任何区别。

Spring对接口类型使用JDK动态代理,对普通类使用CGLIB创建子类。如果一个Bean的class是final,Spring将无法为其创建子类。

可见,虽然Spring容器内部实现AOP的逻辑比较复杂(需要使用AspectJ解析注解,并通过CGLIB实现代理类),但我们使用AOP非常简单,一共需要三步:

  1. 定义执行方法,并在方法上通过AspectJ的注解告诉Spring应该在何处调用此方法;
  2. 标记@Component@Aspect
  3. @Configuration类上标注@EnableAspectJAutoProxy

至于AspectJ的注入语法则比较复杂,请参考Spring文档

Spring也提供其他方法来装配AOP,但都没有使用AspectJ注解的方式来得简洁明了,所以我们不再作介绍。

拦截器类型

顾名思义,拦截器有以下类型:

  • @Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了;

  • @After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行;

  • @AfterReturning:和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码;

  • @AfterThrowing:和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码;

  • @Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能。

练习

装配AOP - 图1下载练习:使用AOP实现日志 (推荐使用IDE练习插件快速下载)

小结

在Spring容器中使用AOP非常简单,只需要定义执行方法,并用AspectJ的注解标注应该在何处触发并执行。

Spring通过CGLIB动态创建子类等方式来实现AOP代理模式,大大简化了代码。

读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论:

装配AOP - 图2 装配AOP - 图3