
字节码技术在我们常见的各大框架中都有用到. 这篇文章我们将讲解 ASM 在 cglib 和 fastjson 上的实际使用案例。
如果说 ASM 是字节码改写事实上的标准,那么可以说 cglib 则是动态代理事实上的标准。cglib 是一个强大的、高性能的代码生成库,被大量框架使用

有这样一个 Person 类,想在 doJob 调用前和调用后分别记录一些日志
public class Person {
public void doJob(String jobName) {
System.out.println("who is this class: " + getClass());
System.out.println("doing job: " + jobName);
}
}
我们可以使用 JDK 动态代理来实现,不过介于 JDK 动态代理有个明显的缺点(需要目标对象实现一个或多个接口),在这里重点介绍 cglib 的实现方案。一个典型的实现方案是实现一个 net.sf.cglib.proxy.MethodInterceptor 接口,用来拦截方法调用。这个接口只有一个方法:public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable;
这个方法的第一个参数 obj 是代理对象,第二个参数 method 是拦截的方法,第三个参数是方法的参数,第四个参数 proxy 用来调用父类的方法。MethodInterceptor 作为一个桥梁连接了目标对象和代理对象

cglib 代理的核心是 net.sf.cglib.proxy.Enhancer类,它用于创建一个 cglib 代理。这个类有一个静态方法public static Object create(Class type, Callback callback),该方法的第一个参数 type 指明要代理的对象类型,第二个参数 callback 是要拦截的具体实现,一般都会传入一个 MethodInterceptor 的实现
public static void main(String[] _args) {
MethodInterceptor interceptor = new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println(">>>>>before intercept");
Object o = methodProxy.invokeSuper(obj, args);
System.out.println(">>>>>end intercept");
return o;
}
};
Person person = (Person) Enhancer.create(Person.class, interceptor);
person.doJob("coding");
}
运行上面的代码输出:
>>>>>before intercept
who is this class: class Person?EnhancerByCGLIB?a1da8fe5
doing job: coding
>>>>>end intercept
可以用设置系统变量让 cglib 输出生成的文件System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/path/to/cglib-debug-location");

核心类是 Person?EnhancerByCGLIB?a1da8fe5.class,这个类的反编译以后的代码如下
public class Person?EnhancerByCGLIB?a1da8fe5 extends Person implements Factory {
public final void doJob(String jobName) {
MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_0;
methodInterceptor.intercept(this, CGLIB$doJob$0$Method, new Object[]{jobName}, CGLIB$doJob$0$Proxy);
}
}
可以看到 cglib 生成了一个 Person 的子类,实现了 doJob 方法,此方法会调用 MethodInterceptor 的 intercept 函数,这个函数先输出 ">>>>>before intercept" 然后调用父类(也即真正的 Person 类)的 doJob 的方法,最后输出 ">>>>>end intercept"
fastjson 是目前 java 语言中最快的 json 库,比自称最快的 jackson 速度要快。fastjson 库内置 ASM,基于 objectweb asm 3.3 改造,只保留必要的部分不到 2000 行代码,通过 ASM 自动生成序列号、反序列化字节码,减少反射开销,理论上可以提高 20% 的性能。如果不用反射,一个 json 反序列化要怎么样来做呢?下面写了一个最简单粗暴的例子,来反序列化下面的 json 字符串
{
"id": "A10001",
"name": "Arthur.Zhang",
"score": 100
}
对应 Java bean
public static class MyBean {
public String id;
public String name;
public Integer score;
}
假定不考虑嵌套,特殊字符的情况,不做语法解析的情况下,可以这么来写
public static void main(String[] args) {
String json = "{ \"id\": \"A10001\", \"name\": \"Arthur.Zhang\", \"score\": 100 }";
// 去掉头尾的 {}
String str = json.substring(1, json.length() - 1);
// 用 "," 分割字符串
String[] fieldStrArray = str.split(",");
MyBean bean = new MyBean();
for (String item : fieldStrArray) {
// 分隔 key value
String[] parts = item.split(":");
String key = parts[0].replaceAll("\"", "").trim();
String value = parts[1].replaceAll("\"", "").trim();
// 通过反射获取字段对应的 field
Field field = MyBean.class.getDeclaredField(key);
// 根据字段类型通过反射设置字段的值
if (field.getType() == String.class) {
field.set(bean, value);
} else if (field.getType() == Integer.class) {
field.set(bean, Integer.valueOf(value));
}
}
System.out.println(bean);
}
可以看到获取获取字段 field、设置字段值都需要通过反射的方式。那么 fastjson 是怎么解决反射低效的问题的呢?通过调试的方式,把 fastjson 生成的字节码写入到文件中。针对 MyBean,fastjson 使用 ASM 为它生成了一个反序列化的类,里面硬编码了处理序列化需要用到的所有可能场景,不再需要任何反射相关的代码。结合创新的 sort field fast match 算法,速度更上一层楼。下面是通过阅读字节码精简以后的 Java 代码。
public class FastjsonASMDeserializer_1_MyBean extends JavaBeanDeserializer {
public char[] id_asm_prefix__ = "\"id\":".toCharArray();
public char[] name_asm_prefix__ = "\"name\":".toCharArray();
public char[] score_asm_prefix__ = "\"score\":".toCharArray();
@Override
public Object deserialze(DefaultJSONParser parser, Type type, Object fieldName, int features) {
JSONLexerBase lexer = (JSONLexerBase) parser.lexer;
MyTest.MyBean localMyBean = new MyTest.MyBean();
String id = lexer.scanFieldString(this.id_asm_prefix__);
if (lexer.matchStat > 0) {
localMyBean.id = id;
}
String name = lexer.scanFieldString(this.name_asm_prefix__);
if (lexer.matchStat > 0) {
localMyBean.name = name;
}
Integer score = lexer.scanFieldInt(this.score_asm_prefix__);
if (lexer.matchStat > 0) {
localMyBean.score = score;
}
return localMyBean;
}
}
通过上面的两个例子,我们可以看到 ASM 字节码技术在底层库上的强大。可能每天写业务代码不会需要使用这些底层的优化,但是当我们想造一个轮子,想读懂开源代码背后的核心时,都不得不深入的去学习了解这部分知识,很难,但很值得。
这篇文章我们主要讲解了 ASM 字节码改写技术在 cglib 和 fastjson 上的应用,一起来回顾一下要点: