前言

Javassist作为一个字节码生成工具,在链路追踪、性能分析等领域都有广泛的应用。不过官方文档只介绍了简单的用法,部分高级特性比如泛型显示、参数名称保留等没有详细介绍,同时网上的相关资料也比较缺乏。本文主要是结合开发过程中对Javassist的一些使用经验,总结了一下Javassist的基本用法和高级特性。

基本用法

Javassist对Java的各种概念进行了一层抽象,比如CtClass代表Class、CtMethod代表Method、CtField代表Field等。ClassPool是CtClass的容器,我们创建类、修改类都需要通过ClassPool。

新增类

示例

@Test
public void testCreateClass() throws Exception {
    //获取ClassPool实例
    ClassPool classPool = ClassPool.getDefault();
    //指定类名
    CtClass carCtClass = classPool.makeClass("com.demo.Car");

    //新增字段
    CtField ctField1 = CtField.make("private String brand = \"world\";", carCtClass);
    carCtClass.addField(ctField1);

    //创建方法形式1
    CtMethod helloMethod1 = CtMethod.make("public void run(){System.out.println(\"---run---\");}", carCtClass);
    carCtClass.addMethod(helloMethod1);

    //创建方法形式2
    CtMethod helloMethod2 = CtNewMethod.make("public void stop(){System.out.println(\"---stop---\");}", carCtClass);
    carCtClass.addMethod(helloMethod2);

    //创建方法形式3
    CtMethod helloMethod3 = CtNewMethod.make(Modifier.PUBLIC, CtClass.voidType, "slow", null, null, "{System.out.println(\"---slow---\");}", carCtClass);
    carCtClass.addMethod(helloMethod3);

    //添加构造器
    CtConstructor ctConstructor = CtNewConstructor.make(new CtClass[]{classPool.getCtClass(String.class.getName())}, null, carCtClass);
    ctConstructor.setBody("{ $0.brand = $1; }");
    carCtClass.addConstructor(ctConstructor);

    //添加默认构造器
    carCtClass.addConstructor(CtNewConstructor.defaultConstructor(carCtClass));

    //将方法写入文件
    carCtClass.writeFile("/home/lxd/test");
}

执行上面的代码,我们就可以在/home/lxd/test/com/demo/目录下得到一个Car.class,将此文件拖入Idea,可以看到生成的代码如下:

说明

  • 字段新增可以直接 new CtField(),也可以通过CtField.make()来进行操作,方法以及构造函数的操作新增类似
  • 方法和构造函数还分别有CtNewMethodCtNewConstructor,这两个类里面封装了很多有用的静态方法,可以简化操作,比如CtNewMethod内封装有getter以及setter的快捷方法,CtNewConstructor内封装有defaultConstructor等方法
  • 在javassist中,$0代表this,$1 $2 ...分别对应方法的具体参数,这些信息可以从官方文档中看到。

编辑类

示例

@Test
public void modifyClass() throws Exception {

    ClassPool classPool = ClassPool.getDefault();
    CtClass ctClass = classPool.get("com.example.Hello");
    CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello", new CtClass[]{classPool.getCtClass(String.class.getName())});
    ctMethod.insertBefore("System.out.println(\"---before---\");");
    ctMethod.insertAfter("System.out.println(\"---after---\");");
    //完全替换method内容
    //ctMethod.setBody("{}");

    ctClass.writeFile("/home/lxd/test");
}

通过上面的代码,我们可以为sayHello方法前后分别插入相关的日志打印功能。

说明

  • CtMethodinsertBeforeinsertAfter方法,可以很方便的进行代码插桩。
  • 将一个class类加载为CtClass后,我们就可以用CtField、CtMethod相关的抽象进行代码修改工作。
  • ClassPool.getDefault()默认和jvm有相同的类检索路径,我们可以通过pool.insertClassPath("xxxx");的形式新增加类检索路径。

高级特性

如果我们想用一些比较高级的特性,比如添加注解、泛型展示等,需要对类文件有一个大概的了解。通过对类文件结构的了解,我们可以很方便的从Javassist中找到对应的API。同时有一点需要注意:大部分高级操作都有相对来说比较简便、封装程度比较高的API,不用直接用Javassist的Bytecode类库。Bytecode相对来说较底层,操作的话对字节码了解水平要求较高。

javassist.jar类文件结构如上图,相关class文件信息都可以从上面的两个包中找到相关抽象。

类文件

ClassFile {
   u4             magic;
   u2             minor_version;
   u2             major_version;
   u2             constant_pool_count;
   cp_info        constant_pool[constant_pool_count-1];
   u2             access_flags;
   u2             this_class;
   u2             super_class;
   u2             interfaces_count;
   u2             interfaces[interfaces_count];
   u2             fields_count;
   field_info     fields[fields_count];
   u2             methods_count;
   method_info    methods[methods_count];
   u2             attributes_count;
   attribute_info attributes[attributes_count];
}

