Spring学习笔记--AOP详解

转载自: http://www.iteye.com/topic/1116696(在此先感谢作者的整理)

1 AOP各种的实现

 

AOP就是面向切面编程,我们可以从几个层面来实现AOP。

 

在编译器修改源代码,在运行期字节码加载前修改字节码或字节码加载后动态创建代理类的字节码,以下是各种实现机制的比较。 

 

类别

机制

原理

优点

缺点

静态AOP

静态织入

在编译期,切面直接以字节码的形式编译到目标字节码文件中。

对系统无性能影响。

灵活性不够。

动态AOP

动态代理

在运行期,目标类加载后,为接口动态生成代理类,将切面植入到代理类中。

相对于静态AOP更加灵活。

切入的关注点需要实现接口。对系统有一点性能影响。

动态字节码生成

在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中。

没有接口也可以织入。

扩展类的实例方法为final时,则无法进行织入。

自定义类加载器

在运行期,目标加载前,将切面逻辑加到目标字节码里。

可以对绝大部分类进行织入。

代码中如果使用了其他类加载器,则这些类将不会被织入。

字节码转换

在运行期,所有类加载器加载字节码前,前进行拦截。

可以对所有类进行织入。

 



2 AOP里的公民

· Joinpoint:拦截点,如某个业务方法。

· Pointcut:Joinpoint的表达式,表示拦截哪些方法。一个Pointcut对应多个Joinpoint。

· Advice:  要切入的逻辑。

· Before Advice 在方法前切入。

· After Advice 在方法后切入,抛出异常时也会切入。

· After Returning Advice 在方法返回后切入,抛出异常则不会切入。

· After Throwing Advice 在方法抛出异常时切入。

· Around Advice 在方法执行前后切入,可以中断或忽略原有流程的执行。 l

· 公民之间的关系

织入器通过在切面中定义pointcut来搜索目标(被代理类)的JoinPoint(切入点),然后把要切入的逻辑(Advice)织入到目标对象里,生成代理类。

3 AOP的实现机制 

本章节将详细介绍AOP有各种实现机制。


3.1 动态代理 

Java在JDK1.3后引入的动态代理机制,使我们可以在运行期动态的创建代理类。使用动态代理实现AOP需要有四个角色:被代理的类,被代理类的接 口,织入器,和InvocationHandler,而织入器使用接口反射机制生成一个代理类,然后在这个代理类中织入代码。被代理的类是AOP里所说的 目标,InvocationHandler是切面,它包含了Advice和Pointcut。


3.1.1 使用动态代理

  那如何使用动态代理来实现AOP。下面的例子演示在方法执行前织入一段记录日志的代码,其中Business是代理 类,LogInvocationHandler是记录日志的切面,IBusiness, IBusiness2是代理类的接口,Proxy.newProxyInstance是织入器。
清单一:动态代理的演示

Java代码  

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;   
    }   
}   

接口IBusiness和IBusiness2定义省略。   

 

   业务类,需要代理的类。

Java代码  

public class Business implements IBusiness, IBusiness2 {   

    @Override   
    public boolean doSomeThing() {   
        System.out.println("执行业务逻辑");   
        return true;   
    }   

    @Override   
    public void doSomeThing2() {   
        System.out.println("执行业务逻辑2");   
    }   

}   
 输出

Java代码  

53 执行业务逻辑2   

54 记录日志   

55 执行业务逻辑   

 

  可以看到“记录日志”的逻辑切入到Business类的doSomeThing方法前了。

 

3.1.2 动态代理原理

    本节将结合动态代理的源代码讲解其实现原理。动态代理的核心其实就是代理对象的生成,即 Proxy.newProxyInstance(classLoader, proxyInterface, handler)。让我们进入newProxyInstance方法观摩下,核心代码其实就三行。
清单二:生成代理类

Java代码  

//获取代理类   
Class cl = getProxyClass(loader, interfaces);   
//获取带有InvocationHandler参数的构造方法   
Constructor cons = cl.getConstructor(constructorParams);   
//把handler传入构造方法生成实例   
return (Object) cons.newInstance(new Object[] { h });     
 

    其中getProxyClass(loader, interfaces)方法用于获取代理类,它主要做了三件事情:在当前类加载器的缓存里搜索是否有代理类,没有则生成代理类并缓存在本地JVM里。清单三:查找代理类。

Java代码  

 // 缓存的key使用接口名称生成的List   
