一、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);
}
}