这里给出类文件的基本结构,详细各种属性,可以参考jvm规范

示例类

接下来的高级特性演示主要针对如下的Hello类:

package com.example;

public class Hello {
    private String lang = "en";

    public void sayHello(String name) {
        System.out.println("Hello " + name);
    }

    public void setLang(String lang) {
        this.lang = lang;
    }
}

添加注解

package com.example;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String file();
}

假设我们想为上面的Hello类分别在类级别以及方法级别添加一个Log注解,我们需要创建出 AnnotationsAttribute , 然后将这个Attribute添加到对应的ClassFile中以及getMethodInfo里面。

@Test
public void addAnnotation() throws Exception {

    ClassPool classPool = ClassPool.getDefault();
    CtClass ctClass = classPool.get("com.example.Hello");
    //获取到常量池
    ConstPool constpool = ctClass.getClassFile().getConstPool();
    
    //创建类级别的注解属性
    AnnotationsAttribute clzAnnotationAttr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
    //类注解
    Annotation clzLogAnnotation = new Annotation(Log.class.getName(), constpool);
    clzLogAnnotation.addMemberValue("file", new StringMemberValue("CLASS_LOG", constpool));
    clzAnnotationAttr.setAnnotations(new Annotation[]{clzLogAnnotation});
    //添加到类属性中
    ctClass.getClassFile().addAttribute(clzAnnotationAttr);

    //创建方法级别的注解属性
    AnnotationsAttribute methodAnnotationAttr = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
    Annotation methodLogAnnotation = new Annotation(Log.class.getName(), constpool);
    methodLogAnnotation.addMemberValue("file", new StringMemberValue("METHOD_LOG", constpool));
    methodAnnotationAttr.setAnnotations(new Annotation[]{clzLogAnnotation});
    //添加到方法属性中
    CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello");
    ctMethod.getMethodInfo().addAttribute(methodAnnotationAttr);

    //方法级别的注解类似
    //ctField.getFieldInfo().addAttribute();

    ctClass.writeFile("/home/lxd/test");
}

执行后,我们就可以看到Hello类添加上了对应的注解。

泛型显示

Java的泛型其实是一个语法糖,在运行态均是Object类型,然后强转为对应的类型。为了使class文件看起来相对有好一些,我们需要通过泛型签名来进行泛型展示。

@Test
public void genericTest() throws Exception {
    ClassPool classPool = ClassPool.getDefault();
    CtClass ctClass = classPool.get("com.example.Hello");
    //添加字段的泛型签名
    CtField ctField = CtField.make("private java.util.List exts;", ctClass);
    ctField.setGenericSignature("Ljava/util/List<Ljava/lang/String;>;");
    ctClass.addField(ctField);

    //添加方法的泛型签名
    CtMethod ctMethod = CtNewMethod.make(" public void setExts(java.util.List exts) {$0.exts = $1;}", ctClass);
    ctMethod.setGenericSignature("(Ljava/util/List<Ljava/lang/String;>;)V");
    ctClass.addMethod(ctMethod);

    ctClass.writeFile("/home/lxd/test");
}

说明

  • 我们创建方法或者字段,如果直接传入泛型,javassist会报错,只能通过添加泛型签名的形式进行泛型的展示。
  • 泛型签名中类的全限定名需要用 /代替.
  • 我们还可以通过SignatureAttribute.ClassSignature以及SignatureAttribute.MethodSignature来简化泛型签名的拼写。

参数名称保留

@Test
public void keepParamName() throws Exception {
    ClassPool classPool = ClassPool.getDefault();
    CtClass ctClass = classPool.get("com.example.Hello");
    ConstPool constPool = ctClass.getClassFile().getConstPool();

    //添加方法
    CtMethod ctMethod = CtNewMethod.make(" public void setExts(java.util.List exts) {}", ctClass);
    ctMethod.setGenericSignature("(Ljava/util/List<Ljava/lang/String;>;)V");

    String[] paramNames = new String[]{"exts"};
    int[] accessFlags = new int[]{0};

    //添加方法名称Attribute
    MethodParametersAttribute parametersAttribute = new MethodParametersAttribute(constPool, paramNames, accessFlags);
    ctMethod.getMethodInfo().addAttribute(parametersAttribute);

    ctClass.addMethod(ctMethod);

    ctClass.writeFile("/home/lxd/test");
}

class文件中默认参数名称会变成var$的形式,原始参数名称不会保留,我们也需要通过添加属性到相关属性表中来保留参数名称。

总结

如果我们只是用来代码插桩或者不需要查看class文件的场景,那么javassist的官方文件已经满足大部分需要了。如果需要更深入的应用,则需要了解Javassist的源码。