Javassist动态编程

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态”AOP”框架。

项目地址

https://github.com/jboss-javassist/javassist

http://jboss-javassist.github.io/javassist

Javassist动态编程

要想将编译时不存在的类在运行时动态创建并加载,通常有两种策略:

  1. 动态编译
  2. 动态生成二进制字节码(.class)

对于第二种策略,实际上已经有诸多比较成熟的开源项目提供支持,如CGLib、ASM、Javassist等。这些开源项目通常都具备两方面的功能:

  1. 动态创建新类或新接口的二进制字节码
  2. 动态扩展现有类或接口的二进制字节码

我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是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));
}
}

特别注意:

  1. Javassist不支持要创建或注入的类中存在泛型参数
  2. Javassist对@类型的注解(Annotation)只支持查询,不支持添加或修改

参考:


重点来了

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!