Javassist动态编程 | 伈情的博客
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态”AOP”框架。
项目地址
https://github.com/jboss-javassist/javassist
http://jboss-javassist.github.io/javassist
Javassist动态编程
要想将编译时不存在的类在运行时动态创建并加载,通常有两种策略:
- 动态编译
- 动态生成二进制字节码(.class)
对于第二种策略,实际上已经有诸多比较成熟的开源项目提供支持,如CGLib、ASM、Javassist等。这些开源项目通常都具备两方面的功能:
- 动态创建新类或新接口的二进制字节码
- 动态扩展现有类或接口的二进制字节码
我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是Javassit。
Javassit其实就是一个二方包,提供了运行时操作Java字节码的方法。大家都知道,Java代码编译完会生成.class文件,就是一堆字节码。JVM(准确说是JIT)会解释执行这些字节码(转换为机器码并执行),由于字节码的解释执行是在运行时进行的,那我们能否手工编写字节码,再由JVM执行呢?答案是肯定的,而Javassist就提供了一些方便的方法,让我们通过这些方法生成字节码。
类似字节码操作方法还有ASM。几种动态编程方法相比较,在性能上Javassist高于反射,但低于ASM,因为Javassist增加了一层抽象。在实现成本上Javassist和反射都很低,而ASM由于直接操作字节码,相比Javassist源码级别的api实现成本高很多。几个方法有自己的应用场景,比如Kryo使用的是ASM,追求性能的最大化。而NBeanCopyUtil采用的是Javassist,在对象拷贝的性能上也已经明显高于其他的库,并保持高易用性。实际项目中推荐先用Javassist实现原型,若在性能测试中发现Javassist成为了性能瓶颈,再考虑使用其他字节码操作方法做优化。
总结:CGLib的底层基于ASM实现,是一个高效高性能的生成库;而ASM是一个轻量级的类库,但需要涉及到JVM的操作和指令;相比而言,Javassist要简单的多,完全是基于Java的API,但其性能相比前二者要差一些。在性能要求相对低的场合,Javassist仍然十分有用,如JBoss中就调用了Javassist。
如下的代码是动态创建Java类二进制字节码并通过反射调用的示例,可供参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtField; import javassist.CtNewMethod; import javassist.Modifier; import javassist.NotFoundException; import javassist.CtField.Initializer; public class JavassistGenerator { public static void main(String[] args) throws CannotCompileException, NotFoundException, InstantiationException, IllegalAccessException, ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException { // 创建类 ClassPool pool = ClassPool.getDefault(); CtClass cls = pool.makeClass("cn.ibm.com.TestClass"); // 添加私有成员name及其getter、setter方法 CtField param = new CtField(pool.get("java.lang.String"), "name", cls); param.setModifiers(Modifier.PRIVATE); cls.addMethod(CtNewMethod.setter("setName", param)); cls.addMethod(CtNewMethod.getter("getName", param)); cls.addField(param, Initializer.constant("")); // 添加无参的构造体 CtConstructor cons = new CtConstructor(new CtClass[] {}, cls); cons.setBody("{name = \"Brant\";}"); cls.addConstructor(cons); // 添加有参的构造体 cons = new CtConstructor(new CtClass[] {pool.get("java.lang.String")}, cls); cons.setBody("{$0.name = $1;}"); cls.addConstructor(cons); // 打印创建类的类名 System.out.println(cls.toClass()); // 通过反射创建无参的实例,并调用getName方法 Object o = Class.forName("cn.ibm.com.TestClass").newInstance(); Method getter = o.getClass().getMethod("getName"); System.out.println(getter.invoke(o)); // 调用其setName方法 Method setter = o.getClass().getMethod("setName", new Class[] {String.class}); setter.invoke(o, "Adam"); System.out.println(getter.invoke(o)); // 通过反射创建有参的实例,并调用getName方法 o = Class.forName("cn.ibm.com.TestClass").getConstructor(String.class).newInstance("Liu Jian"); getter = o.getClass().getMethod("getName"); System.out.println(getter.invoke(o)); } } |
特别注意:
- Javassist不支持要创建或注入的类中存在泛型参数
- Javassist对@类型的注解(Annotation)只支持查询,不支持添加或修改
参考:
Javassist学习总结
http://blog.csdn.net/sadfishsc/article/details/9999169javassist学习
http://yonglin4605.iteye.com/blog/1396494Java动态编程初探——Javassist
http://www.cnblogs.com/hucn/p/3636912.html
重点来了
Dubbo为什么使用Javassist替代反射
以下内容摘自Dubbo官网:
Dubbo框架中使用到Javassist ProxyFactory Stable通过字节码生成代替反射,性能比较好(推荐使用) 依赖于javassist.jar包,占用JVM的Perm内存,Perm可能要设大一些:java -XX:PermSize=128m 可用于生产环境 Alibaba
直接上Dubbo核心领域(Proxy)代码,看看怎么使用Javassist的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | /** 核心领域中的代理实现类:com.alibaba.dubbo.common.bytecode.Proxy 通过它实例化服务提供者的接口实现类,也就是具体业务逻辑方法; 不过别着急,这里还没有出现Javassist的影子,注意看其中的: com.alibaba.dubbo.common.bytecode.ClassGenerator 这个是阿里自己根据需要又在Javassist上面封装的一个工具类,直接共Proxy类使用 后面贴上ClassGenerator的部分核心代码,里面有直接调用Javassist动态创建类; */ long id = PROXY_CLASS_COUNTER.getAndIncrement(); String pkg = null; ClassGenerator ccp = null, ccm = null; try { ccp = ClassGenerator.newInstance(cl); Set<String> worked = new HashSet<String>(); List<Method> methods = new ArrayList<Method>(); for(int i=0;i<ics.length;i++) { if( !Modifier.isPublic(ics[i].getModifiers()) ) { String npkg = ics[i].getPackage().getName(); if( pkg == null ) { pkg = npkg; } else { if( !pkg.equals(npkg) ) throw new IllegalArgumentException("non-public interfaces from different packages"); } } ccp.addInterface(ics[i]); for( Method method : ics[i].getMethods() ) { String desc = ReflectUtils.getDesc(method); if( worked.contains(desc) ) continue; worked.add(desc); int ix = methods.size(); Class<?> rt = method.getReturnType(); Class<?>[] pts = method.getParameterTypes(); StringBuilder code = new StringBuilder("Object[] args = new Object[").append(pts.length).append("];"); for(int j=0;j<pts.length;j++) code.append(" args[").append(j).append("] = ($w)$").append(j+1).append(";"); code.append(" Object ret = handler.invoke(this, methods[" + ix + "], args);"); if( !Void.TYPE.equals(rt) ) code.append(" return ").append(asArgument(rt, "ret")).append(";"); methods.add(method); ccp.addMethod(method.getName(), method.getModifiers(), rt, pts, method.getExceptionTypes(), code.toString()); } } if( pkg == null ) pkg = PACKAGE_NAME; // create ProxyInstance class. String pcn = pkg + ".proxy" + id; ccp.setClassName(pcn); ccp.addField("public static java.lang.reflect.Method[] methods;"); ccp.addField("private " + InvocationHandler.class.getName() + " handler;"); ccp.addConstructor(Modifier.PUBLIC, new Class<?>[]{ InvocationHandler.class }, new Class<?>[0], "handler=$1;"); ccp.addDefaultConstructor(); Class<?> clazz = ccp.toClass(); clazz.getField("methods").set(null, methods.toArray(new Method[0])); // create Proxy class. String fcn = Proxy.class.getName() + id; ccm = ClassGenerator.newInstance(cl); ccm.setClassName(fcn); ccm.addDefaultConstructor(); ccm.setSuperClass(Proxy.class); ccm.addMethod("public Object newInstance(" + InvocationHandler.class.getName() + " h){ return new " + pcn + "($1); }"); Class<?> pc = ccm.toClass(); proxy = (Proxy)pc.newInstance(); } |
com.alibaba.dubbo.common.bytecode.ClassGenerator类的核心方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | public Class<?> toClass(ClassLoader loader, ProtectionDomain pd) { if( mCtc != null ) mCtc.detach(); long id = CLASS_NAME_COUNTER.getAndIncrement(); try { CtClass ctcs = mSuperClass == null ? null : mPool.get(mSuperClass); if( mClassName == null ) mClassName = ( mSuperClass == null || javassist.Modifier.isPublic(ctcs.getModifiers()) ? ClassGenerator.class.getName() : mSuperClass + "$sc" ) + id; mCtc = mPool.makeClass(mClassName); if( mSuperClass != null ) mCtc.setSuperclass(ctcs); mCtc.addInterface(mPool.get(DC.class.getName())); // add dynamic class tag. if( mInterfaces != null ) for( String cl : mInterfaces ) mCtc.addInterface(mPool.get(cl)); if( mFields != null ) for( String code : mFields ) mCtc.addField(CtField.make(code, mCtc)); if( mMethods != null ) { for( String code : mMethods ) { if( code.charAt(0) == ':' ) mCtc.addMethod(CtNewMethod.copy(getCtMethod(mCopyMethods.get(code.substring(1))), code.substring(1, code.indexOf('(')), mCtc, null)); else mCtc.addMethod(CtNewMethod.make(code, mCtc)); } } if( mDefaultConstructor ) mCtc.addConstructor(CtNewConstructor.defaultConstructor(mCtc)); if( mConstructors != null ) { for( String code : mConstructors ) { if( code.charAt(0) == ':' ) { mCtc.addConstructor(CtNewConstructor.copy(getCtConstructor(mCopyConstructors.get(code.substring(1))), mCtc, null)); } else { String[] sn = mCtc.getSimpleName().split("\\$+"); // inner class name include $. mCtc.addConstructor(CtNewConstructor.make(code.replaceFirst(SIMPLE_NAME_TAG, sn[sn.length-1]), mCtc)); } } } return mCtc.toClass(loader, pd); } catch(RuntimeException e) { throw e; } catch(NotFoundException e) { throw new RuntimeException(e.getMessage(), e); } catch(CannotCompileException e) { throw new RuntimeException(e.getMessage(), e); } } |
为什么Dubbo使用Javassist动态创建类而不适用反射呢?请看Proxy类中的如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | private static final Map<ClassLoader, Map<String, Object>> ProxyCacheMap = new WeakHashMap<ClassLoader, Map<String, Object>>(); // 这里省略部分代码...... //核心在这里↓↓↓ // get cache by class loader. Map<String, Object> cache; synchronized( ProxyCacheMap ) { cache = ProxyCacheMap.get(cl); if( cache == null ) { cache = new HashMap<String, Object>(); ProxyCacheMap.put(cl, cache); } } Proxy proxy = null; synchronized( cache ) { do { Object value = cache.get(key); if( value instanceof Reference<?> ) { proxy = (Proxy)((Reference<?>)value).get(); if( proxy != null ) return proxy; } if( value == PendingGenerationMarker ) { try{ cache.wait(); }catch(InterruptedException e){} } else { cache.put(key, PendingGenerationMarker); break; } } while( true ); } long id = PROXY_CLASS_COUNTER.getAndIncrement(); |
总结:Dubbo使用Javassist来代替反射很巧妙的使用了Javassist动态通过字节码在消费者中创建了类并实例化,然后放入jvm内存中缓存起来,也就是ProxyCacheMap对象中,在第一次使用的时候通过Javassist动态创建然后放入缓存中,后续就直接从缓存中拿来用了,so~~~除了第一次慢,再使用当然快了,对于这个设计膜拜了!这才符合阿里的特点啊!
Never Give Up!