AOP
AOP 概述
AOP是Asepect Oriented Programming的简称,核心思想是将分散在各个业务代码中的相同代码通过横向切割的方法抽取到一个独立的模块中。如图所示:
术语
- 连接点 Jointpoint
特定点是程序执行的某个特殊位置,如类开始初始化之前,之后;类的某个方法调用前,后;方法抛出异常后;这些代码中的特定点被称为连接点。Spring仅支持方法的连接点,即仅能在方法调用前,调用后,方法抛出异常时这些程序执行点植入增强。连接点由两个信息确定:一是方法(表示的程序执行点),二是用相对位置表示的方位(前后)。 - 切点 Pointcut
AOP通过切点来定位特定的连接点。切点和连接点可以是一对多的关系,相当于查询关系 - 增强 Advice
增强是植入目标类连接点上的一段程序代码。Spring提供的增强接口都是带方位名的:BeforeAdvice,AfterReturningAdvice,ThrowsAdvice等 - 目标对象 Target
增强逻辑的植入目标类 - 引介 Introduction
引介是一种特殊的增强,它为类添加一些属性和方法。假如某个业务类原本没有实现某个接口,通过AOP引介功能,可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。 - 织入 Weaving
织入是将增强添加到目标类的具体连接点上的过程。根据不同的实现技术,AOP有3种织入方式:
1) 编译期织入,要求使用特殊的Java编译器
2) 类预载期织入,要求使用特殊的类装载器
3) 动态代理织入, 在运行期为目标类添加增强生成子类的方式。
Spring采取动态代理织入,AspectJ采用编译期织入和类装载期织入。 - 代理 Proxy
一个类被AOP织入增强后,产生了一个结果类,踏实融合了原类和增强逻辑的代理类。根据不同的代理方式,代理类即可能是与原类具有相同接口的类,也可能就是原类的子类,所以可以采用与调用原类相同的方式调用代理类。 - 切面 Aspect
切面由切点和增强(或引介)组成。
Spring AOP
Spring AOP 使用动态代理技术,有两种代理机制:一种是基于JDK的动态代理,一种是基于CGLib的动态代理。JDK只提供接口的代理,不支持类的代理。
动态代理
JDK动态代理
主要涉及java.lang.reflect包中的两个类: Proxy和InvocationHandler。InvocationHandler是一个接口,通过该接口定义横切逻辑。 Proxy是利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。
代理的作用是在不改变被代理类的情况下强化被代理类的某个方法,分为静态代理和动态代理。JDK动态代理要求被代理类要有一个接口,这个接口定义了需要被强化的方法。静态代理需要创建一个新的类,这个新的类需要实现和被代理类同样的接口,然后重写需要强化的方法。动态代理不需要知道这个具体的接口和类是什么,利用java反射功能可以实现对任意类创建代理,前提是这个被代理类必须实现了某接口。
实例:创建一个性能监视横切逻辑的例子
需要被植入横切代码的service类
package com.smart.proxy;
public class ForumServiceImpl implements ForumService{
public void removeTopic(int topicId) {
//PerformanceMonitor.begin(...)
System.out.println("模拟删除Topic记录:"+topicId);
try{
Thread.currentThread().sleep(20);
} catch (Exception e) {
throw new RuntimeException(e);
}
//PerformanceMonitor.end();
}
}
实现横切代码逻辑的实现了InvocationHandler接口的类: PerformanceHandler
package com.smart.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
//实现IncocationHandler
public class PerforamnceHandler implements InvocationHandler{
private Object target; //目标业务类
public PerformanceHandler(Object target){
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
PerformanceMonitor.begin(
target.getClass().getName()+"."+method.getName()); //执行方法之前
Object obj = method.invoke(target, args); //通过java反射机制间接调用目标对象的方法(执行该方法)
PerformanceMonitor.end();//执行方法之后
return obj;
}
}
创建代理的实例
package com.smart.proxy;
import java.lang.reflect.Proxy;
import org.testing.annotations.*;
public class ForumServiceTest{
@Test
public void proxy(){
ForumService target = new ForumServiceImpl();
PerformanceHandler handler = new PerformanceHandler(target);
ForumService proxy = (ForumService)
Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass.getInterfaces(),
handler);
proxy.removeTopic(1012);
}
}
CGLib动态代理
使用JDK创建代理有一个限制,即它只能为接口创建代理实例。如果不想为业务方法定义接口,可以使用CGLib动态代理。CGLib采用底层的字节码技术,可以为一个类创建子类,在子类中采取方法拦截的技术拦截所有的父类方法的调用并顺势织入横切逻辑。例子:
CglibProxy:
package com.smart.proxy;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CglibProxy implements MethodInterceptor{
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz){
enahncer.setSuperclass(clazz);//设置需要创建子类的类
enhancer.setCallback(this);
return enhancer.create(); //通过字节码技术动态创建子类实例
}
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) Throws Throwable{
PerformanceMonitor.begin(obj.getClass().getName()+"."+method.getName());
Object result = proxy.invokeSuper(obj, args);
PerformanceMonitor.end();
return result;
}
}
创建代理的实例
package com.smart.proxy;
import java.lang.reflect.Proxy;
import org.testng.annotations.*;
public class ForumServiceTest{
@Test
public void proxy(){
CglibProxy proxy = new CglibProxy();
ForumServiceImpl forumService = (ForumServiceImpl) proxy.getProxy(ForumServiceImpl.class);
forumService.removeTopic(1023);
}
}
CGLib的缺点就是代理类的interceptor方法会替换掉所有的被代理类的方法而不能拦截特定的方法。切因为其采用动态创建子类的方式生成代理对象,所以不能对目标类中的final或private方法进行代理。
JDK动态代理创建的代理对象性能比较差,但CGLib创建代理对象花费的时间比JDK动态代理要多。所以对于singleton的代理对象或具有实例池的代理比较适合采用CGLib,需要频繁创建的对象适合采用JDK。这两种方法是Spring底层实现AOP的方法。
创建增强类
Spring使用增强类定义横切逻辑,同时由于Spring只支持方法连接点,增强还包括在方法的哪一点加入横切代码的方位信息。所以增强既包含横切逻辑,有包含部分连接点的信息。
增强类型
AOP联盟为增强定义了org.aopalliance.aop.Advice
接口,Spring支持5种类型的增强:
按照增强在目标类方法中的连接点位置,可分为5类:
- 前置增强
org.springframework.aop.BeforeAdvice
,表示在目标方法执行之前实施增强 - 后置增强
org.springframework.aop.AfterReturningAdive
表示在目标方法执行后实施增强 - 环绕增强
org.aopalliance.intercept.MethodInterceptor
在目标方法执行前后实施增强 - 异常抛出增强
org.springframework.aop.ThrowsAdvice
在目标方法抛出异常后实施增强 - 引介增强
org.springframework.aop.IntroductionInterceptor
表示在目标类中添加一些新的方法和属性
Spring的ProxyFactory比直接使用CGLib或JDK动态代理省了很多事。当然它其实还是用这两种方法实现的。我们可以手动选择使用哪种方式实现。默认使用CGLib,如果设置了interface就会启用JDK动态代理。
可以通过Spring的配置以很Spring的方式声明一个代理,例子如下:
ProxyFactoryBean是FactoryBean接口的实现类。ProxyFactoryBean
负责为其他Bean创建代理实例,他在内部使用ProxyFactory来完成这项工作。ProxyFactoryBean的几个常用的可配置属性:
- target:被代理的目标对象
- proxy Interfaces:代理需要实现的接口
- interceptorNames: 需要织入目标对象的Bean列表
- singleton:是否是singleton
- optimize: 设置为true时强制使用CGLib
- proxyTargetClass: 是否对类进行代理
切面
Spring通过org.springframework.aop.Pointcut
接口描述切点。Pointcut
由ClassFilter
和MethodMacher
构成,通过ClassFilter
定位到某些特定类上,通过MethodMacher
定位到某些特定方法上。这样Pointcut
类就拥有了描述某鞋类的特定方法的能力。Pointcut类关系图
Spring支持两种方法匹配器:静态方法匹配器和动态方法匹配器。静态方法匹配器仅对方法名签名(包括方法名和入参类型及顺序)进行匹配;动态方法匹配器会在运行期检查方法入参的值。动态匹配每次调用方法都会判断(会匹配每次入参的值),所以对资源消耗很大,不常用。
切点类型
Spring提供6中类型切点:
- 静态方法切点
- 动态方法切点
- 注解切点
- 表达式切点
- 流程切点
- 复合切点
切面类型
Spring使用org.springframework.aop.Advisor
接口表示切面的概念。一个切面同时拥有横切代码和连接点的信息。切面接口分为三类:
- Advisor:代表一般切面,仅包含一个Advice。Advice本身是一个很简单的切面,连接点是目标类的所有方法,一般不会直接使用。
- PoincutAdvisor: 代表具有切点的切面。 包含
Advice
和Poincut
两个类,可以通过类,方法名,方法方位等信息灵活的定义切面的连接点。
ProductAdvisor的主要实现类体系:
ProductAdvisor有六个实现类:
DefaultPointcutAdvisor
: 最常用的切面类型,可以通过任意Pointcut和Advice定义一个切面,唯一不支持的是引介的切面类型,一般可以通过拓展该类实现自定义的切面。NameMatchMethodPointcutAdvisor
: 按方法名定义切点的切面。RegexpMethodPointcutAdvisor
:按正则表达式匹配方法名。StaticMethodMacherPointcutAdvisor
:静态方法匹配器,默认情况下匹配所有的类AspectJExpressionPointcutAdvisor
: 用AspectJ切点表达式定义切点的切面AspectJPointcutAdvisor
: 用于AspectJ语法定义切点的切面
- IntroductionAdvisor: 拥有
ClassFilter
和Advice
, 应用到类层面上。
实例:
普通静态方法匹配切面
package com.smart.advisor;
import java.lang.reflect.Method;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.support.StaticMethodMacherPointcutAdvisor;
import org.springframework.aop.MethodBeforeAdvice;
public class Waitor{
public void greetTo(String name){
System.out.println("Waitor greet to "+ name+"...");
}
public void serveTo(String name){
System.out.println("Waitor serving "+ name + "...");
}
}
public class seller{
public void greetTo(String name){
System.out.println("Seller greet to "+ name+"...");
}
}
public class GreetingAdvisor extends StaticMethodMacherPointcutAdvisor{
public boolean matches(Method method, Class clazz){ //必须定义
return "greetTo".equals(method.getName());
}
public ClassFilter getClassFilter(){ //如果不覆盖这个方法,会匹配所有的类
return new ClassFilter(){
public boolean matches(Class clazz){
return Waitor.class.isAssignableFrom(clazz); //参数类和Waitor是同一个类或是Waitor的子类
}
}
}
}
public class GreetingBeforeAdvice implements MethodBeforeAdvice{
public void before(Method method, Object[] args, object object) throws Throwable{
System.out.println(obj.getClass().getName()+"."+method.hetName());
String clientName = (String)args[0];
System.out.println("How are you? Mr "+ clientName+".");
}
}
总结: 在这个例子中,greetingAdvisor切面中装入了greetingAdvice(横切代码和方位),用编码的方式写了matches(), classFilter, 还可以定义order(用于定义Ordered接口表示的顺序)。waiter和seller在spring中被代理类强化,因为两者有公共的配置信息,所以用了一个父bean简化配置。
测试代码:
String configPath = "com/smart/advisor/beans.xml";
ApplicationContext ctx = new ApplicationContext(configPath);
Waiter waiter = (Waiter) ctx.getBean("waiter");
Seller seller = (Seller) ctx.getBean("Seller");
waiter.greetTo("John");
waiter.serveTo("John");
seller.greetTo("John");
静态正则表达式方法匹配切面
可以在配置文件中定义正则表达式匹配方法,和普通静态方法相比不需要定义matches()来匹配了。可以说正则表达式比普通静态方法要灵活,普通静态方法能做到的正则表达式方法也能轻松做到。
动态切面
使用DefaultPointcutAdvisor
和DynamicMethodMacherPointcut
。DynamicMethodMacherPointcut
为一抽象类,他将isRuntime()标为final并返回true,因此它的子类一定是动态切点。改抽象类默认匹配所有类和方法,需要扩展该类编写符合要求的动态切点。和StaticMethodMacherPointcutAdvisor
相比多了一步动态检查。
流程切面
Spring的流程切面由DefaultPoincutAdvisor
和ControlFlowPointcut
实现。流程切点是指由某个方法发起的直接或间接调用其他方法,这个方法可能代表一个流程,例子:
如果希望所有由WaiterDelegate#service()方法调用的方法都植入增强,需要用流程切面。代码略。流程切面和动态切面一样需要在调用时检查调用堆栈中是否有满足流程切点要求的方法。
复合切点切面
Spring提供的ComposablePointcut
可以把多个切点组合起来。它实现了Pointcut接口,因此也是切点。(更确切的翻译是:可组合的切点)
引介切面
引介切面的类继承关系图:
IntroductionAdvisor
继承了Advisor
和IntroductionInfo
,它仅有一个ClassFilter,只能过滤类而不能针对方法。DefaultIntroductionAdvisor
有三个构造函数:
自动创建代理
BeanPostProcessor
:
基于注解定义切面
@AspectJ 标注类,第三方程序可以通过类是否拥有此注解判断其是否是一个切面
@Before 标识此方法方位是before,在目标类greetTo()方法植入增强,任意返回值类型,任意入参。
如果日后要用AOP,需好好学习AspectJ