68

浅析 AOP 实现原理

 6 years ago
source link: http://mp.weixin.qq.com/s/A6L81DlT0CC-NdZ2kcRscQ?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1、AOP概述

AOP Aspect Oriented Programing 面向切面编程 aop作为一种设计理念,拦截方法执行前后,它利用一种称为"横切"的技术,剖解开封装的对象内部, 并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面", 简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码, 降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

Joinpoint(连接点):所谓连接点是指那些被拦截到的点。对哪些方法进行拦截,拦截后怎么处理。

Pointcut(切入点):通过指定,比如指定名称,正则表达式过滤, 指定某个/些连接点, 切点描绘了在 何地做

Advice(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)何时做什么

Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field.

Weaving(织入):将切面应用到目标对象的过程

Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类

Aspect(切面): 是切入点和通知(引介)的结合 何时何地做什么

2、AOP底层实现

JDK动态代理实现

public interface Aop {
    public String getClassName();
}
public class AopImpl implements Aop {	
    @Override    
   public String getClassName() {
    return this.getClass().getName();    } }
public class AopProxy implements InvocationHandler {	
   private Aop target;
   public AopProxy(Aop target){
       this.target = target;    }
   @Override  
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {    System.out.println("方法执行前输出");    Object object = method.invoke(target, args);    System.out.println("方法执行后输出");
   return object;    }
   public  Aop createProxy(){
       return (Aop) Proxy.newProxyInstance(target.getClass().getClassLoader(),
       target.getClass().getInterfaces(), this);    } }
Aop aopProxy = new AopProxy(new AopImpl()).createProxy();
aopProxy.getClassName();

动态代理原理的核心就是如何生成代理类查看源码发现主要代码有三行:

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

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

// 缓存的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);   }



代理类的生成主要是以下两行代码:

//生成代理类的字节码文件并保存到硬盘中(默认不保存到硬盘)   
proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);   
//使用类加载器将字节码加载到内存中   
proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);

代理类的生成过程:



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

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

 localObject1 = this.interfaces[i].getMethods();    for (int k = 0; k < localObject1.length; k++) {       addProxyMethod(localObject1[k], this.interfaces[i]);    }   }  

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

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

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

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

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

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

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

localFileOutputStream.write(this.val$classFile);

通过以上分析,动态代理为我们生成的代理类 getClassName方法体改为调用AopProxy的invoke方法 代理类代码如下:



public class AopImpl implements Aop {

    private AopProxy h;

    public ProxyClass(AopProxy h){

       this.h = h;

} @Override

public String getClassName() {

      try {

  Method   m = (h.target).getClass().getMethod("getClassName", null);    h.invoke(this, m, null); } catch (Throwable e) { e.printStackTrace();

}

        return null;  

} }

第一代理类必须实现一个接口,如果没实现接口会抛出一个异常。第二性能影响,因为动态代理使用反射的机制实现的,首先反射肯定比直接调用要慢,经过测试大概每个代理类比静态代理多出10几毫秒的消耗。其次使用反射大量生成类文件可能引起Full GC造成性能影响,因为字节码文件加载后会存放在JVM运行时区的方法区(或者叫持久代)中,当方法区满的时候,会引起Full GC,所以当你大量使用动态代理时,可以将持久代设置大一些,减少Full GC次数。 (jdk8好移除了方法区用元数据区代替,元数据区是堆外直径内存,直径消耗本地内存分配)

动态字节码

原理是 读入 现有Class 的字节码, 生成结构树, 通过二次开发接口, 提供增强功能, 然后再把字节码写入

  • 继承 ClassAdapter 获取Class内域和方法的访问器 ClassVisitor

  • 继承 MethodAdapter 实现方法的覆盖

  • 通过 ClassReader 和 ClassWriter 修改原先的实现类源码

CGLib (Code Generation Library)

因为 asm可读性较差, 产生了更易用的框架 cglib,其底层使用的是 asm 接口;

a、Enhancer 类似于 动态代理的 Proxy 提供代理的对象

b、MethodInterceptor 类似于 动态代理的 InvocationHandler 提供拦截逻辑

对应动代理的例子, 就是类似如下代码

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(AopImpl.class);
enhancer.setCallback(new AopProxy());
AopImpl aop = (AopImpl)enhancer.create();
aop.aop();

javasist

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

//类加载监听器核心代码

//前面可以加入过滤逻辑 定义加载哪些类特殊处理

CtClass  cc = pool.get(classname);  

//获得指定方法名的方法  
CtMethod m = cc.getDeclaredMethod("getClassName");   //在方法执行前插入代码  
m.insertBefore("{ System.out.println(\"记录日志\"); }");

