AOP是有特定的应用场合的,它只适合那些具有横切逻辑的应用场合,如性能检测、访问控制、事务管理及日志记录
(1)连接点(Joinpoint):由两个信息确定:一是用方法表示的程序执行点;二是用相对位置表示的方位。如在Test.foo()方法执行前的连接点,执行点为Test.foo(),方位为该方法执行前的位置。Spring使用切点对执行点进行定位,而方位则在增强类型中定义。
(2)切点(Pointcut):在Spring中,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件,Spring AOP的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。确切地说,应该是执行点而非连接点,因为连接点是方法执行前、执行后等包括方位信息的具体程序执行点,而切点只定位到某个方法上,所以如果希望定位到具体的连接点上,还需要提供方位信息。
(3)增强(Advice):是织入目标类连接点上的一段程序代码。在Spring中,增强除用于描述一段程序代码外,还拥有另一个和连接点相关的信息,这便是执行点的方位。结合执行点的方位信息和切点信息,就可以找到特定的连接。
(4)目标对象(Target):增强逻辑的织入目标类。如果没有AOP,那么目标业务类需要自己实现所有的逻辑。
(5)引介(Introduction):引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过AOP的引介功能,也可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
(6)织入(Weaving):是将增强添加到目标类的具体连接点上的过程。AOP有3种织入方式:编译期织入、类装载期织入、动态代理织入,Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
(7)代理(Proxy):一个类被AOP织入增强后,就产生了一个结果类,它是融合了原类和增强逻辑的代理类。
(8)切面(Aspect):切面由切点和增强(引介)组成,它既包括横切逻辑的定义,也包括连接点的定义。Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入切面所指定的连接点中。
Spring AOP使用了两种代理机制:一种是基于JDK的动态代理;另一种是基于CGLib的动态代理。
JDK的动态代理主要涉及java.lang.reflect包中的两个类:Proxy和InvocationHandler。其中,InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态地将横切逻辑和业务逻辑编织在一起。而Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。
ForumServiceImpl是业务逻辑类,如下代码:
public class ForumServiceImpl implements ForumService {
public void removeTopic(int topicId) {
System.out.println("模拟删除Topic记录:"+topicId);
try {
Thread.currentThread().sleep(20);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void removeForum(int forumId) {
System.out.println("模拟删除Forum记录:"+forumId);
try {
Thread.currentThread().sleep(40);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
在PerformanceMonitor中实现类横切逻辑,如下代码:
public class PerformanceMonitor {
private static ThreadLocal performaceRecord = new ThreadLocal();
public static void begin(String method) {
System.out.println("begin monitor...");
MethodPerformace mp = performaceRecord.get();
if(mp == null){
mp = new MethodPerformace(method);
performaceRecord.set(mp);
}else{
mp.reset(method);
}
}
public static void end() {
System.out.println("end monitor...");
MethodPerformace mp = performaceRecord.get();
mp.printPerformace();
}
}
PerformanceHandler实现了InvocationHandler接口,该接口定义了一个invoke(Object proxy,Method method,Object[] args)方法,其中,proxy是最终生成的代理实例,method是被代理目标实例的某个具体方法,通过它可以发起目标实例方法的反射调用;args是被代理实例某个方法的入参,在方法反射调用时使用。如下代码所示,PerformanceHandler类将横切逻辑代码和业务逻辑代码编织在一起。
public class PerformanceHandler implements InvocationHandler {
private Object target;
public PerformaceHandler(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);
PerformanceMonitor.end();
return obj;
}
}
下面通过Proxy结合PerformanceHandler创建ForumService接口的代理实例。
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.removeForum(10);
proxy.removeTopic(1012);
}
使用JDK创建代理有一个限制,即它只能为接口创建代理实例,这一点从Proxy的方法newProxyInstance(ClassLoader loader,Class[] interfaces,InvacationHandler h)中可以看出,第二个参数interfaces就是需要代理实例实现的接口列表。
CGLib采用底层的字节码技术,可以为一个类创建子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑。下面是用CGLib技术编写的可以为任何类创建织入横切逻辑代理对象的代理创建器。
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 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;
}
}
下面通过CglibProxy为ForumServiceImpl类创建代理对象,并测试代理对象的方法
public void proxy(){
CglibProxy proxy = new CglibProxy();
ForumServiceImpl forumService = (ForumServiceImpl)proxy.getProxy(ForumServiceImpl.class);
forumService.removeForum(10);
forumService.removeTopic(1023);
}
由于CGlib采用动态创建子类的方式生成代理对象,所以不能对目标类中的final或private方法进行代理。
CGLib所创建的动态代理对象的性能比JDK所创建的动态代理的对象的性能高,但CGLib在创建代理对象时所花费的时间比JDK动态代理多。对于Singleton的代理对象或者具有实例池的代理,因为无须频繁地创建代理对象,所以比较适合采用CGLib动态代理技术;反之则采用JDK动态代理技术。
按增强在目标类方法中的连接点位置,可分为:
(1)前置增强:org.springframework.aop.BeforeAdvice代表前置增强,表示在目标方法执行前实施增强。因为Spring只支持方法级的增强,所以MethodBeforeAdvice是目前可用的前置增强。
(2)后置增强:org.springframework.aop.AfterReturningAdvice代表后置增强,表示在目标方法执行后实施增强。
(3)环绕增强:org.aopalliance.intercept.MethodInterceptor代表环绕增强,表示在目标方法执行前后实施增强。
(4)异常抛出增强:org.springframework.aop.ThrowsAdvice代表抛出异常增强,表示在目标方法抛出异常后实施增强。
(5)引介增强:org.springframework.aop.IntroductionInterceptor代表引介增强,表示在目标类中添加一些新的方法和属性。
如下是业务逻辑类:
public class NativeWaiter implements Waiter{
public void greetTo(String name){
System.out.println("greet to "+name+"...");
}
public void serveTo(String name){
System.out.println("serving "+name+"...");
}
}
GreetingBeforeAdvice实现了MethodBeforeAdvice接口的before(Method method,Object[] args,Object obj)throws Throwable方法,实现横切逻辑,如下代码:
public class GreetingBeforeAdvice implements MethodBeforeAdvice {
public void before(Method method, Object[] args, Object obj) throws Throwable {
String clientName = (String)args[0];
System.out.println(obj.getClass().getName()+"."+method.getName());
System.out.println("How are you!Mr."+clientName+".");
}
}
使用ProxyFactory将业务逻辑和横切逻辑编织在一起,如下代码:
@Test
public void before() {
Waiter target = new NaiveWaiter();
BeforeAdvice advice = new GreetingBeforeAdvice();
ProxyFactory pf = new ProxyFactory();
pf.setInterfaces(target.getClass().getInterfaces());
pf.setOptimize(true);
pf.setTarget(target);
pf.addAdvice(advice);
Waiter proxy = (Waiter)pf.getProxy();
proxy.greetTo("John");
proxy.serveTo("Tom");
}
ProxyFactory内部使用JDK或CGLib动态代理技术将增强应用到目标类中。通过ProxyFactory的setInterfaces(Class[] interfaces)方法指定目标接口进行代理,则ProxyFactory使用JDK动态代理技术,如果是针对类的代理,则使用CGLib动态代理技术。通过ProxyFactory的setOptimize(true)方法让ProxyFactory启动优化代理方式,这样针对接口的代理也会使用CGLib。
在Spring中配置增强:
ProxyFactoryBean是FactoryBean接口的实现类,负责为其他Bean创建代理实例,它在内部使用ProxyFactory来完成这项工作。ProxyFactoryBean中的可配置属性如下:
target:代理的目标对象
proxyInterfaces:代理所要实现的接口,可以是多个接口。该属性还有一个别名属性interfaces。
interceptorNames:需要织入目标对象的Bean列表,采用Bean的名称指定。这些Bean必须是实现了org.aopalliance.intercept.MethodInterceptor或org.springframeword.aop.Advisor的Bean,配置中的顺序对应调用的顺序。
singleton:返回的代理是否是单实例,默认为单实例。
optimize:当设置为true时,强制使用CGLib动态代理。
proxyTargetClass:是否对类进行代理,设置为true时,使用CGLib动态代理。
通过实现AfterReturningAdvice来定义后置增强的逻辑,AfterReturningAdvice接口也仅定义了唯一的方法afterReturning(Object returnObj,Method method,Obeject[] args,Object obj)throws Throwable。其中,returnObj为目标实例方法返回的结果;method为目标类的方法;args为目标实例方法的入参;而obj为目标类实例。如果在后置增强中抛出异常,如果该异常是目标方法声明的异常,则异常归并到目标方法中;如果不是目标方法所声明的异常,则Spring将其转为运行期异常抛出。
Spring使用MethodInterceptor作为环绕增强的接口。该接口拥有唯一的接口方法Object invoke(MethodInvocation invocation)throws Throwable。MethodInvocation 不但封装了目标方法及其入参数组,还封装了目标方法所在的实例对象,通过MethodInvocation 的getArguments()方法可以获取目标方法的入参数组,通过proceed()方法反射调用目标实例相应的方法。
ThrowsAdvice异常抛出增强接口没有定义的任何方法,它是一个标签接口,在运行期Spring使用反射机制自行判断,必须采用一些签名形式定义异常抛出的增强方法:
void afterThrowing(Method method,Object[] args,Object target,Throwable),方法名必须为afterThrowing,方法入参规定如下:前3个入参是可选的(要么都提供,要么都不提供),而最后一个入参是Throwable或其子类。
目标方法抛出异常后,优先选取拥有异常入参和抛出的异常相似度最高的afterThrowing()方法。
public interface Monitorable {
void setMonitorActive(boolean active);
}
下面通过扩展DelegatingIntroductionInterceptor为目标类引入性能监视的可控功能
public class ControllablePerformaceMonitor extends DelegatingIntroductionInterceptor implements Monitorable, Testable {
private ThreadLocal MonitorStatusMap = new ThreadLocal();
public void setMonitorActive(boolean active) {
MonitorStatusMap.set(active);
}
public Object invoke(MethodInvocation mi) throws Throwable {
Object obj = null;
if (MonitorStatusMap.get() != null && MonitorStatusMap.get()) {
PerformanceMonitor.begin(mi.getClass().getName() + "."+ mi.getMethod().getName());
obj = super.invoke(mi);
PerformanceMonitor.end();
} else {
obj = super.invoke(mi);
}
return obj;
}
}
下面通过Spring的配置,将这个引介增强织入业务类ForumService中
引介增强的配置与一般配置的区别:
一是需要指定引介增强所实现的接口,如上图(1)所示
二是由于只能通过为目标类创建子类的方式生成引介增强的代理,所以必须将proxyTargetClass设置为true。
public class Waiter {
private Waiter waiter;
public void serveTo(String name){
System.out.println("waiter serving "+name+"...");
//waiter.greetTo(name);
}
public void greetTo(String name) {
System.out.println("waiter greet to "+name+"...");
}
}
public class Seller {
public void greetTo(String name) {
System.out.println("seller greet to "+name+"...");
}
}
下面是切面类的代码,StaticMethodMatcherPointcutAdvisor默认可以匹配所有类,这里覆盖了getClassFilter方法,让它仅匹配Waiter类及其子类
public class GreetingAdvisor extends StaticMethodMatcherPointcutAdvisor {
public boolean matches(Method method, Class clazz) {
return "greetTo".equals(method.getName());
}
public ClassFilter getClassFilter(){
return new ClassFilter(){
public boolean matches(Class clazz){
return Waiter.class.isAssignableFrom(clazz);
}
};
}
}
Advisor还需要一个增强类,下面是一个前置增强:
public class GreetingBeforeAdvice implements MethodBeforeAdvice {
public void before(Method method, Object[] args, Object obj) throws Throwable {
String clientName = (String)args[0];
System.out.println(obj.getClass().getName()+"."+method.getName());
System.out.println("How are you!Mr."+clientName+".");
}
}
最后使用Spring配置来定义切面:
pattern:如果只有一个匹配模式串,则可以使用该属性进行配置。patterns属性用于定义多个匹配模式串,这些匹配模式串之间是或的关系
order:切面在织入时对应的顺序。
在定义动态切点时,要同时覆盖getClassFilter()和matches(Method method,Class clazz)方法,通过静态切点检查排除大部分方法。
Spring的流程切面由DefaultPointAdvisor和ControlFlowPointcut实现。流程切点代表由某个方法直接或间接发起调用的其他方法。下面的例子实现所有由WaiterDelegate#service()方法发起调用的其他方法都织入增强。
public class WaiterDelegate {
private Waiter waiter;
public void service(String clientName) {
waiter.greetTo(clientName);
waiter.serveTo(clientName);
}
public void setWaiter(Waiter waiter) {
this.waiter = waiter;
}
}
下面使用DefaultPointAdvisor配置一个流程切面:
ControlFlowPointcut有两个构造函数,分别是ControlFlowPointcut(Class clazz)和ControlFlowPointcut(Class clazz,String methodName)。第一个构造函数指定一个类作为流程切点;第二个构造函数指定一个类和某一个方法作为流程切点。