字节码增强——使用javassist生成一个类的复制类并添加字段和注解

2,424 阅读4分钟

一、Javassist简介

  • 是在 Java 中编辑字节码的类库。
  • 它使 Java 程序能够在运行时定义一个新类, 并在 JVM 加载时修改类文件。

二、和反射的区别

  • 反射的定义:是让程序在运行时,能够动态获取或修改对象的所有成员变量,调用对象的所有方法。
    可以看到反射只可以获取和修改成员变量,可以调用方法,但不能修改方法。如果想要修改方法、新增属性就要使用到字节码增强技术,即:Javassist,可以通过修改字节码的方式来实现对类结构的修改。

三、背景介绍

用到这个技术是因为产品提了一个需求,要做一个excel表格导出,把用户基本信息和用户的成绩拼接在一起导出为一个表格。
因此我们利用Javassist技术copy一个新的类,把基本信息和成绩拼在一起,再加上@SheetColumn注解即可。

四、引入pom

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.26.0-GA</version>
</dependency>

五、newScoreCopyClass方法

这个方法是调用ClassBuilder类的方法。
可以看到先初始化了一个我们自己定义的ScoreClassBulder。resultLists是学生们的答题结果集合,因为我们只需要获取每道题的题目信息,获取第一个list就好,之后遍历把每一道题的名字通过addField方法注入到这个Builder类中,最后调用build方法完成构建。

private Class newScoreCopyClass(List<List<CompResultDto>> resultLists) throws Exception {
    ScoreCopyBuilder subClassBuilder = new ScoreCopyBuilder();
    List<CompResultDto> compResults = resultLists.get(i);
    for (int i = 0; i < compResults.size(); i++) {
        CompResultDto rlt = compResults.get(i);
        String name = "score" + i;
        subClassBuilder.addField(Integer.class, name, rlt.getPracticeName());
    }
    return subClassBuilder.build();
}

六、ScoreCopyBuilder类

这里我们具体来拆解一下ScoreCopyBuilder这个类
根据上面调用方法,核心方法有两个:addField注入属性,build方法完成构建。

6.1 addField方法

首先我们是自己定义了一个FieldWrapper内部类作为实体类,来存储每一个属性的3个信息。 字段类型是Integer,字段名称是之前方法调用时传入的score+i,sheetColumnValue也就是生成excel表格的表头内容。
可以看到这个方法只是把新增字段信息封装存入了list中,真正的构建过程应该是在build的时候发生。

    List<FieldWrapper> fieldWrappers = Lists.newArrayList();
    
    /**
     * 添加一个Field
     *
     * @param fieldType        字段类型
     * @param fieldName        字段名
     * @param sheetColumnValue 注解SheetColumn的value值
     * @return 返回ScoreCopyBuilder对象
     */
    public ScoreCopyBuilder addField(@NonNull Class fieldType,
                                     @NonNull String fieldName,
                                     @NonNull String sheetColumnValue) {
        final FieldWrapper fieldWrapper = new FieldWrapper(
                fieldType, fieldName, sheetColumnValue);
        if (fieldWrappers.contains(fieldWrapper)) {
            throw new RuntimeException("不能重复声明相同的 field name!");
        }
        fieldWrappers.add(fieldWrapper);
        return this;
    }
    
    @ToString
    @EqualsAndHashCode
    @AllArgsConstructor
    @FieldDefaults(level = AccessLevel.PRIVATE)
    private class FieldWrapper {
        @EqualsAndHashCode.Exclude
        Class fieldType;
        String fieldName;
        @EqualsAndHashCode.Exclude
        String sheetColumnValue;
    }

6.2 build方法