Object key = Arrays.asList(interfaceNames);   
synchronized (cache) {   
    do {   
		Object value = cache.get(key);   
		// 缓存里保存了代理类的引用   
		if (value instanceof Reference) {   
			proxyClass = (Class) ((Reference) value).get();   
		}   

		if (proxyClass != null) {   
		// 代理类已经存在则返回   
			return proxyClass;   
		} else if (value == pendingGenerationMarker) {   
			// 如果代理类正在产生,则等待   
			try {   
				cache.wait();   
			} catch (InterruptedException e) {   
			}   
			continue;   
		} else {   
			//没有代理类,则标记代理准备生成   
			cache.put(key, pendingGenerationMarker);   
			break;   
		}   
    } while (true);   
}   


  

代理类的生成主要是以下这两行代码。 清单四:生成并加载代理类

 

Java代码  

88 //生成代理类的字节码文件并保存到硬盘中(默认不保存到硬盘)   

89 proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);   

90 //使用类加载器将字节码加载到内存中   

91 proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);   

 

  ProxyGenerator.generateProxyClass()方法属于sun.misc包下,Oracle并没有提供源代码,但是我们可以使用JD-GUI这样的反编译软件打开jre\lib\rt.jar来一探究竟,以下是其核心代码的分析。
清单五:代理类的生成过程

Java代码  

92 //添加接口中定义的方法,此时方法体为空   

93 for (int i = 0; i < this.interfaces.length; i++) {   

94   localObject1 = this.interfaces[i].getMethods();   

95   for (int k = 0; k < localObject1.length; k++) {   

96      addProxyMethod(localObject1[k], this.interfaces[i]);   

97   }   

98 }   

99   

100 //添加一个带有InvocationHandler的构造方法   

101 MethodInfo localMethodInfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);   

102   

103 //循环生成方法体代码(省略)   

104 //方法体里生成调用InvocationHandler的invoke方法代码。(此处有所省略)   

105 this.cp.getInterfaceMethodRef("InvocationHandler", "invoke", "Object; Method; Object;")   

106   

107 //将生成的字节码,写入硬盘,前面有个if判断,默认情况下不保存到硬盘。   

108 localFileOutputStream = new FileOutputStream(ProxyGenerator.access$000(this.val$name) + ".class");   

109 localFileOutputStream.write(this.val$classFile);   

 

  那么通过以上分析,我们可以推出动态代理为我们生成了一个这样的代理类。把方法doSomeThing的方法体修改为调用LogInvocationHandler的invoke方法。
清单六:生成的代理类源码

 

Java代码  

110 public class ProxyBusiness implements IBusiness, IBusiness2 {   

111   

112 private LogInvocationHandler h;   

113   

114 @Override   

115 public void doSomeThing2() {   

116     try {   

117         Method m = (h.target).getClass().getMethod("doSomeThing", null);   

118         h.invoke(this, m, null);   

119     } catch (Throwable e) {   

120         // 异常处理(略)   

121     }   

122 }   

123   

124 @Override   

125 public boolean doSomeThing() {   

126     try {   

127        Method m = (h.target).getClass().getMethod("doSomeThing2", null);   

128        return (Boolean) h.invoke(this, m, null);   

129     } catch (Throwable e) {   

130         // 异常处理(略)   

131     }   

132     return false;   

133 }   

134   

135 public ProxyBusiness(LogInvocationHandler h) {   

136     this.h = h;   

137 }   

138   

139 //测试用   

140 public static void main(String[] args) {   

141     //构建AOP的Advice   

142     LogInvocationHandler handler = new LogInvocationHandler(new Business());   

143     new ProxyBusiness(handler).doSomeThing();   

144     new ProxyBusiness(handler).doSomeThing2();   

145 }   

146 }   

 

3.1.3 小结   

 从前两节的分析我们可以看出,动态代理在运行期通过接口动态生成代理类,这为其带来了一定的灵活性,但这个灵活性却带来了两个问题,第一代理类必须实现一 个接口,如果没实现接口会抛出一个异常。第二性能影响,因为动态代理使用反射的机制实现的,首先反射肯定比直接调用要慢,经过测试大概每个代理类比静态代 理多出10几毫秒的消耗。其次使用反射大量生成类文件可能引起Full GC造成性能影响,因为字节码文件加载后会存放在JVM运行时区的方法区(或者叫持久代)中,当方法区满的时候,会引起Full GC,所以当你大量使用动态代理时,可以将持久代设置大一些,减少Full GC次数。


3.2 动态字节码生成

   使用动态字节码生成技术实现AOP原理是在运行期间目标字节码加载后,生成目标类的子类,将切面逻辑加入到子类中,所以使用Cglib实现AOP不需要基于接口。


    本节介绍如何使用Cglib来实现动态字节码技术。Cglib是一个强大的,高性能的Code生成类库,它可以在运行期间扩展Java类和实现Java接口,它封装了Asm,所以使用Cglib前需要引入Asm的jar。 清单七:使用CGLib实现AOP

Java代码  

147 public static void main(String[] args) {   

148         byteCodeGe();   

149     }   

150   

151     public static void byteCodeGe() {   

152         //创建一个织入器   

153         Enhancer enhancer = new Enhancer();   

154         //设置父类   

155         enhancer.setSuperclass(Business.class);   

156         //设置需要织入的逻辑   

157         enhancer.setCallback(new LogIntercept());   

158         //使用织入器创建子类   

159         IBusiness2 newBusiness = (IBusiness2) enhancer.create();   

160         newBusiness.doSomeThing2();   

161     }   

162   

163     /**  

164      * 记录日志  

165      */   

166     public static class LogIntercept implements MethodInterceptor {   

167   

168         @Override   

169         public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {   

170             //执行原有逻辑,注意这里是invokeSuper   

171             Object rev = proxy.invokeSuper(target, args);   

172             //执行织入的日志   

173             if (method.getName().equals("doSomeThing2")) {   

174                 System.out.println("记录日志");   

175             }   

176             return rev;   

177         }   

178     }   

 

 

3.3 自定义类加载器

   如果我们实现了一个自定义类加载器,在类加载到JVM之前直接修改某些类的方法,并将切入逻辑织入到这个方法里,然后将修改后的字节码文件交给虚拟机运行,那岂不是更直接。

 



Javassist是一个编辑字节码的框架,可以让你很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法。这比使用Cglib实现AOP更加高效,并且没太多限制,实现原理如下图:






    我们使用系统类加载器启动我们自定义的类加载器,在这个类加载器里加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑,咱们再看看使用Javassist实现AOP的代码:
清单八:启动自定义的类加载器

Java代码  

179 //获取存放CtClass的容器ClassPool   

180 ClassPool cp = ClassPool.getDefault();   

181 //创建一个类加载器   

182 Loader cl = new Loader();   

183 //增加一个转换器   

184 cl.addTranslator(cp, new MyTranslator());   

185 //启动MyTranslator的main函数   

186 cl.run("jsvassist.JavassistAopDemo$MyTranslator", args);   

 清单九:类加载监听器

Java代码  

187 public static class MyTranslator implements Translator {   

188   

189         public void start(ClassPool pool) throws NotFoundException, CannotCompileException {   

190         }   

191   

192         /* *  

193          * 类装载到JVM前进行代码织入  

194          */   

195         public void onLoad(ClassPool pool, String classname) {   

196             if (!"model$Business".equals(classname)) {   

197                 return;   

198             }   

199             //通过获取类文件   

200             try {   

201                 CtClass  cc = pool.get(classname);   

202                 //获得指定方法名的方法   

203                 CtMethod m = cc.getDeclaredMethod("doSomeThing");   

204                 //在方法执行前插入代码   

205                 m.insertBefore("{ System.out.println(\"记录日志\"); }");   

206             } catch (NotFoundException e) {   

207             } catch (CannotCompileException e) {   

208             }   

209         }   

210   

211         public static void main(String[] args) {   

212             Business b = new Business();   

213             b.doSomeThing2();   

214             b.doSomeThing();   

215         }   

216     }   

 输出: 

Java代码  

217 执行业务逻辑2   

218 记录日志   

219 执行业务逻辑  

 
    其中Bussiness类在本文的清单一中定义。看起来是不是特别简单,CtClass是一个class文件的抽象描述。咱们也可以使用insertAfter()在方法的末尾插入代码,使用insertAt()在指定行插入代码。

3.3.1 小结

    从本节中可知,使用自定义的类加载器实现AOP在性能上要优于动态代理和Cglib,因为它不会产生新类,但是它仍然存在一个问题,就是如果其他的类加载器来加载类的话,这些类将不会被拦截。

3.4 字节码转换

    自定义的类加载器实现AOP只能拦截自己加载的字节码,那么有没有一种方式能够监控所有类加载器加载字节码呢?有,使用Instrumentation, 它是 Java 5 提供的新特性,使用 Instrumentation,开发者可以构建一个字节码转换器,在字节码加载前进行转换。本节使用Instrumentation和 javassist来实现AOP。

3.4.1 构建字节码转换器

    首先需要创建字节码转换器,该转换器负责拦截Business类,并在Business类的doSomeThing方法前使用javassist加入记录日志的代码。

Java代码  

220 public class MyClassFileTransformer implements ClassFileTransformer {   

221   

222     /**  

223      * 字节码加载到虚拟机前会进入这个方法  

224      */   

225     @Override   

226     public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,   

227                             ProtectionDomain protectionDomain, byte[] classfileBuffer)   

