转载

操作 Java 字节码

通常对于用 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 文件的,或者使用修改字节码的方式更方便:

  1. 在第三方依赖中加入一些检测数据
  2. AOP 操作,例如 Android 自动埋点统计
  3. 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,进而操作类。

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

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

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

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

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

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 属性赋值。

操作 Java 字节码

原文  https://www.hchstudio.cn/article/2018/e8df/
正文到此结束
Loading...