可以看到核心的build方法做了如下事情:

  • 首先做了类的命名,这里是定义了一个私有方法,做的事情就是把新增属性的名字拼接起来,为的是之后如果属性完全相同可以直接从对象池中复用,不需要重新构建。
  • 之后是从缓存池中查找,通过名字来查找,如果相同则直接return。
  • 接着从classPool拷贝一个我们定义好的类,并重新命名。
  • 之后我们向复制的类中新增属性,把我们之前注入新增属性的集合List进行遍历,依次将属性和注解添加到ctClass对象中。
  • 返回Class类对象,完成类的加载。
    public Class build() throws Exception {
        // 生成子类的名
        final String subClassName = getSubClassName(this.fieldWrappers);

        // 当前类的类加器对象
        final var launchedURLClassLoader = this.getClass().getClassLoader();

        // 首先从classPool中的缓存池中查找
        final var existClass = loadClassFromPool(subClassName);
        if (Objects.nonNull(existClass)) {
            return existClass;
        }

        // 拷贝一个类并重命名
        try {
            ctClass = classPool.getAndRename(SUPER_CLASS_NAME, subClassName);
        } catch (RuntimeException e) {
            log.error("getAndRename Error: ", e);
        }
        constPool = ctClass.getClassFile().getConstPool();

        // 添加所有的字段到CtClass中
        addAllFieldToCtClass();

        // 类加载
        MonkeyClassLoader monkeyClassLoader = new MonkeyClassLoader(
                launchedURLClassLoader, ctClass.toBytecode());
        Class<?> klass = monkeyClassLoader.loadClass(subClassName);

        // 去除引用
        ctClass = null;
        monkeyClassLoader = null;

        return klass;
    }

6.3 ScoreCopyBuilder类代码

整体ScoreCopyBuilder类代码如下:

/**
 * 使用Javassit动态生成一个ScoreDto类的复制类。
 *
 * @author KD
 */
@Slf4j
@FieldDefaults(level = AccessLevel.PRIVATE)
public class ScoreCopyBuilder {

    static final String SUPER_CLASS_NAME = "com.test.competition.dto.resp.ScoreDto";
    static final String SUB_CLASS_NAME_PREFIX = "com.test.competition.dto.resp.CompScr";
    static final String NEW_FIELD_ANNOTATION_NAME = SheetColumn.class.getName();
    static final String NEW_FIELD_ANNOTATION_VALUE = "value";

    /**
     * CtClass对象池
     */
    ClassPool classPool;

    /**
     * 通过ClassPool生成的类
     */
    CtClass ctClass;

    /**
     * 新的类文件的常量池
     */
    ConstPool constPool;

    /**
     * 新加的字段
     */
    List<FieldWrapper> fieldWrappers = Lists.newArrayList();

    public ScoreCopyBuilder() {
        classPool = ClassPool.getDefault();
    }

    /**
     * 添加一个Field
     *
     * @param fieldType        字段类型
     * @param fieldName        字段名
     * @param sheetColumnValue 注解SheetColumn的value值
     * @return 返回ScoreCopyBuilder对象
     */
    public ScoreCopyBuilder addField(@NonNull Class fieldType,
                                     @NonNull String fieldName,
                                     @NonNull String sheetColumnValue) {
        final FieldWrapper fieldWrapper = new FieldWrapper(
                fieldType, fieldName, sheetColumnValue);
        if (fieldWrappers.contains(fieldWrapper)) {
            throw new RuntimeException("不能重复声明相同的 field name!");
        }
        fieldWrappers.add(fieldWrapper);
        return this;
    }

    /**
     * 创建一个ScoreDto类的复制类
     *
     * @return 返回一个加载后的Class对象
     * @throws Exception
     */
    public Class build() throws Exception {
        // 生成子类的名
        final String subClassName = getSubClassName(this.fieldWrappers);

        // 当前类的类加器对象
        final var launchedURLClassLoader = this.getClass().getClassLoader();

        // 首先从classPool中的缓存池中查找
        final var existClass = loadClassFromPool(subClassName);
        if (Objects.nonNull(existClass)) {
            return existClass;
        }

        // 拷贝一个类并重命名
        try {
            ctClass = classPool.getAndRename(SUPER_CLASS_NAME, subClassName);
        } catch (RuntimeException e) {
            log.error("getAndRename Error: ", e);
        }
        constPool = ctClass.getClassFile().getConstPool();

        // 添加所有的字段到CtClass中
        addAllFieldToCtClass();

        // 类加载
        MonkeyClassLoader monkeyClassLoader = new MonkeyClassLoader(
                launchedURLClassLoader, ctClass.toBytecode());
        Class<?> klass = monkeyClassLoader.loadClass(subClassName);

        // 去除引用
        ctClass = null;
        monkeyClassLoader = null;

        return klass;
    }

    @ToString
    @EqualsAndHashCode
    @AllArgsConstructor
    @FieldDefaults(level = AccessLevel.PRIVATE)
    private class FieldWrapper {
        @EqualsAndHashCode.Exclude
        Class fieldType;
        String fieldName;
        @EqualsAndHashCode.Exclude
        String sheetColumnValue;
    }

    private String getSubClassName(List<FieldWrapper> fieldWrappers) {
        StringBuffer buffer = new StringBuffer();
        for (FieldWrapper fieldWrapper : fieldWrappers) {
            buffer.append(fieldWrapper.toString());
        }
        final String md5 = DigestUtils.md5DigestAsHex(
                buffer.toString().getBytes(Charsets.UTF_8));
        return SUB_CLASS_NAME_PREFIX + md5;
    }

    private Class loadClassFromPool(String classname) {
        try {
            var subCtClass = classPool.getCtClass(classname);
            if (subCtClass != null && subCtClass.isFrozen()) {
                log.info("在ClassPool的Cache中找到这个类,直接加载它!");
                var loader = new MonkeyClassLoader(
                        this.getClass().getClassLoader(), subCtClass.toBytecode());
                return loader.loadClass(classname);
            }
        } catch (NotFoundException e) {
            log.info("没有在ClassPool的Cache中找到这个类,开始构建&加载...");
        } catch (IOException e) {
            log.error(ExceptionUtils.getStackTrace(e));
        } catch (CannotCompileException e) {
            log.error(ExceptionUtils.getStackTrace(e));
        } catch (ClassNotFoundException e) {
            log.error(ExceptionUtils.getStackTrace(e));
        }
        return null;
    }

    private void addAllFieldToCtClass() throws CannotCompileException, NotFoundException {
        for (FieldWrapper fieldWrapper : fieldWrappers) {
            final String fieldTypeName = fieldWrapper.fieldType.getName();
            final String fieldName = fieldWrapper.fieldName;
            // 创建一个CtField
            CtField newCtField = new CtField(
                    classPool.getCtClass(fieldTypeName), fieldName, ctClass);
            newCtField.setModifiers(Modifier.PRIVATE);
            final FieldInfo fieldInfo = newCtField.getFieldInfo();

            // 属性附上注解
            AnnotationsAttribute fieldAttr = new AnnotationsAttribute(
                    constPool, AnnotationsAttribute.visibleTag);
            Annotation fieldAnno = new Annotation(NEW_FIELD_ANNOTATION_NAME, constPool);
            fieldAnno.addMemberValue(NEW_FIELD_ANNOTATION_VALUE,
                    new StringMemberValue(fieldWrapper.sheetColumnValue, constPool));
            fieldAttr.addAnnotation(fieldAnno);
            fieldInfo.addAttribute(fieldAttr);

            // 最后给这个子类添加字段
            ctClass.addField(newCtField);

            // 为字段添加getter和setter
            ctClass.addMethod(CtNewMethod.getter(getter(fieldName), newCtField));
            ctClass.addMethod(CtNewMethod.setter(setter(fieldName), newCtField));
        }
    }

    private String getter(@NonNull String fieldName) {
        return "get" + StringUtils.capitalize(fieldName);
    }

    private String setter(@NonNull String fieldName) {
        return "set" + StringUtils.capitalize(fieldName);
    }

}