本文属于spring的系列学习笔记,发现自己对spring还有不明白的地方,重新梳理下,加深印象。另外,本文转发自方腾飞大神,
原文地址:http://www.iteye.com/topic/1116696
AOP(Aspect-OrientedProgramming,面向方面编程),是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。OOP是从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中。
而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。
实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。
AOP用来封装横切关注点,可以在权限、缓存、日志、事物等场景使用。
方面(Aspect):一个关注点的模块化,这个关注点实现可能另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的 Advisor或拦截器实现。
连接点(Joinpoint): 程序执行过程中明确的点,如方法的调用或特定的异常被抛出。
通知(Advice): 在特定的连接点,AOP框架执行的动作。各种类型的通知包括“around”、“before”和“throws”通知。通知类型将在下面讨论。许多AOP框架包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链。Spring中定义了四个advice: BeforeAdvice, AfterAdvice, ThrowAdvice和DynamicIntroductionAdvice
切入点(Pointcut): 指定一个通知将被引发的一系列连接点的集合。AOP框架必须允许开发者指定切入点:例如,使用正则表达式。 Spring定义了Pointcut接口,用来组合MethodMatcher和ClassFilter,可以通过名字很清楚的理解, MethodMatcher是用来检查目标类的方法是否可以被应用此通知,而ClassFilter是用来检查Pointcut是否应该应用到目标类上
引入(Introduction): 添加方法或字段到被通知的类。 Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现 IsModified接口,来简化缓存。Spring中要使用Introduction, 可有通过DelegatingIntroductionInterceptor来实现通知,通过DefaultIntroductionAdvisor来配置Advice和代理类要实现的接口
目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象。POJO
AOP代理(AOP Proxy): AOP框架创建的对象,包含通知。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
织入(Weaving): 组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。
AOP就是面向切面编程,我们可以从几个层面来实现AOP。
在编译器修改源代码,在运行期字节码加载前修改字节码或字节码加载后动态创建代理类的字节码,以下是各种实现机制的比较。
类别 |
机制 |
原理 |
优点 |
缺点 |
静态AOP |
静态织入 |
在编译期,切面直接以字节码的形式编译到目标字节码文件中。 |
对系统无性能影响。 |
灵活性不够。 |
动态AOP |
动态代理 |
在运行期,目标类加载后,为接口动态生成代理类,将切面植入到代理类中。 |
相对于静态AOP更加灵活。 |
切入的关注点需要实现接口。对系统有一点性能影响。 |
动态字节码生成 |
在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中。 |
没有接口也可以织入。 |
扩展类的实例方法为final时,则无法进行织入。 |
|
自定义类加载器 |
在运行期,目标加载前,将切面逻辑加到目标字节码里。 |
可以对绝大部分类进行织入。 |
代码中如果使用了其他类加载器,则这些类将不会被织入。 |
|
字节码转换 |
在运行期,所有类加载器加载字节码前,前进行拦截。 |
可以对所有类进行织入。 |
织入器通过在切面中定义pointcut来搜索目标(被代理类)的JoinPoint(切入点),然后把要切入的逻辑(Advice)织入到目标对象里,生成代理类。
Java在JDK1.3后引入的动态代理机制,使我们可以在运行期动态的创建代理类。使用动态代理实现AOP需要有四个角色:被代理的类,被代理类的接口,织入器,和InvocationHandler,而织入器使用接口反射机制生成一个代理类,然后在这个代理类中织入代码。被代理的类是AOP里所说的目标,InvocationHandler是切面,它包含了Advice和Pointcut。
demo如下,演示在方法执行前织入一段记录日志的代码,其中Business是代理类,LogInvocationHandler是记录日志的切面,IBusiness, IBusiness2是代理类的接口,Proxy.newProxyInstance是织入器。
清单一:动态代理的演示
package reflect; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class DynamicProxyDemo { /** * @param args */ public static void main(String[] args) { //需要代理的接口,被代理类实现的多个接口都必须在这里定义 Class[] proxyInterface = new Class[] { IBusiness.class, IBusiness2.class }; //构建AOP的Advice,这里需要传入业务类的实例 LogInvocationHandler handler = new LogInvocationHandler(new Business()); //生成代理类的字节码加载器 ClassLoader classLoader = DynamicProxyDemo.class.getClassLoader(); //织入器,织入代码并生成代理类 IBusiness2 proxyBusiness = (IBusiness2) Proxy.newProxyInstance(classLoader, proxyInterface, handler); //使用代理类的实例来调用方法。 proxyBusiness.doSomeThing2(); ((IBusiness) proxyBusiness).doSomeThing(); } /** * 打印日志的切面 */ public static class LogInvocationHandler implements InvocationHandler { private Object target; //目标对象 LogInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //执行原有逻辑 Object rev = method.invoke(target, args); //执行织入的日志,你可以控制哪些方法执行切入逻辑 if (method.getName().equals("doSomeThing2")) { System.out.println("记录日志"); } return rev; } } }
package reflect; public class Business implements IBusiness, IBusiness2 { @Override public boolean doSomeThing() { System.out.println("执行业务逻辑"); return true; } @Override public void doSomeThing2() { System.out.println("执行业务逻辑2"); } }
package reflect; public interface IBusiness { public boolean doSomeThing(); }
package reflect; public interface IBusiness2 { public void doSomeThing2(); }运行结果:
可见“记录日志”的逻辑切入到Business类的doSomeThing2方法后面了。
原理分析:参见动态代理那篇http://blog.csdn.net/bohu83/article/details/51124094
小结:
动态代理在运行期通过接口动态生成代理类,这为其带来了一定的灵活性,但这个灵活性却带来了两个问题,第一代理类必须实现一个接口,如果没实现接口会抛出一个异常。第二性能影响,因为动态代理使用反射的机制实现的,首先反射肯定比直接调用要慢,经过测试大概每个代理类比静态代理多出10几毫秒的消耗。其次使用反射大量生成类文件可能引起Full GC造成性能影响,因为字节码文件加载后会存放在JVM运行时区的方法区(或者叫持久代)中,当方法区满的时候,会引起Full GC,所以当你大量使用动态代理时,可以将持久代设置大一些,减少Full GC次数。
本节介绍如何使用Cglib来实现动态字节码技术。Cglib是一个强大的,高性能的Code生成类库,它可以在运行期间扩展Java类和实现Java接口,它封装了Asm,所以使用Cglib前需要引入Asm的jar。 清单七:使用CGLib实现AOP
package reflect; 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 CgTest { public static void main(String[] args) { byteCodeGe(); } public static void byteCodeGe() { //创建一个织入器 Enhancer enhancer = new Enhancer(); //设置父类 enhancer.setSuperclass(Business.class); //设置需要织入的逻辑 enhancer.setCallback(new LogIntercept()); //使用织入器创建子类 IBusiness2 newBusiness = (IBusiness2) enhancer.create(); newBusiness.doSomeThing2(); } /** * 记录日志 */ public static class LogIntercept implements MethodInterceptor { @Override public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable { //执行原有逻辑,注意这里是invokeSuper Object rev = proxy.invaokeSuper(target, args); //执行织入的日志 if (method.getName().equals("doSomeThing2")) { System.out.println("记录日志"); } return rev; } } }注意:这里要引入cglib的jar:cglib-3.*.jar,asm*.jar
CGlib是一个强大的,高性能,高质量的Code生成类库。它可以在运行期扩展Java类与实现Java接口。其底层是通过小而快的字节码处理框架ASM(http://forge.ow2.org/projects/asm,使用BSD License)来转换字节码并生成新的类。大部分功能实际上是asm所提供的,CGlib只是封装了asm,简化了asm的操作,实现了在运行期动态生成新的class。
本质上,它是通过动态的生成一个子类去覆盖所要代理类的不是final的方法,并设置好callback,则原有类的每个方法调用就会转变成调用用户定义的拦截方法(interceptors),这比JDK动态代理方法快多了。可见,Cglib的原理是对指定的目标类动态生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类和final方法进行代理。
创建一个具体类的代理时,通常要用到的CGLIB包的APIs:
net.sf.cglib.proxy.Callback接口:在CGLIB包中是一个很关键的接口,所有被net.sf.cglib.proxy.Enhancer类调用的回调(callback)接口都要继承这个接口。
net.sf.cglib.proxy.MethodInterceptor接口:是最通用的回调(callback)类型,它经常被AOP用来实现拦截(intercept)方法的调用。这个接口只定义了一个方法。
public Object intercept(Object object, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable;当net.sf.cglib.proxy.MethodInterceptor做为所有代理方法的回调 (callback)时,当对基于代理的方法调用时,在调用原对象的方法的之前会调用这个方法,如图下图所示。第一个参数是代理对像,第二和第三个参数分别 是拦截的方法和方法的参数。原来的方法可能通过使用java.lang.reflect.Method对象的一般反射调用,或者使用 net.sf.cglib.proxy.MethodProxy对象调用。net.sf.cglib.proxy.MethodProxy通常被首选使用,因为它更快。在这个方法中,我们可以在调用原方法之前或之后注入自己的代码。
这样我们在结合上面的代码就容易理解了,注意一点:LogIntercept里面执行原来方法的时候使用了proxy.invokeSuper。是由于性能的原因,对原始方法的调用我们使用CGLIB的net.sf.cglib.proxy.MethodProxy对象,而不是反射中一般使用java.lang.reflect.Method对象。
Javassist是一个编辑字节码的框架,可以让你很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法。这比使用Cglib实现AOP更加高效,并且没太多限制,实现原理如下图:
我们使用系统类加载器启动我们自定义的类加载器,在这个类加载器里加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑,咱们再看看使用Javassist实现AOP的代码:
清单八:启动自定义的类加载器及类加载监听器
package reflect; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.Loader; import javassist.NotFoundException; import javassist.Translator; public class JavassistAopDemo { /** * @param args * @throws Throwable */ public static void main(String[] args) throws Throwable { // TODO Auto-generated method stub //获取存放CtClass的容器ClassPool ClassPool cp = ClassPool.getDefault(); //创建一个类加载器 Loader cl = new Loader(); //增加一个转换器 cl.addTranslator(cp, new MyTranslator()); //启动MyTranslator的main函数 cl.run("reflect.JavassistAopDemo$MyTranslator", args); } public static class MyTranslator implements Translator { public void start(ClassPool pool) throws NotFoundException, CannotCompileException { } /* * * 类装载到JVM前进行代码织入 */ public void onLoad(ClassPool pool, String classname) { if (!"model$Business".equals(classname)) { return; } //通过获取类文件 try { CtClass cc = pool.get(classname); //获得指定方法名的方法 CtMethod m = cc.getDeclaredMethod("doSomeThing"); //在方法执行前插入代码 m.insertBefore("{ System.out.println(\"记录日志\"); }"); } catch (NotFoundException e) { } catch (CannotCompileException e) { } } public static void main(String[] args) { Business b = new Business(); b.doSomeThing2(); b.doSomeThing(); } } }注意:这里要引用javassist.jar
package javassist; import java.lang.reflect.Method; import javassist.util.proxy.MethodFilter; import javassist.util.proxy.MethodHandler; import javassist.util.proxy.ProxyFactory; public class JavassistAopDemo2 { public static void main(String[] args) throws Exception{ ProxyFactory factory=new ProxyFactory(); //设置父类,ProxyFactory将会动态生成一个类,继承该父类 factory.setSuperclass(Business.class); //设置过滤器,判断哪些方法调用需要被拦截 factory.setFilter(new MethodFilter() { @Override public boolean isHandled(Method m) { if(m.getName().equals("doSomeThing")){ return true; } return false; } }); //设置拦截处理 factory.setHandler(new Javassisthandler()); Class<?> c=factory.createClass(); Business object=(Business) c.newInstance(); object.doSomeThing2(); object.doSomeThing(); } } class Javassisthandler implements MethodHandler{ @Override public Object invoke(Object target, Method method, Method proxy, Object[] args) throws Throwable { // TODO Auto-generated method stub //这里插入日志,可以根据实际情况调整 System.out.println("记录日志..."); Object result = proxy.invoke(target, args); return result; } }
CGLib的底层基于ASM实现,是一个高效高性能的生成库;而ASM是一个轻量级的类库,但需要涉及到JVM的操作和指令;相比而言,Javassist要简单的多,完全是基于Java的API,但是它仍然存在一个问题,就是如果其他的类加载器来加载类的话,这些类将不会被拦截。
我在整理这块的时候,需要去看看对应的API,如果你跟我一样没用过cglib,javassist这些,建议网上专门找对应的专题看看,本文只是侧重aop的介绍。
package javassist; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.NotFoundException; public class MyClassFileTransformer implements ClassFileTransformer { /** * 字节码加载到虚拟机前会进入这个方法 */ public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println(className); //如果加载Business类才拦截 if (!"javassist/Business".equals(className)) { return null; } //javassist的包名是用点分割的,需要转换下 if (className.indexOf("/") != -1) { className = className.replaceAll("/", "."); } try { //通过包名获取类文件 CtClass cc = ClassPool.getDefault().get(className); //获得指定方法名的方法 CtMethod m = cc.getDeclaredMethod("doSomeThing"); //在方法执行前插入代码 m.insertBefore("{ System.out.println(\"记录日志\"); }"); return cc.toBytecode(); } catch (NotFoundException e) { } catch (CannotCompileException e) { } catch (IOException e) { //忽略异常处理 } return null; } public static void premain(String options, Instrumentation ins) { //注册我自己的字节码转换器 ins.addTransformer(new MyClassFileTransformer()); } }其中最下面的 使用premain函数注册字节码转换器,该方法在main函数之前执行。
可以看出来,最开始打包的时候,没有MANIFEST.MF,所以报错了。
Manifest-Version: 1.0 Created-By: 1.7.0_79 (Oracle Corporation) Premain-class: javassist.MyClassFileTransformer然后在JVM的启动参数里加上。-javaagent:D:\klsf123\Test\lib\aop.jar
运行测试结果如下:
从输出中可以看到系统类加载器加载的类也经过了这里。
Spring默认采取的动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用CGlib机制。但Spring的AOP有一定的缺点,第一个只能对方法进行切入,不能对接口,字段,静态代码块进行切入(切入接口的某个方法,则该接口下所有实现类的该方法将被切入)。第二个同类中的互相调用方法将不会使用代理类。因为要使用代理类必须从Spring容器中获取Bean。第三个性能不是最好的,从3.3章节我们得知使用自定义类加载器,性能要优于动态代理和CGlib。
可以获取代理类
public IMsgFilterService getThis() { return (IMsgFilterService) AopContext.currentProxy(); } public boolean evaluateMsg () { // 执行此方法将织入切入逻辑 return getThis().evaluateMsg(String message); } @MethodInvokeTimesMonitor("KEY_FILTER_NUM") public boolean evaluateMsg(String message) {不能获取代理类
public boolean evaluateMsg () { // 执行此方法将不会织入切入逻辑 return evaluateMsg(String message); } @MethodInvokeTimesMonitor("KEY_FILTER_NUM") public boolean evaluateMsg(String message) {
参考:
http://blog.csdn.net/dreamthen/article/details/26687727
http://blog.csdn.net/moreevan/article/details/11977115
http://blog.sina.com.cn/s/blog_624a352c0101fo9j.html
https://www.ibm.com/developerworks/cn/java/j-lo-springaopcglib/
http://blog.csdn.net/mhmyqn/article/details/48474815
http://blog.csdn.net/zhoudaxia/article/details/30591941