共计 3883 个字符,预计需要花费 10 分钟才能阅读完成。
在 AOP 编程中,我们经常会遇到下面的概念:
- Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
- Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
- Pointcut:切入点,即一组连接点的集合;
- Advice:增强,指特定连接点上执行的动作;
- Introduction:引介,指为一个已有的 Java 对象动态地增加新的接口;
- Weaving:织入,指将切面整合到程序的执行流程中;
- Interceptor:拦截器,是一种实现增强的方式;
- Target Object:目标对象,即真正执行业务的核心逻辑对象;
- AOP Proxy:AOP 代理,是客户端持有的增强后的对象引用。
看完上述术语,是不是感觉对 AOP 有了进一步的困惑?其实,我们不用关心 AOP 创造的“术语”,只需要理解 AOP 本质上只是一种代理模式的实现方式,在 Spring 的容器中实现 AOP 特别方便。
我们以 UserService
和MailService
为例,这两个属于核心业务逻辑,现在,我们准备给 UserService
的每个业务方法执行前添加日志,给 MailService
的每个业务方法执行前后添加日志,在 Spring 中,需要以下步骤:
首先,我们通过 Maven 引入 Spring 对 AOP 的支持:
- org.springframework:spring-aspects:6.0.0
上述依赖会自动引入 AspectJ,使用 AspectJ 实现 AOP 比较方便,因为它的定义比较简单。
然后,我们定义一个LoggingAspect
:
@Aspect
@Component
public class LoggingAspect {// 在执行 UserService 的每个方法前执行:
@Before("execution(public * com.itranswarp.learnjava.service.UserService.*(..))")
public void doAccessCheck() {System.err.println("[Before] do access check...");
}
// 在执行 MailService 的每个方法前后执行:
@Around("execution(public * com.itranswarp.learnjava.service.MailService.*(..))")
public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {System.err.println("[Around] start" + pjp.getSignature());
Object retVal = pjp.proceed();
System.err.println("[Around] done" + pjp.getSignature());
return retVal;
}
}
观察 doAccessCheck()
方法,我们定义了一个 @Before
注解,后面的字符串是告诉 AspectJ 应该在何处执行该方法,这里写的意思是:执行 UserService
的每个 public
方法前执行 doAccessCheck()
代码。
再观察 doLogging()
方法,我们定义了一个 @Around
注解,它和 @Before
不同,@Around
可以决定是否执行目标方法,因此,我们在 doLogging()
内部先打印日志,再调用方法,最后打印日志后返回结果。
在 LoggingAspect
类的声明处,除了用 @Component
表示它本身也是一个 Bean 外,我们再加上 @Aspect
注解,表示它的 @Before
标注的方法需要注入到 UserService
的每个 public
方法执行前,@Around
标注的方法需要注入到 MailService
的每个 public
方法执行前后。
紧接着,我们需要给 @Configuration
类加上一个 @EnableAspectJAutoProxy
注解:
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AppConfig {...}
Spring 的 IoC 容器看到这个注解,就会自动查找带有 @Aspect
的 Bean,然后根据每个方法的 @Before
、@Around
等注解把 AOP 注入到特定的 Bean 中。执行代码,我们可以看到以下输出:
[Before] do access check...
[Around] start void com.itranswarp.learnjava.service.MailService.sendRegistrationMail(User)
Welcome, test!
[Around] done void com.itranswarp.learnjava.service.MailService.sendRegistrationMail(User)
[Before] do access check...
[Around] start void com.itranswarp.learnjava.service.MailService.sendLoginMail(User)
Hi, Bob! You are logged in at 2020-02-14T23:13:52.167996+08:00[Asia/Shanghai]
[Around] done void com.itranswarp.learnjava.service.MailService.sendLoginMail(User)
这说明执行业务逻辑前后,确实执行了我们定义的 Aspect(即 LoggingAspect
的方法)。
有些童鞋会问,LoggingAspect
定义的方法,是如何注入到其他 Bean 的呢?
其实 AOP 的原理非常简单。我们以 LoggingAspect.doAccessCheck()
为例,要把它注入到 UserService
的每个 public
方法中,最简单的方法是编写一个子类,并持有原始实例的引用:
public UserServiceAopProxy extends UserService {private UserService target;
private LoggingAspect aspect;
public UserServiceAopProxy(UserService target, LoggingAspect aspect) {this.target = target;
this.aspect = aspect;
}
public User login(String email, String password) {// 先执行 Aspect 的代码:
aspect.doAccessCheck();
// 再执行 UserService 的逻辑:
return target.login(email, password);
}
public User register(String email, String password, String name) {aspect.doAccessCheck();
return target.register(email, password, name);
}
...
}
这些都是 Spring 容器启动时为我们自动创建的注入了 Aspect 的子类,它取代了原始的 UserService
(原始的UserService
实例作为内部变量隐藏在 UserServiceAopProxy
中)。如果我们打印从 Spring 容器获取的 UserService
实例类型,它类似UserService$$EnhancerBySpringCGLIB$$1f44e01c
,实际上是 Spring 使用 CGLIB 动态创建的子类,但对于调用方来说,感觉不到任何区别。
注意
Spring 对接口类型使用 JDK 动态代理,对普通类使用 CGLIB 创建子类。如果一个 Bean 的 class 是 final,Spring 将无法为其创建子类。
可见,虽然 Spring 容器内部实现 AOP 的逻辑比较复杂(需要使用 AspectJ 解析注解,并通过 CGLIB 实现代理类),但我们使用 AOP 非常简单,一共需要三步:
- 定义执行方法,并在方法上通过 AspectJ 的注解告诉 Spring 应该在何处调用此方法;
- 标记
@Component
和@Aspect
; - 在
@Configuration
类上标注@EnableAspectJAutoProxy
。
至于 AspectJ 的注入语法则比较复杂,请参考 Spring 文档。
Spring 也提供其他方法来装配 AOP,但都没有使用 AspectJ 注解的方式来得简洁明了,所以我们不再作介绍。
拦截器类型
顾名思义,拦截器有以下类型:
- @Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了;
- @After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行;
- @AfterReturning:和 @After 不同的是,只有当目标代码正常返回时,才执行拦截器代码;
- @AfterThrowing:和 @After 不同的是,只有当目标代码抛出了异常时,才执行拦截器代码;
- @Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能。
练习
使用 AOP 实现日志。
下载练习
小结
在 Spring 容器中使用 AOP 非常简单,只需要定义执行方法,并用 AspectJ 的注解标注应该在何处触发并执行。
Spring 通过 CGLIB 动态创建子类等方式来实现 AOP 代理模式,大大简化了代码。