在AOP编程中,我们经常会遇到下面的概念:
看完上述术语,是不是感觉对AOP有了进一步的困惑?其实,我们不用关心AOP创造的“术语”,只需要理解AOP本质上只是一种代理模式的实现方式,在Spring的容器中实现AOP特别方便。
我们以UserService
和MailService
为例,这两个属于核心业务逻辑,现在,我们准备给UserService
的每个业务方法执行前添加日志,给MailService
的每个业务方法执行前后添加日志,在Spring中,需要以下步骤:
首先,我们通过Maven引入Spring对AOP的支持:
上述依赖会自动引入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非常简单,一共需要三步:
@Component
和@Aspect
;@Configuration
类上标注@EnableAspectJAutoProxy
。至于AspectJ的注入语法则比较复杂,请参考Spring文档。
Spring也提供其他方法来装配AOP,但都没有使用AspectJ注解的方式来得简洁明了,所以我们不再作介绍。
顾名思义,拦截器有以下类型:
使用AOP实现日志。
在Spring容器中使用AOP非常简单,只需要定义执行方法,并用AspectJ的注解标注应该在何处触发并执行。
Spring通过CGLIB动态创建子类等方式来实现AOP代理模式,大大简化了代码。