Spring AOP
一、AOP概述
1.1 什么是AOP
AOP(Aspect Oriented Programing):面向切面编程
- 按照软件重构的思想,如果多个类中出现相同的代码,则应该考虑定义一个父类,将这些相同的代码提取到父类。比如Horse、Pig、Camel这些对象都有run()和eat()方法,通过引入一个包含这两个方法的抽象的Animal父类,Horse、Pig、Camel就可以通过继承Animal复用run()和eat()方法。这种情况我们称之为纵向抽取。
- 那什么时候会出现面向切面编程的需求?我们看以下的情况,如果给所有的代码都加上性能监视代码,它在方法调用前启动,在方法调用返回前结束,并在内部记录性能监视的结果信息。这种情况下我们无法通过继承父类的纵向抽取方法来减少代码的重复,因为性能监视代码是在方法中实现,依赖于这个方法,这种情况下就出现了AOP,把分散在各个业务逻辑代码中相同的代码抽取成一个独立的模块,让业务逻辑代码专注于业务的实现,不用考虑其他的。
- 抽取出来简单,难点就是如何将这些独立的逻辑融合到业务逻辑中,完成跟原来一样的业务逻辑,这就是AOP解决的主要问题。
没有使用AOP的论坛管理业务类的代码:
public class ForumService{
private TransactionManager transManger;
private PerformanceMonitor pmoitor;
private TopicDao topicDao;
private ForumDao forumDao;
public void removeTopic(int topicId){
pmoitor.start();
transManger.beginTransaction;
topicDao.removeTopic(topicId);
transManger.commit();
pmoitor.end();
}
public void createForum(Forum forum){
pmoitor.start();
transManger.beginTransaction;
forumDao.create(forum);
transManger.commit();
pmoitor.end();
}
}
1.2 AOP术语
- 连接点(Joinpoint)
特定点是程序执行的某个特定的位置,如类开始初始化前、类初始化后、类的某个方法调用前/调用后、方法抛出异常后,一个类或一段代码拥有一些具有边界性质的特定点,这些代码中的特定点被称为“连接点”。 - 切点(Pointcut)
每个程序类都拥有多个连接点,如一个拥有两个方法的类,这两个方法都是连接点,即连接点是程序类中客观存在的事务。在为数众多的连接点中,AOP通过“切点”定位特定的连接点。连接点相当于数据库中的记录,而切点相当于查询条件。切点和连接点不是一对一的关系,一个切点可以匹配多个连接点。
在Spring中,切点通过org.springframewprk.aop.Pointcut
接口进行描述,它使用类和方法作为连接点的查询条件,Spring AOP的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。 - 增强(Advice)
增强是织入到目标类连接点上的一段程序代码。在Spring 中,增强除用于描述一段程序代码外,还拥有另一个和连接点相关的信息,这便是执行点的方位。Spring所提供的增强接口都是带方位名的,如BeforeAdvice
、AfterReturningAdvice
、ThrowsAdvice
等。BeforeAdvice
表示方法调用前的位置,而AfterReturningAdvice
表示访问返回后的位置。所以只有切点结合增强,才能确定特定的连接点并实施增强逻辑。 - 目标对象(Target)
增强逻辑的织入目标类,即需要被加强的业务对象。如果没有AOP,那么目标业务类需要自己实现所有的逻辑。有了AOP,只需要实现非横切逻辑的程序逻辑。 - 引介(Introduction)
引介是一种特殊的增强,它为类添加一些属性和方法,这样即使一个业务类原本没有实现某个接口,通过AOP的引介功能,也可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。 - 织入(Weaving)
织入就是将增强添加到对目标类具体连接点上的过程。织入是一个形象的说法,具体来说,就是生成代理对象并将切面内容融入到业务流程的过程。
根据不同的实现技术,AOP有3种织入方式:- 编译器织入,这要求使用特殊的Java编译器;
- 类装载期织入,这要求使用特殊的类装载器
- 动态代理织入,在运行期间为目标类添加增强生成子类的方式
Spring采用动态代理织入,而AspectJ采用编译器织入和类装载期织入。
- 代理(Proxy)
一个类被AOP增强之后,就产生了一个结果类,它是融合了原类和增强逻辑的代理类。根据不同的代理方式,代理类既可以是和原类具有相同接口的类,也可能就是原类的子类,所以可以采用与调用原类相同的方式调用代理类。 - 切面(Aspect)
切面由切点和增强组成,它既包括横切逻辑的定义,也包括连接点的定义。Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入切面所指定的连接点中。
AOP的工作重心在于如何将增强应用于目标对象的连接点上。这里包含两项工作:
- 如何通过切点和增强定位到连接点上
- 如何在增强中编写切面的代码。
二、动态代理
2.1 JDK动态代理
JDK动态代理技术主要涉及java.lang.reflect包中的两个类:Proxy
和InvocationHandler
,其中,InvocationHandler
是一个接口,可以通过实现该接口定义横切类逻辑,并通过反射机制调用目标类的代码,动态地将横切逻辑和业务逻辑编织在一起。
而Proxy
利用InvocationHandler
动态创建一个符合某一接口地实例,生成目标类的代理对象。
- 我们移除
ForumService
中的性能检测的横切代码,使得ForumService
只负责具体的业务逻辑。
public class ForumService{
public void removeTopic(int topicId){
System.out.println("模拟删除Topic记录:" + topicId);
try {
Thread.sleep(40);
}catch (Exception e){
throw new RuntimeException(e);
}
}
public void createForum(int forumId){
System.out.println("模拟删除forum记录:" + forumId);
try {
Thread.sleep(40);
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
- 我们把性能横切代码安置在
PerformanceHandler
中
2.2 CGLib动态代理
使用JDK创建代理有一个限制,即它只能为接口创建代理实例,这一点可以从Proxy的接口方法newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)
中可以知道。第二个入参interfaces就是需要代理实例实现的接口列表。
那么对于没有通过接口定义业务方法的类,如何动态创建代理实例?
可以使用CGLib动态代理。
CGLib采用底层的字节码技术,可以为一个类创建子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑。
由于CGLib采用动态创建子类的方式生成代理对象,所以不能对目标中的final或private方法进行代理。
public class ForumService {
public void removeTopic(int topicId){
System.out.println("模拟删除Topic记录:" + topicId);
try {
Thread.sleep(40);
}catch (Exception e){
throw new RuntimeException(e);
}
}
public void createForum(int forumId){
System.out.println("模拟删除forum记录:" + forumId);
try {
Thread.sleep(40);
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
public class CglibProxy implements MethodInterceptor {
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz){
//设置需要创建的类
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
//通过字节码技术动态创建子类实例
return enhancer.create();
}
//拦截父类所有方法的调用
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
PerformanceMonitor.begin(o.getClass().getName()+"."+method.getName());
//通过代理类调用父类中的方法
Object result = methodProxy.invokeSuper(o,objects);
PerformanceMonitor.end();
return result;
}
}
public class ForumServiceTest {
public static void main(String[] args) {
CglibProxy proxy = new CglibProxy();
ForumService forumService = (ForumService)proxy.getProxy(ForumService.class);
forumService.removeTopic(1);
forumService.createForum(10);
}
}
运行结果
proxy.ForumService$ $ EnhancerByCGLIB $ $ d2cd2660.removeTopic
模拟删除Topic记录:1
结束
proxy.ForumService $ $ EnhancerByCGLIB $ $ d2cd2660.createForum
模拟删除forum记录:10
结束
2.3 动态代理总结
通过PerformanceHandler
或CglibProxy
实现了性能监视横切逻辑的动态织入,但是这种实现方式存在3个明显需要改进的地方。
- 目标类的所有方法都添加了性能监视横切逻辑,而有时候这并不是我们所期望的,我们可能只是希望对业务类中的某些特定方法添加横切逻辑。
- 通过硬编码的方式指定了织入横切逻辑的织入点,即在目标类业务方法的开始和结束前织入代码。
- 手工编写代理实例的创建过程,在为不同类创建代理时,需要分别编写相应的创建代码,无法做到通用。