操作 Java 字节码

524 阅读6分钟
原文链接: mp.weixin.qq.com

原作者:ChanghuiN版权声明:本文由微信公众号 快乐的飞猪们 所有,未经许可,不得以任何形式专注

本博客主要介绍通过 Javassist、ASM 操作 Java 字节码。

Class 文件是什么

通常对于用 idea 的同学来说,class 文件是直接可以查看的,可以看到像 java 那样的代码。其实 class 文件是一种字节码文件,我们平时在 idea 所看到的,是 idea 自动反编译后的结果。如果把 class 文件用 sublime 打开,就会看到许多字节码,而不是 Java 代码了。像这样: 

1cafe babe 0000 0034 0017 0100 1163 6e2f26863 6873 7475 6469 6f2f 5573 6572 070030101 0010 6a61 7661 2f6c 616e 672f 4f6246a65 6374 0700 0301 0004 6e61 6d65 01005......

Class文件是一组以 8 位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在 Class 文件中,中间无任何分隔符。  我们这里所说的操作 Java 字节码,就是操作修改 class 文件内容。

Why

同学们可能会有这样一个疑问,为什么要操作 Java 字节码,直接改 java 文件不是很好吗? 很多情况下是无法操作的 java 文件的,或者使用修改字节码的方式更方便:

  1. 在第三方依赖中加入一些检测数据

  2. AOP 操作,例如 Android 自动埋点统计

  3. Spring 框架的 AOP 操作使用 ASM 操作 Java 字节码

总的来说,可以更方便开发,也同时了解一些底层的原理。

Javassist

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

导包

1compile group: 'org.javassist', name: 'javassist', version: '3.23.1-GA'

版本号可能不是最新的,想要最新的话查找 Maven 仓库获取最新的版本号即可。

类搜索路径

通过 ClassPool.getDefault() 获取的 ClassPool 使用 JVM 的类搜索路径。如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径。

下面的例子中,pool 代表一个 ClassPool 对象:

1pool.insertClassPath(new ClassClassPath(this.getClass()));

上面的语句将 this 指向的类添加到 pool 的类加载路径中。你可以使用任意 Class 对象来代替 this.getClass(),从而将 Class 对象添加到类加载路径中。传参支持 ClassPath、URLClassPath、ByteArrayClassPath 类型。

编辑

1// 创建 User 类2CtClass ctClass = classPool.makeClass("cn.hchstudio.User");3// 获取 String 类4CtClass CtString = classPool.get("java.lang.String");

通过 makeClass 和 get 方法可以分别创建、获取 CtClass,进而操作类。

1CtField name = new CtField(CtString, "name", ctClass);2name.setModifiers(Modifier.PRIVATE);3ctClass.addField(name);

上面的语句是创建一个变量,new CtField 中分别传入类型、名称、ctClass。setModifiers 设置变量修饰符;addField 表示把变量加入到这个类中。

1CtMethod setSex = CtMethod.make("public void setSex(java.lang.String sex){" +2                    "this.sex = sex;" +3                    "}", ctClass);4ctClass.addMethod(setSex);

Javassist 有一个简单除暴的新增方法方式,就是直接把要写的 java 代码变为字符串,之后 Javassist 便可自动完成代码校验,转为字节码的过程。

对于已存在的方法,可以使用 insertBefore、insertAfter 方法插入到方法函数之后或之后。

1ctMethod.insertBefore("System.out.println(\"lalala\");");2ctMethod.insertAfter("System.out.println(\"lalala\");");

一个栗子

举一个栗子,这里通过 Javassist 生成一个 User 类,其中包括 name、sex 属性,并有其 set、get 方法。并且输出到 ./out/production/classes 目录下。 

 1ClassPool classPool = ClassPool.getDefault(); 2 3try { 4    CtClass ctClass = classPool.makeClass("cn.hchstudio.User"); 5 6    CtClass CtString = classPool.get("java.lang.String"); 7 8    CtField name = new CtField(CtString, "name", ctClass); 9    name.setModifiers(Modifier.PRIVATE);10    ctClass.addField(name);11    CtField sex = new CtField(CtString, "sex", ctClass);12    sex.setModifiers(Modifier.PRIVATE);13    ctClass.addField(sex);1415    CtMethod setName = new CtMethod(CtClass.voidType, "setName",16            new CtClass[]{CtString}, ctClass);17    setName.setModifiers(Modifier.PUBLIC);18    setName.setBody("name = $1;");19    ctClass.addMethod(setName);20    CtMethod getName = new CtMethod(CtString, "getName",21            new CtClass[]{}, ctClass);22    getName.setModifiers(Modifier.PUBLIC);23    getName.setBody("return name;");24    ctClass.addMethod(getName);25    CtMethod setSex = CtMethod.make("public void setSex(java.lang.String sex){" +26            "this.sex = sex;" +27            "}", ctClass);28    ctClass.addMethod(setSex);29    CtMethod getSex = new CtMethod(CtString, "getSex",30            new CtClass[]{}, ctClass);31    getSex.setModifiers(Modifier.PUBLIC);32    getSex.setBody("return sex;");33    ctClass.addMethod(getSex);3435    ctClass.writeFile("./out/production/classes");36} catch (Exception e) {37    System.out.println(e.toString());38    e.printStackTrace();39}

ASM

ASM 也是一个操作 Java 字节码的框架,相比于 Javassist,它更加底层、轻量级、速度也快,不过在编写代码的时候可能容易出错,它需要我们直接写 Java 字节码。 

Java jdk 自带了 ASM 的依赖,在 rt.jar!/jdk/internal/org/objectweb/asm 下。  Android 环境下则需要自己导入依赖,因为 Android 去掉了 rt.jar!/jdk 包。

编辑

ASM 编辑代码则比较复杂,需要对字节码有一定了解的同学才可以。  通常的方式是我们需要用 java 写出一个想要自动生成的类,然后查看他的 class 字节码

1mv = cw.visitMethod(ACC_PUBLIC, "setName", "(Ljava/lang/String;)V", null, null);2mv.visitCode();3mv.visitVarInsn(ALOAD, 0);4mv.visitVarInsn(ALOAD, 1);5mv.visitFieldInsn(PUTFIELD, "cn/hchstudio/User", "name", "Ljava/lang/String;");6mv.visitInsn(RETURN);7mv.visitMaxs(2, 2);8mv.visitEnd();

这里改出一段示例代码,意思为新建一个 setName 方法,并给 name 属性赋值。

关注我们