自定义类加载器在性能上要优于动态代理和CGLIB,不会产生新类,不过如果其他的类加载器来加载类的话,这些拦截逻辑就不会被织入了

Instrumentation

Instrumentation(指令改写), Java提供的这个功能开口, 可以向java类文件添加字节码信息。可以用来辅助日志信息,记录方法执行的次数,或者建立代码覆盖测试的工具。通过该方式,不会对现有应用功能产生更改。

a、静态方式 PreMain

manifest文件 指定 Premain-Class 实现类 实现 ClassFileTransformer 接口的transform 方法,transform方法可以采用javassist加入要切入的逻辑(直接操作移位byte实在是太不方便了)

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

public class MyClassFileTransformer implements ClassFileTransformer {   
    public static void premain(String options, Instrumentation ins) {   
        //注册我自己的字节码转换器   
        ins.addTransformer(new MyClassFileTransformer());   
}   
}

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

Manifest-Version: 1.0   
Premain-Class: bci. MyClassFileTransformer

然后在jvm启动参数里加上 -javaagent:{你打的jar包路径}

b、动态方式 AgentMain manifest文件 指定Agent-Class 实现类 Attach Tools API 把代码绑定到具体JVM上

这个功能正常的用途是JVM管理的一个功能,可以监控系统运行的;原理上也可以用在系统增强上面,不过premain和agentmain是针对整个虚拟机的钩子(hook),每个类装载进来都会执行,所以很少使用在功能增强上

AOP的概念和知识点很多, 只要能理解其用途, 无非就是两个部分 需要何时拦截哪些模块编译期拦截,字节码加载前,字节码加载后。增加哪些功能   缓存代理,缓存方法的返回值下次调用该方法从缓存里取,性能监控,记录日志,权限验证等

部门招聘

高级Java开发工程师

工作职责:

1、负责58同城APP,58同镇等相关后端研发工作;

2、负责基础平台的架构设计,核心代码开发;

3、调研并掌握业内通用技术方案,引入项目迭代,提升研发效率;

职位要求:

1、3年以上Java互联网项目开发经验;

2、Java基础扎实,编码规范,程序具备较高的健壮性,熟悉常用设计模式;

3、对MVC框架、RPC框架、基础服务组件等有深入的研究;

4、掌握Linux环境下的网络编程、多线程编程,数据结构和算法能力良好;

5、对高并发高可用系统设计有深入的实践经验;

6、具有高度的责任心、勇于承担责任,能承受较强的工作压力;

7、积极主动,敢于接受挑战,有较强的团队合作精神;

高级前端研发工程师

工作职责:

1、负责58同城App前端产品研发;

2、负责58同城前端无线产品某一技术方向,人才培养;

3、前端研发所需类库、框架、脚手架搭建;

4、交互模式调研及创新(React,ReactNative);

职位要求:

1、计算机及相关专业本科以上学历;

2、3年以上前端开发经验,负责过复杂应用的前端设计和开发 ;

3、精通web前端技术(js/css/html),熟悉主流框架类库的设计实现、w3c标准,熟悉ES6/7优先;

4、熟悉前端模块化开发方式(commonjs/webpack …);

5、熟悉移动端开发、自适应布局和开发调试工具,熟悉hybrid app开发;

6、掌握一门后端语言(node/java/php...),对前后端合作模式有深入理解;

7、有良好的产品意识和团队合作意识,能够和产品、UI交互部门协作完成产品面向用户端的呈现;

8、有技术理想,致力于用技术去推动和改变前端研发;

9、熟悉Vue/React/ReactNative优先,有BAT等公司经验优先;

高级Android开发工程师

岗位描述:

1、负责58同城App的研发工作;

2、肩负平台化任务(插件框架,Walle,Hybrid,WubaRN) ;

3、维护和开发服务库,公共库的工作;

4、调研Android前端技术;

5、提升开发效率和应用性能;

职位要求:

1、2年以上的Android开发工作经验;

2、精通Java语言,精通Android Studio开发,了解Gradle编译;

3、精通常用算法、数据结构和架构设计;

4、了解Android性能限制及优化方案;

5、了解常用的开源工具:Volley,RxJava,Fresco等等;

6、了解git, maven等等工具;

7、有插件开发经验,Hybrid开发经验,ReactNative开发经验优先;

8、积极主动、喜欢挑战,有强烈的创业精神,能承受高强度的工作压力;

以上如有小伙伴感兴趣,请发送简历到:

[email protected]

Image

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK