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 在方法执行前后切入,可以中断或忽略原有流程的执行。
- 公民之间的关系
织入器通过在切面中定义pointcut来搜索目标(被代理类)的JoinPoint(切入点),然后把要切入的逻辑(Advice)织入到目标对象里,生成代理类。
3 AOP的实现机制
本章节将详细介绍AOP有各种实现机制。
动态字节码生成
使用动态字节码生成技术实现AOP原理是在运行期间目标字节码加载后,生成目标类的子类,将切面逻辑加入到子类中,所以使用Cglib实现AOP不需要基于接口。
本节介绍如何使用Cglib来实现动态字节码技术。Cglib是一个强大的,高性能的Code生成类库,它可以在运行期间扩展Java类和实现Java接口,它封装了Asm,所以使用Cglib前需要引入Asm的jar。 清单七:使用CGLib实现AOP
- 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.invokeSuper(target, args);
- //执行织入的日志
- if (method.getName().equals("doSomeThing2")) {
- System.out.println("记录日志");
- }
- return rev;
- }
- }
3.3 自定义类加载器
如果我们实现了一个自定义类加载器,在类加载到JVM之前直接修改某些类的方法,并将切入逻辑织入到这个方法里,然后将修改后的字节码文件交给虚拟机运行,那岂不是更直接。
Javassist是一个编辑字节码的框架,可以让你很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法。这比使用Cglib实现AOP更加高效,并且没太多限制,实现原理如下图:
我们使用系统类加载器启动我们自定义的类加载器,在这个类加载器里加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑,咱们再看看使用Javassist实现AOP的代码:
清单八:启动自定义的类加载器
- //获取存放CtClass的容器ClassPool
- ClassPool cp = ClassPool.getDefault();
- //创建一个类加载器
- Loader cl = new Loader();
- //增加一个转换器
- cl.addTranslator(cp, new MyTranslator());
- //启动MyTranslator的main函数
- cl.run("jsvassist.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();
- }
- }
输出:
- 执行业务逻辑2
- 记录日志
- 执行业务逻辑
其中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加入记录日志的代码。
- public class MyClassFileTransformer implements ClassFileTransformer {
- /**
- * 字节码加载到虚拟机前会进入这个方法
- */
- @Override
- public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
- ProtectionDomain protectionDomain, byte[] classfileBuffer)
- throws IllegalClassFormatException {
- System.out.println(className);
- //如果加载Business类才拦截
- if (!"model/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;
- }
3.4.2 注册转换器
使用premain函数注册字节码转换器,该方法在main函数之前执行。
- public class MyClassFileTransformer implements ClassFileTransformer {
- public static void premain(String options, Instrumentation ins) {
- //注册我自己的字节码转换器
- ins.addTransformer(new MyClassFileTransformer());
- }
- }
3.4.3 配置和执行
需要告诉JVM在启动main函数之前,需要先执行premain函数。首先需要将premain函数所在的类打成jar包。并修改该jar包里的META-INF\MANIFEST.MF 文件。
- Manifest-Version: 1.0
- Premain-Class: bci. MyClassFileTransformer
然后在JVM的启动参数里加上。-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar
3.4.4 输出
执行main函数,你会发现切入的代码无侵入性的织入进去了。
- public static void main(String[] args) {
- new Business().doSomeThing();
- new Business().doSomeThing2();
- }
输出
- model/Business
- sun/misc/Cleaner
- java/lang/Enum
- model/IBusiness
- model/IBusiness2
- 记录日志
- 执行业务逻辑
- 执行业务逻辑2
- java/lang/Shutdown
- java/lang/Shutdown$Lock
从输出中可以看到系统类加载器加载的类也经过了这里。