228             throws IllegalClassFormatException {   

229         System.out.println(className);   

230         //如果加载Business类才拦截   

231         if (!"model/Business".equals(className)) {   

232             return null;   

233         }   

234   

235         //javassist的包名是用点分割的,需要转换下   

236         if (className.indexOf("/") != -1) {   

237             className = className.replaceAll("/", ".");   

238         }   

239         try {   

240             //通过包名获取类文件   

241             CtClass cc = ClassPool.getDefault().get(className);   

242             //获得指定方法名的方法   

243             CtMethod m = cc.getDeclaredMethod("doSomeThing");   

244             //在方法执行前插入代码   

245             m.insertBefore("{ System.out.println(\"记录日志\"); }");   

246             return cc.toBytecode();   

247         } catch (NotFoundException e) {   

248         } catch (CannotCompileException e) {   

249         } catch (IOException e) {   

250             //忽略异常处理   

251         }   

252         return null;   

253 }   

 

3.4.2 注册转换器

    使用premain函数注册字节码转换器,该方法在main函数之前执行。

Java代码  

254 public class MyClassFileTransformer implements ClassFileTransformer {   

255     public static void premain(String options, Instrumentation ins) {   

256         //注册我自己的字节码转换器   

257         ins.addTransformer(new MyClassFileTransformer());   

258 }   

259 }   

 

3.4.3 配置和执行   

需要告诉JVM在启动main函数之前,需要先执行premain函数。首先需要将premain函数所在的类打成jar包。并修改该jar包里的META-INF\MANIFEST.MF 文件。 

Java代码  

260 Manifest-Version: 1.0   

261 Premain-Class: bci. MyClassFileTransformer  

     然后在JVM的启动参数里加上。-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar

3.4.4 输出

   执行main函数,你会发现切入的代码无侵入性的织入进去了。

Java代码  

262 public static void main(String[] args) {   

263    new Business().doSomeThing();   

264    new Business().doSomeThing2();   

265 }   

266    

   输出

Java代码  

267 model/Business   

268 sun/misc/Cleaner   

269 java/lang/Enum   

270 model/IBusiness   

271 model/IBusiness2   

272 记录日志   

273 执行业务逻辑   

274 执行业务逻辑2   

275 java/lang/Shutdown   

276 java/lang/Shutdown$Lock   

  

 从输出中可以看到系统类加载器加载的类也经过了这里。

 

4 AOP实战

说了这么多理论,那AOP到底能做什么呢? AOP能做的事情非常多。

· 性能监控,在方法调用前后记录调用时间,方法执行太长或超时报警。

· 缓存代理,缓存某方法的返回值,下次执行该方法时,直接从缓存里获取。

· 软件破解,使用AOP修改软件的验证类的判断逻辑。

· 记录日志,在方法执行前后记录系统日志。

· 工作流系统,工作流系统需要将业务代码和流程引擎代码混合在一起执行,那么我们可以使用AOP将其分离,并动态挂接业务。

· 权限验证,方法执行前验证是否有权限执行当前方法,没有则抛出没有权限执行异常,由业务代码捕捉。 

4.1 Spring的AOP   

Spring默认采取的动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用CGlib机制。但Spring的AOP有一定的缺点,第一个 只能对方法进行切入,不能对接口,字段,静态代码块进行切入(切入接口的某个方法,则该接口下所有实现类的该方法将被切入)。第二个同类中的互相调用方法 将不会使用代理类。因为要使用代理类必须从Spring容器中获取Bean。第三个性能不是最好的,从3.3章节我们得知使用自定义类加载器,性能要优于 动态代理和CGlib。
可以获取代理类

Java代码  

277 public IMsgFilterService getThis()   

278 {   

279         return (IMsgFilterService) AopContext.currentProxy();   

280 }   

281   

282 public boolean evaluateMsg () {   

283    // 执行此方法将织入切入逻辑   

284 return getThis().evaluateMsg(String message);   

285 }   

286   

287 @MethodInvokeTimesMonitor("KEY_FILTER_NUM")   

288 public boolean evaluateMsg(String message) {   

 不能获取代理类

Java代码  

289 public boolean evaluateMsg () {   

290    // 执行此方法将不会织入切入逻辑   

291 return evaluateMsg(String message);   

292 }   

293   

294 @MethodInvokeTimesMonitor("KEY_FILTER_NUM")   

295 public boolean evaluateMsg(String message) {   

 

 4.2 参考资料

· Java 动态代理机制分析及扩展

· CGlib的官方网站

· ASM官方网站

· JbossAOP

· Java5特性Instrumenttation实践


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值