
本博客主要介绍通过 Javassist、ASM 操作 Java 字节码。
Class 文件是什么
通常对于用 idea 的同学来说,class 文件是直接可以查看的,可以看到像 java 那样的代码。其实 class 文件是一种字节码文件,我们平时在 idea 所看到的,是 idea 自动反编译后的结果。如果把 class 文件用 sublime 打开,就会看到许多字节码,而不是 Java 代码了。像这样:
- cafe babe 0000 0034 0017 0100 1163 6e2f
- 6863 6873 7475 6469 6f2f 5573 6572 0700
- 0101 0010 6a61 7661 2f6c 616e 672f 4f62
- 6a65 6374 0700 0301 0004 6e61 6d65 0100
- ......
Class文件是一组以 8 位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在 Class 文件中,中间无任何分隔符。
我们这里所说的操作 Java 字节码,就是操作修改 class 文件内容。
Why
同学们可能会有这样一个疑问,为什么要操作 Java 字节码,直接改 java 文件不是很好吗?
很多情况下是无法操作的 java 文件的,或者使用修改字节码的方式更方便:
- 在第三方依赖中加入一些检测数据
- AOP 操作,例如 Android 自动埋点统计
- Spring 框架的 AOP 操作使用 ASM 操作 Java 字节码
总的来说,可以更方便开发,也同时了解一些底层的原理。
Javassist
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba(千叶 滋)所创建的。它已加入了开放源代码 JBoss 应用服务器项目,通过使用Javassist对字节码操作为 JBoss 实现动态”AOP”框架。
导包
- compile group: 'org.javassist', name: 'javassist', version: '3.23.1-GA'
类搜索路径版本号可能不是最新的,想要最新的话查找 Maven 仓库获取最新的版本号即可。
通过 ClassPool.getDefault() 获取的 ClassPool 使用 JVM 的类搜索路径。如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径。
下面的例子中,pool 代表一个 ClassPool 对象:
- pool.insertClassPath(new ClassClassPath(this.getClass()));
上面的语句将 this 指向的类添加到 pool 的类加载路径中。你可以使用任意 Class 对象来代替 this.getClass(),从而将 Class 对象添加到类加载路径中。传参支持 ClassPath、URLClassPath、ByteArrayClassPath 类型。
编辑
- // 创建 User 类
- CtClass ctClass = classPool.makeClass("cn.hchstudio.User");
- // 获取 String 类
- CtClass CtString = classPool.get("java.lang.String");
通过 makeClass 和 get 方法可以分别创建、获取 CtClass,进而操作类。
上面的语句是创建一个变量,new CtField 中分别传入类型、名称、ctClass。setModifiers 设置变量修饰符;addField 表示把变量加入到这个类中。
- CtField name = new CtField(CtString, "name", ctClass);
- name.setModifiers(Modifier.PRIVATE);
- ctClass.addField(name);
对于已存在的方法,可以使用 insertBefore、insertAfter 方法插入到方法函数之后或之后。Javassist 有一个简单除暴的新增方法方式,就是直接把要写的 java 代码变为字符串,之后 Javassist 便可自动完成代码校验,转为字节码的过程。
- ctMethod.insertBefore("System.out.println(\"lalala\");");
- ctMethod.insertAfter("System.out.println(\"lalala\");");
一个栗子
举一个栗子,这里通过 Javassist 生成一个 User 类,其中包括 name、sex 属性,并有其 set、get 方法。并且输出到 ./out/production/classes 目录下。
- ClassPool classPool = ClassPool.getDefault();
-
- try {
- CtClass ctClass = classPool.makeClass("cn.hchstudio.User");
-
- CtClass CtString = classPool.get("java.lang.String");
-
- CtField name = new CtField(CtString, "name", ctClass);
- name.setModifiers(Modifier.PRIVATE);
- ctClass.addField(name);
- CtField sex = new CtField(CtString, "sex", ctClass);
- sex.setModifiers(Modifier.PRIVATE);
- ctClass.addField(sex);
-
- CtMethod setName = new CtMethod(CtClass.voidType, "setName",
- new CtClass[]{CtString}, ctClass);
- setName.setModifiers(Modifier.PUBLIC);
- setName.setBody("name = $1;");
- ctClass.addMethod(setName);
- CtMethod getName = new CtMethod(CtString, "getName",
- new CtClass[]{}, ctClass);
- getName.setModifiers(Modifier.PUBLIC);
- getName.setBody("return name;");
- ctClass.addMethod(getName);
- CtMethod setSex = CtMethod.make("public void setSex(java.lang.String sex){" +
- "this.sex = sex;" +
- "}", ctClass);
- ctClass.addMethod(setSex);
- CtMethod getSex = new CtMethod(CtString, "getSex",
- new CtClass[]{}, ctClass);
- getSex.setModifiers(Modifier.PUBLIC);
- getSex.setBody("return sex;");
- ctClass.addMethod(getSex);
-
- ctClass.writeFile("./out/production/classes");
- } catch (Exception e) {
- System.out.println(e.toString());
- e.printStackTrace();
- }
ASM
ASM 也是一个操作 Java 字节码的框架,相比于 Javassist,它更加底层、轻量级、速度也快,不过在编写代码的时候可能容易出错,它需要我们直接写 Java 字节码。
Java jdk 自带了 ASM 的依赖,在 rt.jar!/jdk/internal/org/objectweb/asm 下。
Android 环境下则需要自己导入依赖,因为 Android 去掉了 rt.jar!/jdk 包。
编辑
ASM 编辑代码则比较复杂,需要对汇编有一定了解的同学才可以。
通常的方式是我们需要用 java 写出一个想要自动生成的类,然后查看他的 class 字节码
- mv = cw.visitMethod(ACC_PUBLIC, "setName", "(Ljava/lang/String;)V", null, null);
- mv.visitCode();
- mv.visitVarInsn(ALOAD, 0);
- mv.visitVarInsn(ALOAD, 1);
- mv.visitFieldInsn(PUTFIELD, "cn/hchstudio/User", "name", "Ljava/lang/String;");
- mv.visitInsn(RETURN);
- mv.visitMaxs(2, 2);
- mv.visitEnd();
这里改出一段示例代码,意思为新建一个 setName 方法,并给 name 属性赋值。