Javassist介绍
ASM是在指令层次上操作字节码的,通过字节码增强技术-ASM,我们的直观感受是在指令层次上操作字节码的框架实现起来比较晦涩。我们再简单介绍另外一类框架:强调源代码层次操作字节码的框架Javassist
。
Javassist(Java Programming Assistant)
是一个用于在运行时操作字节码的 Java 库,它允许开发人员动态生成、修改和分析 Java 类的字节码。Javassist
提供了一种更高级别的 API,以 Java 代码的方式来操作字节码,而不需要直接操作复杂的字节码指令。这使得动态代码生成和修改变得更加容易和可维护。以下是 Javassist
的一些重要特点和使用方式:
Javassist
允许您在运行时通过编写 Java 代码的方式来生成新的类和方法。这种方式使动态代码生成变得非常直观和易于理解。Javassist
修改已经存在的类的字节码,例如添加、修改或删除字段、方法等。Javassist
提供了一套 API,用于操作类的字节码指令,如创建方法、添加指令、修改参数等。这使得您能够以更高级别的抽象方式来进行字节码操作,而无需深入了解底层字节码细节。Javassist
可以用于实现 AOP(面向切面编程)的功能,通过在方法前后插入代码来实现横切关注点的处理。Javassist
可以帮助您避免使用繁琐的 Java 反射 API,通过生成字节码来直接调用方法和访问字段。Javassist
支持处理不同版本的 Java 字节码,因此您可以在不同的 Java 版本间进行字节码操作。Javassist操作步骤
使用 Javassist
的基本步骤如下:
Javassist
库添加到项目的依赖中。Javassist
的核心类,用于加载和保存类文件。您可以通过 ClassPool 加载要操作的类。从开发视角来看,ClassPool是一张保存CtClass信息的HashTable,key为类名,value为类名对应的CtClass对象。当我们需要对某个类进行修改时,就是通过pool.getCtClass(“className”)方法从pool中获取到相应的CtClass。- CtMethod、CtField:这两个比较好理解,对应的是类中的方法和属性。
下面我们写一个小Demo来展示Javassist
简单、快速的特点。我们依然是对MathUtils中的add()方法做增强,在方法调用前后分别输出”start”和”end”,实现代码如下。我们需要做的就是从pool中获取到相应的CtClass对象和其中的方法,然后执行method.insertBefore和insertAfter方法,参数为要插入的Java代码,再以字符串的形式传入即可,实现起来也极为简单。
首先引用jar包:
org.javassist
javassist
3.28.0-GA
我们还用上篇文章的测试类
public class MathUtils {
public int add(int a, int b) {
return a + b;
}
}
public class JavassistTest {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException, IOException {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("com.demo.bytecode.MathUtils");
CtMethod m = cc.getDeclaredMethod("add");
m.insertBefore("{ System.out.println("start"); }");
m.insertAfter("{ System.out.println("end"); }");
Class c = cc.toClass();
cc.writeFile("/Users/xx/work/spring-demo/");
MathUtils h = (MathUtils) c.newInstance();
int result = h.add(1, 2);
System.out.println(result);
}
}
运行后输出结果为:
我们打开生成的class文件,可以看到已经生成了相关代码:
ClassPool
需要关注的方法:
getDefault
: 返回默认的ClassPool
是单例模式的,一般通过该方法创建我们的ClassPool;appendClassPath
, insertClassPath
: 将一个ClassPath
加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;toClass
: 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass
的toClass
方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;get
, getCtClass
: 根据类路径名获取该类的CtClass对象,用于后续的编辑。CtClass
需要关注的方法:
freeze
: 冻结一个类,使其不可修改;isFrozen
: 判断一个类是否已被冻结;prune
: 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;defrost
: 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost
, 则禁止调用 prune
方法;detach
: 将该class从ClassPool
中删除;writeFile
: 根据CtClass生成 .class
文件;toClass
: 通过类加载器加载该CtClass。上面我们创建一个新的方法使用了CtMethod
类。CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。
CtMethod
中的一些重要方法:
insertBefore
: 在方法的起始位置插入代码;insterAfter
: 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception
;insertAt
: 在指定的位置插入代码;setBody
: 将方法的内容设置为要写入的代码,当方法被 abstract
修饰时,该修饰符被移除;make
: 创建一个新的方法。使用Javassist
写个Bean Copy
的工具
首先定义一个对象转换的接口,生成的转换类实现这个接口
public interface Converter {
/**
* 将一个对象复制到另一个对象
*
* @param from from
* @param to to
*/
void convert(Object from, Object to);
}
实现一个工具类CopyUtil
生成转换器
public class CopyUtil {
private static final ConcurrentHashMap CACHE = new ConcurrentHashMap(32);
private static final AtomicInteger ID = new AtomicInteger();
private static final ClassPool pool = ClassPool.getDefault();
private static final CtClass converterInterface;
static {
try {
converterInterface = pool.getCtClass(Converter.class.getName());
} catch (NotFoundException e) {
throw new RuntimeException(e);
}
}
public static void copy(Object from, Object to) {
Class fromClass = from.getClass();
Class toClass = to.getClass();
Converter converter = getConverter(fromClass, toClass);
converter.convert(from, to);
}
/**
* 从缓存获取converter
*
* @param fromClass 源类
* @param toClass 目标类
* @return 转换器
*/
private static Converter getConverter(Class fromClass, Class toClass) {
ConverterKey key = new ConverterKey(fromClass, toClass);
return CACHE.computeIfAbsent(key, CopyUtil::generateConverter);
}
/**
* 使用javassist生成一个转换器
*
* @param key key
* @return converter
*/
private static Converter generateConverter(ConverterKey key) {
Class fromClass = key.fromClass;
Class toClass = key.toClass;
CtClass converterClass = pool.makeClass("BeanConverter" + ID.getAndIncrement());
try {
converterClass.addInterface(converterInterface);
// 创建一个新的方法
CtMethod convertMethod = CtNewMethod.make(generateMethod(fromClass, toClass), converterClass);
converterClass.addMethod(convertMethod);
Class type = converterClass.toClass(CopyUtil.class.getClassLoader(), CopyUtil.class.getProtectionDomain());
return (Converter) type.newInstance();
} catch (Exception e) {
throw new RuntimeException("- generate converter error", e);
}
}
/**
* 生成转换器方法
*
* @param fromClass 原始类
* @param toClass 目标类
* @return 方法代码
*/
private static String generateMethod(Class fromClass, Class toClass) {
String prefix = "public void convert(Object from, Object to) {n";
// 对象转换
String castFromCode = fromClass.getName() + " a = (" + fromClass.getName() + ") from;n";
String castToCode = toClass.getName() + " b = (" + toClass.getName() + ") to;n";
String postfix = "}n";
// 获取原始类字段
Set fromFields = getFields(fromClass);
// 获取目标类字段
Set toFields = getFields(toClass);
fromFields.retainAll(toFields);
StringBuilder code = new StringBuilder();
for (String field : fromFields) {
field = StringUtils.capitalize(field);
code.append("b.set").append(field).append("(a.get").append(field).append("());n");
}
return prefix + castFromCode + castToCode + code + postfix;
}
/**
* 获取一个类(包含父类)的所有属性
*
* @param type type
* @return 属性list
*/
private static Set getFields(Class type) {
Field[] fields = type.getDeclaredFields();
Set fieldSet = Stream.of(fields).map(Field::getName).collect(toSet());
Class parent = type.getSuperclass();
if (type.equals(Object.class) || parent.equals(Object.class)) {
return fieldSet;
}
Set parentFieldSet = getFields(parent);
fieldSet.addAll(parentFieldSet);
return fieldSet;
}
/**
* 用于缓存的键
*/
@EqualsAndHashCode
@AllArgsConstructor
private static class ConverterKey {
Class fromClass;
Class toClass;
}
}
下面我们测试一下
public static void main(String[] args) {
Person person1 = new Person("zhangsan", 25);
Person person2 = new Person();
CopyUtil.copy(person1, person2);
System.out.println(person2.getName());
System.out.println(person2.getAge());
}
结果正常输出:
总结
总体而言,Javassist
是通过在运行时操作字节码,使用高级别的 API 和类抽象(如 CtClass、CtMethod 等),使得动态代码生成和修改变得更加直观和容易。这种方式使开发人员可以在不直接操作底层字节码指令的情况下,实现对 Java 类的动态操作。