对方法进行性能监控,在方法调用时统计出方法执行时间。
原始做法:在内个方法的开头获取系统时间,然后在方法的结尾获取时间,最后把前后台两次分别获取的系统时间做一个减法,即可获取方法执行所消耗的总时间。
项目中大量的方法,如果对每个方法开头结尾都加上这些代码,工作量会很大。现在不用修改现有代码,在另一个地方做性能监控,AOP(Aspect Oriented Programming,面向方面编程)就是我们寻找的解决方案。
在AOP中,我们需要定义一个Aspect(切面)类来编写需要横切业务逻辑的代码,也就是性能监控代码。此外,我们需要通过一个条件来匹配想要拦截的类,这个条件在AOP中称为Pointcut(切点)。
案例思路,统计出执行每个Controller类的各个方法所消耗的时间。每个Controller类都有Controller注解,也就是说,我们只需要拦截所有带有Controller注解的类就行了,切点很容易就能确定下来,剩下的就是做一个切面了。
代理技术
代理,或称为Proxy,意思就是你不用去做,别人替你去处理。比如说:赚钱方面,我就是我老婆的Proxy;带小孩方面,我老婆就是我的Proxy;家务事方面,没有Proxy。
它在程序中开发起到了非常重要的作用,比如AOP,就是针对代理的一种应用。此外,在设计模式中,还有一个“代理模式”,在公司要上网,要在浏览器中设置一个Http代理。
Hello World例子
//接口 public interface Hello{ void say(String name); } //实现类 public class HelloImpl implements Hello{ @Override public void say(String name){ System.out.println("Hello!"+name); } }
如果要在println方法前面和后面分别需要处理一些逻辑,怎么做呢?把这些逻辑写死在say方法里面吗?这么做肯定不够优雅,“菜鸟”一般这样干,作为一名资深的程序员,我们坚决不能这么做!
我们要用代理模式,写一个HelloProxy类,让它去调用HelloImpl的say方法,在调用的前后分别进行逻辑处理。
public class HelloProxy implements Hello{ private Hello hello; private HelloProxy(){ hello=new HelloImpl(); } private void say(String name){ before(); hello.say(name); after(); } private void before(){ System.out.println("Before"); } private void after(){ System.out.println("After"); } }
用HelloProxy类实现了Hello接口(和HelloImpl实现相同的接口),并且在构造方法中new出一个HelloImpl类的实例。这样一来,我们就可以在HelloProxy的say方法里面去调用HelloImpl的say方法了。更重要的是,我们还可以在调用的前后分别加上before和after两个方法,在这两个方法里去实现那些前后逻辑。
main方法测试
public static void main(String[] args){ Hello helloProxy = new HelloProxy(); helloProxy.say("Jack"); } //打印结果 Before Hello! Jack After
JDK动态代理
于是疯狂使用代理模式,项目中到处都是XXXProxy的身影,直到有一天,架构师看到了我的代码,他惊呆了,对我说“你怎么这么喜欢静态代理呢?你就不会用动态代理吗?全部重构!”
研究了一下,原来一直用的是静态代理(上面的例子),到处都是XXXProxy类。一定要将这些垃圾Proxy都重构为“动态代理”。
/** * 动态代理 */ public class DynamicProxy implements InvocationHandler { private Object target; public DynamicProxy(Object target) { this.target = target; } /* * 用时代理 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object result method.invoke(target, args); after();
return result;
} } //运行 public static void main(String[] args) { Hello hello = new HelloImpl();//用时代理 DynamicProxy dynamicProxy = new DynamicProxy(hello);
Hello helloProxy = (Hello)Proxy.newProxyInstance(
hello.getClass().getClassLoader(),
hello.getClass().getInterfaces(),
dynamicProxy
); //运行run方法 helloProxy.say("Jack"); }
在这个例子中,DynamicProxy定义了一个Object类型的Object变量,它就是被代理的目标对象,通过构造函数来初始化(“注入”,构造方法初始化叫“正着射”,所以反射初始化叫“反着射”,简称“反射”)。
通过DynamicProxy类去包装Car实例,然后再调用JDK给我们的提供的Proxy类的工厂方法newProxyInstance去动态的创建一个Hello接口的代理类,最后调用这个代理类的run方法。
Proxy.newProxyInstance方法的参数
参数1:ClassLoader
参数2:该实现类的所有接口
参数3:动态代理对象
调用完了用强制类型转换下
这一块想办法封装一下,避免再次出现到处都是Proxy.newProxyInstance方法的情况。于是将这个DynamicProxy重构一下:
public class DynamicProxy implements InvocationHandler{
private Object target; public DynamicProxy(Object target) { this.target = target; }
@SupressWarnings("unchecked") public <T> T getProxyInstance() { return (T)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object result = method.invoke(target, args); after(); return result; } //请求前的操作 public void before(){ //预处理 System.out.println(target.getClass()+"被动态代理了,在它执行之前要执行动态代理加入的预处理方法"); } //请求后的操作 public void after(){ //善后处理 System.out.println(target.getClass()+"被动态代理了,在它执行之后要执行动态代理加入的善后方法"); } } public class DynamicProxyDemo { public static void main(String[] args) {
DynamicProxy dynamicProxy = new DynamicProxy(new HelloImpl());
Hello helloProxy = dynamicProxy.getProxy();
helloProxy.say("Jack"); } }
在DynamicProxy里添加了一个getProxy方法,无需传入任何参数,将刚才所说的那块代码放在这个方法中,并且该方法返回一个泛型类型,就不会强制转换类型了。方法头上@SupressWarnings(“unchecked”)注解表示忽略编译时的警告(因为Proxy.newProxyInstance方法返回的是一个Object,这里强制转换为T了,这是向下转型,IDE中就会有警告,编译时也会出现提示)。
调用时就简单了,用2行代理去掉了前面的7行代码(省了5行)。
CGlib动态代理
用了DynamicProxy以后,好处是接口变了,这个动态代理类不用动。而静态代理就不一样了,接口变了,实现类还要动,代理类也要动。但是动态代理并不是万能的,它也有搞不定的时候,比如要代理一个没有任何接口的类,它就没有用武之地了。
CGlib是一个能代理没有接口的类,虽然看起来不起眼,但Spring、Hibernate这样高端的开源框架都用到了它,它是一个在运行期间动态生成字节码的工具,也就是动态生成代理类了。
public class CGLibProxy implements MethodInterceptor{ public <T> T getProxy(Class<T> cls){ return (T) Enhancer.create(cls,this); } public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy) throws Throwable{ before(); Object result = proxy.invokeSuper(obj,args); after(); return result; } ...... }
需要实现CGLib给我们提供的MethodInterceptor实现类,并填充intercept方法。方法中最后一个MethodProxy类型的参数proxy值得注意。CGLib给我们提供的是方法级别的代理,也可以理解为堆方法拦截(也就是“方法拦截器”)。这个功能对于我们程序员来说,如同雪中送炭。我们直接调用proxy的invokeSuper方法,将被代理的对象obj以及方法参数args传入其中即可。
与DynamicProxy类似,在CGLibProxy中也添加了一个泛型的getProxy方法,便于我们可以快速地获取自动生成的代理对象。
public static void main(){ CGLibProxy cgLibProxy = new CGLibProxy(); Hello helloProxy = cgLibProxy.getProxy(HelloImpl.class); helloProxy.say(Jack); }
仍然通过2行代码就可以返回代理对象,与JDK动态代理不同的是,这里不需要任何的接口信息,对谁都可以生成动态代理对象。
用2行代码返回代理对象还是有些多余的,不想总是去new这个CGLibProxy对象,最好new一次,以后随时拿随时用,于是想到了“单例模式”:
public class CGLibProxy implements MethodInterceptor{ private static CGLibProxy instance = new CGLibProxy(); private CGLibProxy(){ } private static CGLibProxy getInstance(){ return instance; }
...
getProxy...
intercept... }
加上以上几行代码问题就解决了。需要说明的是,这里有一个private的构造方法,就是为了限制外界不能再去new它了,换句话说,这个类被阉割了。
public static void main(String[] args){ Hello helloProxy = CGLibProxy.getInstance().getProxy(HelloImpl.class); helloProxy.say("Jack"); }
这里只需要一行代码就可以获取代理对象了.
AOP技术
什么是AOP
AOP(Aspect-Oriented Programming),名字与OOP仅仅差一个字母,其实它是对OOP编程方式的一种补充,并非是取而代之。翻译过来就是“面向切面编程”或“面向方面编程”。最重要的工作就是写这个“切面”,那么什么事“切面”呢?
切面是AOP中的一个术语,表示从业务逻辑中分离出来的横切逻辑,比如性能监控、日志记录、权限控制等,这些功能都可以从业务逻辑代码中抽离出去。也就是说,通过AOP可以解决代码耦合问题,让职责更加单一。
需要澄清的是,其实很早以前就出现了AOP这个概念。最知名强大的Java开源项目就是AspectJ了。它的前身是AspectWerkz(AOP真正的的老祖宗)。Rod Johnson写了一个Spring框架,称为Spring之父。他在Spring的IOC框架基础上又实现了一套AOP框架,后来掉进了深渊,在无法自拔的时候*使用了AspectJ。所以我们现在用的最多的就是Spring+AspectJ这种AOP框架了。
写死代码
public interface Greeting { void sayHello(String name); } public class GreetingImpl implements Greeting { @Override public void sayHello(String name) { before(); System.out.println("Hello! "+name); after(); } private void before(){ System.out.println("Before"); } private void after(){ System.out.println("After"); } }
before与after方法写死在sayHello方法体中了,这样的代码非常不好。如果我们要统计每一个方法的执行时间,以对性能进行评估,那是不是每个方法的一头一尾都做点手脚呢?
再比如我们要写一个JDBC程序,那是不是也要在方法的开头去连接数据库,方法的末尾去关闭数据库连接呢?
这样写代码只会把程序员累死,把架构师气死!
一定要想办法对上面的代码进行重构,首先给出三个解决方案:
静态代理
JDK动态代理
CGLib动态代理
静态代理
最简单的解决方案就是使用静态代理模式了,我们单独为GreetingImpl这个类写一个代理类:
public class GreetingProxy implements Greeting { private GreetingImpl greetingImpl; public GreetingProxy(GreetingImpl greetingImpl) { this.greetingImpl = greetingImpl; } @Override public void sayHello(String name) { before(); System.out.println("Hello! "+name); after(); } private void before(){ System.out.println("Before"); } private void after(){ System.out.println("After"); } }
就用这个GreetingProxy去代理GreetingImpl,看看客户端如何来调用:
public class Client { public static void main(String[] args) { Greeting greetingProxy = new GreetingProxy(new GreetingImpl()); greetingProxy.sayHello("Jack"); } }
这个写的没错,但是有个问题。XxxProxy这样的类会越来越多(这里构造函数参数为GreetingImpl,所以换一个子类就要再次写一个代理类,如果构造函数参数改为接口Greet,这样再使用Greet的子类时可以使用这个类,当换一个接口时又要去写一个代理类),如何才能将这些代理类尽可能减少呢?最好只有一个代理类。
这时我们需要使用JDK的动态代理了。
JDK动态代理
public class JDKDynamicProxy implements InvocationHandler { private Object target; public JDKDynamicProxy(Object target) { this.target = target; } @SuppressWarnings("unchecked") public <T> T getProxy(){ return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object result = method.invoke(target,args); after(); return result; } private void before(){ System.out.println("Before"); } private void after(){ System.out.println("After"); } }
这样所有的代理类都合并到动态代理类中了,但这样做仍然存在一个问题:JDK给我们提供的动态代理只能代理接口,而不能代理没有接口的类。
public class Client { public static void main(String[] args) { //静态代理 /*Greeting greetingProxy = new GreetingProxy(new GreetingImpl()); greetingProxy.sayHello("Jack");*/ //JDK动态代理 Greeting greeting = new JDKDynamicProxy(new GreetingImpl()) .getProxy(); greeting.sayHello("Jack"); } }
CGLib动态代理
我们使用开源的CGLib类库可以代理没有接口的类,这样就弥补了JDK的不足。CGLib动态代理类是这样的:
public class CGLibDynamicProxy implements MethodInterceptor{ //单例模式 private static CGLibDynamicProxy instance = new CGLibDynamicProxy(); //私有化构造函数,防止new private CGLibDynamicProxy(){} //提供给外界获取单一实例的方法 public static CGLibDynamicProxy getInstance(){ return instance; } @SuppressWarnings("unchecked") public <T> T getProxy(Class<T> cls){ return (T) Enhancer.create(cls,this); } @Override public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { before(); Object result = methodProxy.invokeSuper(target,args); after(); return result; } private void before(){ System.out.println("Before"); } private void after(){ System.out.println("After"); } }
注意这里的坐标
<!--不能超过3.0版本,这里用2.2--> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2</version> </dependency> <!--CGLib依赖此包--> <dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> <version>6.2</version> </dependency>
到此为止,能做的都做了,问题似乎全部都解决了。但事情总不会那么完美,而我们一定要追求完美。
Spring AOP
Rod Johnson搞出了一个AOP框架,Spring AOP:前置增强、后置增强、环绕增强(编程式)
上面例子中提到的before方法,在Spring AOP里就叫Before Advice(前置增强)。有些人将Advice直译为“通知”,这里是不太合适的,因为它没有“通知”的含义,而是对原有代码功能的一种“增强”。再者,CGLib中也有一个Enhancer类,它就是一个增强类。
此外,像after这样的方法就叫After Advice(后置增强),因为它放在后面来增强代码的功能。
如果能把before与after结合在一起,那就叫Around Advice(环绕增强),就像汉堡一样。
前置增强类代码(这个类实现了org.spring.framework.aop.MethodBeforeAdvice):
import org.springframework.aop.MethodBeforeAdvice; public class GreetingBeforeAdvice implements MethodBeforeAdvice { @Override public void before(Method method, Object[] objects, Object o) throws Throwable { System.out.println("Before"); } }
后置增强类:
import org.springframework.aop.AfterReturningAdvice; public class GreetingAfterAdvice implements AfterReturningAdvice { @Override public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable { System.out.println("After"); } }
类似的这里实现了org.springframework.aop.afterReturningAdvice接口。
调用
public class Client { public static void main(String[] args) { ProxyFactory proxyFactory = new ProxyFactory(); //创建代理工厂 proxyFactory.setTarget(new GreetingImpl()); //摄入目标类对象 proxyFactory.addAdvice(new GreetingBeforeAdvice()); //添加前置增强 proxyFactory.addAdvice(new GreetingAfterAdvice()); //添加后置增强 Greeting greeting = (Greeting) proxyFactory.getProxy(); greeting.sayHello("Jack"); } }
当然我们完全可以用一个增强类,让它同时实现MethodBeforeAdvice和AfterReturningAdvice这两个接口,代码:
public class GreetingBeforeAndAfterAdvice implements MethodBeforeAdvice,AfterReturningAdvice { @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { System.out.println("before"); } @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("after"); } }
这样我们只需要使用一行代码,就可以同时添加前置与后置增强
proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice());
刚才有提到过“环绕增强”,其实它可以把“前置增强”与“后置增强”的功能合并起来,无须让我们同时实现两个接口。
import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import java.lang.reflect.Method; public class GreetingAroundAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { before(); Object result = methodInvocation.proceed(); after(); return result; } private void before(){ System.out.println("Before"); } private void after(){ System.out.println("After"); } }
环绕增强需要实现org.aopalliance.intercept.MethodInterceptor接口。注意,这个接口不是Spring提供的,它是AOP联盟(一个很高大上的技术联盟)写的,Spring只是借用了它,在客户端汇总同样也需要将该增强类的对象添加到代理工厂中。
public class Client { public static void main(String[] args) { ProxyFactory proxyFactory = new ProxyFactory(); //创建代理工厂 proxyFactory.setTarget(new GreetingImpl()); //摄入目标类对象 //proxyFactory.addAdvice(new GreetingBeforeAdvice()); //添加前置增强 //proxyFactory.addAdvice(new GreetingAfterAdvice()); //添加后置增强 //proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice()); //实现两个接口 proxyFactory.addAdvice(new GreetingAroundAdvice()); //实现一个Around环绕式接口 Greeting greeting = (Greeting) proxyFactory.getProxy(); greeting.sayHello("Jack"); } }
以上就是SpringAOP的基本用法,单这只是“编程式”而已。Spring AOP如果只是这样,那就太弱了,它曾经也一度宣传用Spring配置文件的方式来定义Bean对象,把代码中的new操作全部解脱出来。
SpringAOP:前置增强、后置增强、环绕增强(声明式)
Spring配置文件篇日志
<!--扫描指定包(将带有Component注解的类自动定义为SpringBean)--> <context:component-scan base-package="com.smart4j.framework"/> <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="interfaces" value="com.smart4j.framework.Greeting"/> <!--需要代理的接口--> <property name="target" ref="greetingImpl"/> <!--实现接口类--> <property name="interceptorNames"> <!--拦截器名称(也就是增强类名称,SpringBean的Id)--> <list> <value>greetingAroundAdvice</value> </list> </property> </bean>
使用ProxyFactoryFactoryBean就可以取代前面的ProxyFactory,其实他们是一回事。interceptorNames改名为adviceNames或许更容易让人理解。就是网这个属性里添加增强类。
此外,如果只有一个增强类,可以使用下面这个方法来简化
<bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="interfaces" value="com.smart4j.framework.Greeting"/> <!--需要代理的接口 name也可以为proxyInterfaces--> <property name="target" ref="greetingImpl"/> <!--实现接口类 name也可以为targetName--> <property name="interceptorNames" value="greetingAroundAdvice"> <!--拦截器名称(也就是增强类名称,SpringBean的Id)--> </property> </bean>
需要注意的是,这里使用了Spring2.5+的"Bean扫描"特性,这样我们就无需再Spring配置问加你了不断的定义<bean id="xxx" class="xxx"/>了,从而解脱了我们的双手。
去掉原本的
<bean id="greetingImpl" class="com.smart4j.framework.GreetingImpl"></bean> <bean id="greetingAroundAdvice" class="com.smart4j.framework.aop.GreetingAroundAdvice"></bean>
改为使用@Compoent
@Component public class GreetingImpl implements Greeting{ 。。。 } @Component public class GreetingAroundAdvice implements MethodInterceptor { 。。。 }
代码量确实少了,我们将配置性的代码放入配置文件,这样也有助于后期维护。更重要的是,代码值关注于业务逻辑,而将配置放入文件中,这是一条最佳实践!
除了上面提到的那三个增强意外,其实还有两个增强也需要了解一下,关键的时候要能想到它们才行。
Spring AOP:抛出增强
程序报错,抛出异常了,一般的做法是打印控制台到日志文件中,这样很多地方都得去处理,有没有一个一劳永逸的方法呢?那就是Throws Advice(抛出增强)
@Component public class GreetingThrowAdvice implements ThrowsAdvice { public void afterThrowing(Method method, Object[] args, Object target,Exception e){ System.out.println("-------------Throw Exception-----------------"); System.out.println("Target class: "+target.getClass().getName()); System.out.println("Method Name: "+method.getName()); System.out.println("Exception Message: "+e.getMessage()); System.out.println("---------------------------------------------"); } }
配置spring.xml
<bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces" value="com.smart4j.framework.Greeting"/> <!--需要代理的接口--> <property name="targetName" value="greetingImpl"/> <!--实现接口类--> <property name="interceptorNames"> <!--拦截器名称(也就是增强类名称,SpringBean的Id)--> <list> <!-- <value>greetingAroundAdvice</value>--> <value>greetingThrowAdvice</value> </list> </property> </bean>
结果
抛出增强需要实现org.springframework.aop.ThrowsAdvice接口,在接口方法中可获取方法、参数、目标对象、异常对象等信息。我们可以把这些信息统一写入到日志中,当然也可以持久化到数据库中。
Spring AOP:引入增强
以上提到的都是对方法的增强,那能否对类进行增强呢?用AOP的行话来讲,对方法的增强叫Weaving(织入),而对类的增强叫Introduction(引入),Introduction Advice(引入增强)就是对类的功能增强,它也是Spring AOP提供的最后一种增强。
定义接口
public interface Apology { void saySorry(String name); }
但是我们不想在代码中让GreetingImpl直接去实现这个接口,而想在程序运行的时候动态地实现它。因为加入实现了这个接口,那么久一定要改写GreetingImpl这个类,关键是我们不想改它,或许在真实场景中,这个类有一万行代码。于是,我们需要借助Spring的引入增强。
@Component public class GreetingIntroAdvice extends DelegatingIntroductionInterceptor implements Apology{ @Override public Object invoke(MethodInvocation mi) throws Throwable { return super.invoke(mi); } @Override public void saySorry(String name) { System.out.println("Sorry "+name); } }
以上一个引入增强类,扩展了org.springframework.aop.support.DelegatingIntroductionInterceptor类,同时也实现了新定义的Apology接口。在类中首先覆盖了父类的invoke()方法,然后实现了Apology接口的方法。我们相拥这个增强类去丰富GreetingImpl类的功能,那么这个GreetingImpl类无须直接实现Apology接口,就可以直接在程序运行的时候调用Apology接口的方法了。
配置Spring.xml
<!--引入增强--> <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces" value="com.smart4j.framework.aop.Apology"/> <!--需要动态实现的接口--> <property name="targetName" value="greetingImpl"/> <!--目标类--> <property name="interceptorNames" value="greetingIntroAdvice"/> <!--拦截器名称(也就是增强类名称,SpringBean的Id)--> <property name="proxyTargetClass" value="true"/> <!--代理目标类,(默认为false,代理接口)--> </bean>
需要注意proxyTargetClass属性,它表明是否代理目标类,默认为false,也就是代理接口,此时Spring就用JDK动态代理;如果为TRUE,那么Spring就用CGLib动态代理。
调用
ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml"); //获取Spring Context Greeting greeting = (Greeting) context.getBean("greetingProxy"); //从Context中根据id获取Bean对象(其实也就是一个代理) greeting.sayHello("jack"); //调用代理方法 Apology apology = (Apology) greeting; //将目标类增强向上转型为Apology接口(这是引入增强给我们带来的特性,也是"接口动态实现"功能) apology.saySorry("jack");
sarySorry方法原来是可以被greetingImpl对象来直接调用的,只需将其强制转换为该接口即可。
SpringAOP:切面
之前谈到的AOP框架其实可以将它理解为一个拦截器框架,但这个拦截器似乎非常武断。比如说,如果它拦截了一个类,那么它就拦截这个类中所有的方法。类似的,当我们在使用动态代理的时候,其实也遇到了这个问题。需要在代码中对所拦截的方法名加以判断,才能过滤出我们需要拦截的方法,这种做法确实不太优雅。在大量的真实项目中,似乎我们只需要拦截特定的方法就行了,没必要拦截所有的方法。于是,Spring借助很重要的工具---Advisor(切面),来解决这个问题。它也是AOP中的核心,是我们关注的重点。
也就是说,我们可以通过切面,将增强类与拦截匹配条件组合在一起,然后将这个切面配置到ProxyFactory中,从而生成代理。
这里提到这个“拦截匹配条件”在AOP中就叫作Pointcut(切点),其实说白了就是一个基于表达式的拦截条件。Advisor(切面)封装了Advice(增强)与Pointcut(切点)。
@Component public class GreetingImpl implements Greeting { @Override public void sayHello(String name) { System.out.println("Hello! "+name); } /*切面新增方法*/ public void goodMorning(String name){ System.out.println("Good Morning!"+name); } public void goodNight(String name){ System.out.println("Good Night!"+name); } }
在Spring AOP中,最好用的是基于正则表达式的切面类。
配置
<bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="advice" ref="greetingAroundAdvice"/> <!--增强--> <property name="pattern" value="com.smart4j.framework.GreetingImpl.good.*"/> <!--切点(正则表达式)--> </bean> <!--配置一个代理类--> <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="targetName" value="greetingImpl"/> <!--目标类--> <property name="interceptorNames" value="greetingAdvisor"/> <!--切面(替换之前的拦截器名称)--> <property name="proxyTargetClass" value="true"/> <!--代理目标类,(默认为false,代理接口)--> </bean>
调用
ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml"); //获取Spring Context GreetingImpl greeting = (GreetingImpl) context.getBean("greetingProxy"); //从Context中根据id获取Bean对象(其实也就是一个代理) greeting.sayHello("jack"); //调用未被代理方法 greeting.goodMorning("Jhon"); greeting.goodNight("Sawer");
结果
注意以上代理对象中的配置的interceptorNames,它不再是一个增强,而是一个切面,因为已经将增强封装到该切面中了。此外,切面还定义了一个切点(正则表达式),其目的是为了只对满足切点匹配条件的方法进行拦截。
这里的切点表达式是基于正则表达式的。其中.*代表匹配所有字符,翻译过来就是匹配GreetingImpl类中以good开头的方法。
除了RegexpMethodPointcutAdvisor以外,在Spring AOP中还提供了几个切面类,比如:
- DefaultPointcutAdvisor - 默认切面(可扩展它来自定义切面)
- NameMatchMethodPointcutAdvisor - 根据方法名称进行匹配的切面
- StaticMethodMatcherPointcutAdvisor - 用于匹配静态方法的切面
总的来说,让用户去配置一个或少数几个代理,似乎还可以接受,但随着项目的扩大,代理配置就会越来越多,配置的重复劳动就多了,麻烦不说,还很容易出错。。能否让Spring框架为我们自动生成代理呢?
Spring AOP:自动代理(扫描Bean名称)
Spring AOP提供了一个可以根据Bean名称来自动生成代理的工具,它就是BeanNameAutoProxyCreator。配置如下:
<!--自动代理--> <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="beanNames" value="*Impl"/> <!--为后缀是Impl的Bean生成代理--> <property name="interceptorNames" value="greetingAroundAdvice"/> <!--增强(这里没用切面)--> <property name="optimize" value="true"/> <!--是否对代理生成策略进行优化--> </bean>
调用
ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml"); //获取Spring Context GreetingImpl greeting = (GreetingImpl) context.getBean("greetingImpl"); //从Context中根据id获取Bean对象(自动扫描的id为首字母小写的类名) greeting.sayHello("jack");
以上使用BeanNameAutoProxyCreator只为后缀为"Impl"的Bean生成代理。需要注意的是,这个地方我们不能定义代理接口,也及时interfaces属性,因为我们根本就不知道这些Bean到底实现了多少接口。此时不能代理接口,而只能代理类。所以这里提供了一个新的配置项,它就是optimize。若为true时,则可对代理生成策略进行优化(默认是false)。也就是说,如果该类有接口,就代理接口(JDK动态代理);如果没有接口,就代理类(使用CGLib动态代理)。并非像之前使用的proxyTargetClass属性那样,强制代理类,而不考虑代理接口的方式。
既然CGLib可以代理任何类,那为什么还要用JDK的动态代理呢?
根据实际项目经验得知,CGLib创建代理的速度比较慢,但创建代理后运行的速度却非常快,而JDK动态代理正好相反。如果在运行的时候不断地用CGLib去创建代理,系统的性能会大打折扣,所以建议一般在系统初始化的时候用CGLib去创建代理,并放入Spring的ApplicationContext中以备后用。
这个例子只能匹配目标类,而不能进一步匹配其中指定的方法,要匹配方法,就要考虑使用切面与切点了。Spring AOP基于切面也提供了一个自动代理生成器:DefaultAdvisorAutoProxyCreator。
Spring AOP:自动代理(扫描切面配置)
为了匹配目标类中的指定方法,我们让然需要在Spring中配置切面与切点:
<!--自动代理 - 扫描切面配置--> <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="pattern" value="com.smart4j.framework.GreetingImpl.good.*"/> <property name="advice" ref="greetingAroundAdvice"/> </bean> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"> <property name="optimize" value="true" /> </bean>
这里无须再配置代理,因为代理将由DefaultAdvisorAutoProxyCreator自动生成。也就是说,这个类可以扫描所有的切面类,并为其自动生成代理。
看来不论怎么简化,Rod始终解决不了切面的配置这件繁重的手工劳动。在Spring配置文件中,仍然会存在大量的切面配置。然而在很多情况下,Spring AOP所提供的切面类真的不太够用了,比如像拦截指定注解的方法,我们就必须扩展DefaultPointcutAdvisor类,自定义一个切面类,然后在Spring配置文件中进行切面配置。Rod的解决方案似乎已经掉进了切面类的深渊,最重要的是切面,最麻烦的也是切面。所以要把切面配置给简化掉。
Spring+AspectJ
神一样的rod总算认识到了这一点,接受了网友们的建议,集成了AspectJ,同时也保留了以上提到的切面与代理配置方式(为了兼容老项目,更为了维护自己的面子)。将Spring与AspectJ集成与直接使用AspectJ是不同的,我们不需要定义AspectJ类(它扩展了Java语法的一种新的语言,还需要特定的编译器),只需要使用AspectJ切点表达式即可(它是比正则表达式更加友好的表现形式)。
1.Spring+AspectJ(基于注解:通过AspectJ execution表达式拦截方法)
定义一个Aspect切面类
@Aspect /*切面*/ @Component public class GreetingAspect { @Around("execution(* com.smart4j.framework.GreetingImpl.*(..))") /*切点*/ public Object around(ProceedingJoinPoint pjp) throws Throwable { /*增强*/ before(); Object result = pjp.proceed(); after(); return result; } private void before(){ System.out.println("Before"); } private void after(){ System.out.println("After"); } }
注意:类上面标注的Aspect注解表明该类是一个Aspect(其实就是Advisor)。该类无须实现任何的接口,只需定义一个方法(方法叫什么名字都无所谓),在方法上标注Around注解,在注解中使用AspectJ切点表达式。方法的参数中包括一个ProceedingJoinPoint对象,它在AOP中称为Joinpoint(连接点),可以通过该对象获取方法的任何信息,例如,方法名、参数等。
解析下切点表达式execution(* com.smart4j.framework.GreetingImpl.*(..))
- execution表示拦截方法,括号中可定义需要匹配的规则。
- 第一个"*"表示方法的返回值是任意的;
- 第二个"*"表示匹配该类中的所有方法;
- (..)表示方法的参数是任意的。
是不是比正则表达式可读性更强呢?如果想匹配指定的方法,只需将第二个“*”改为指定的方法名即可。
配置如下
<!--扫描指定包(将带有Component注解的类自动定义为SpringBean)--> <context:component-scan base-package="com.smart4j.framework"/> <aop:aspectj-autoproxy proxy-target-class="true"/>
两行配置就行了,不需要配置大量的代理,更不需要配置大量的切面!proxy-target-class属性,它的值默认是false,默认只能代理接口(使用JDK动态代理),当为true时,才能代理目标类(使用CGLib动态代理)。
Spring与AspectJ结合功能远远不止这些,我们还可以拦截指定注解的方法。
2.Spring+AspectJ(基于注解:通过AspectJ @annotation表达式拦截方法)
注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Tag { }
以上定义一个Tag注解,此注解可标注在方法上,在运行时生效。
@Aspect /*切面*/ @Component public class GreetingAspect { @Around("@annotation(com.smart4j.framework.aspectj.Tag)") /*切点 - 有Tag标记的Method*/ public Object around(ProceedingJoinPoint pjp) throws Throwable { /*增强*/ before(); Object result = pjp.proceed(); after(); return result; } private void before(){ System.out.println("Before"); } private void after(){ System.out.println("After"); } }
直接将Tag注解定义在想要拦截的方法上
@Component public class GreetingImpl implements Greeting { @Tag /*AspectJ 注解*/ @Override public void sayHello(String name) { System.out.println("Hello! "+name); } }
在以上实例中只有一个方法,如果有多个方法,我们只想拦截其中的某一些时,这种解决方案会更加有价值。
除了Around注解外,其实还有几个相关的注解,稍微归纳一下:
- Before - 前置增强
- After - 后置增强
- Around - 环绕增强
- AfterThrowing - 抛出增强
- DeclareParents - 引入增强
此外还有一个AfterReturning(返回后增强),也可理解为Finally增强,相当于finally语句,它是在方法结束后执行的,也就是说,它比After晚一些。
3.Spring+AspectJ(引入增强)
为了实现基于AspectJ的引入增强,我们同样需要定义一个Aspect类:
@Aspect /*切面*/ @Component public class GreetingAspect { /*引入增强*/ @DeclareParents(value = "com.smart4j.framework.GreetingImpl",defaultImpl = ApologyImpl.class) private Apology apology; }
在Aspect类中定义一个需要引入增强的接口,它也就是运行时需要动态实现的接口。在这个接口上标注了DeclareParents注解,该注解有两个属性:
- Value - 目标类;
- defaultImpl - 引入接口的默认实现类。
我们只需要对引入的接口提供一个默认实现类即可完成增强:
public class ApologyImpl implements Apology { @Override public void saySorry(String name) { System.out.println("Sorry! " + name); } }
运行
ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml"); //获取Spring Context GreetingImpl greeting = (GreetingImpl) context.getBean("greetingImpl"); //从Context中根据id获取Bean对象(自动扫描的id为首字母小写的类名) greeting.sayHello("jack"); Apology apology = (Apology) greeting; //将目标类增强向上转型为Apology接口(这是引入增强给我们带来的特性,也是"接口动态实现"功能) apology.saySorry("jack");
从SpringApplicationContext中获取greetingImpl对象(其实是个代理对象),可转型为自己静态实现的接口Greeting,也可转型为自己动态实现的接口Apology,切换起来非常方便。
使用AspectJ的引入增强比原来的SpringAOP的引入增强更加方便了,而且还可面向接口编程(以前只能面向实现类)。
这一切已经非常强大并且非常灵活了,但仍然还是由用户不能尝试这些特性,因为他们还在使用JDK1.4(根本就没有注解这个东西),怎么办呢?SpringAOP为那些遗留系统也考虑到了。
3.Spring+AspectJ(基于配置)
除了使用Aspect注解来定义切面之外,SpringAOP也提供了基于配置的方式来定义切面类:
<!--AspectJ - 基于配置--> <bean id="greetingImpl" class="com.smart4j.framework.GreetingImpl"/> <bean id="greetingAspect" class="com.smart4j.framework.aspectj.GreetingAspect"/> <aop:config> <aop:aspect ref="greetingAspect"> <aop:around method="around" pointcut="execution(* com.smart4j.framework.GreetingImpl.*(..))"/> </aop:aspect> </aop:config>
使用<aop:config>元素来进行AOP配置,在其子元素中配置切面,包括增强类型、目标方法、切点等信息。
无论用户是不能使用注解,还是不愿意使用注解,SpringAOP都能提供全方位的服务。
AOP思维导图
各类增强类型所对应的解决方案
SpringAOP整体架构UML类图