字节码增强之Javassist
Javassist(Java Programming Assist)是编辑字节码的Java类库,它使Java字节码操作变得简单。通过使用Javassist可以使Java程序在运行时定义一个新的类,并且在JVM加载类文件时修改它。Javassist提供两个级别的API:源码级别和字节码级别。如果使用源码级别的API,我们可以在不知道Java字节码知识的情况下编辑Java类文件,就像我们编写Java源代码一样方便。如果使用字节码级别的API,那么需要我们详细了解Java字节码和类文件格式,因为字节码级别的API允许我们对类文件进行任意修改。
Javassist官方教程:Javassist Tutorial
本篇博客结合官方教程讲解了Javassist的几种用法,以及源码级别API的几个重要的工具类中包含的方法
1. start
要使用Javassist,我们需要引入对应的依赖,如果是maven项目的话,我们直接引用下面的代码即可:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
从一个简单的例子开始:生成一个新类
首先让我们回忆下我们自己如何编写一个Java类:
- 确定类名
- 添加字段
- 添加构造函数
- 添加成员函数
那么使用Javassist生成一个新类也包含类似的步骤:
- 获取ClassPool对象(ClassPool代表CtClass的容器)
- 通过ClassPool对象构建一个CtClass对象(一个CtClass代表一个Class的容器)(确定类名)
- 通过CtField对象向类中添加类字段(添加字段)
- 通过CtConstructor对象向类中添加构造函数(添加构造函数)
- 通过CtMethod对象向类中添加方法(添加成员函数)
其实使用Javassist生成一个新类和我们自己编写一个类的过程差不多,只是Javassist是通过API来完成了我们通过源码完成的事。
接下来看看如何使用Javassist创建一个新类吧:
/*
使用Javassist生成一个新的类
*/
public static void createUser() throws Exception {
ClassPool pool = ClassPool.getDefault();
// 构造一个CtClass对象
CtClass ctClass = pool.makeClass("cn.wygandwdn.javassist.learn.User");
// 添加属性字段
CtField username = new CtField(pool.get("java.lang.String"), "username", ctClass);
username.setModifiers(Modifier.PRIVATE);
ctClass.addField(username, CtField.Initializer.constant("ZhangSan"));
CtField pass = new CtField(pool.get("java.lang.String"), "password", ctClass);
pass.setModifiers(Modifier.PRIVATE);
ctClass.addField(pass, CtField.Initializer.constant("123456"));
// 添加构造函数
// 默认构造函数
CtConstructor none = new CtConstructor(new CtClass[] {}, ctClass);
none.setBody("System.out.println(\"使用javassist产生的默认构造函数\");");
ctClass.addConstructor(none);
// 构造函数
CtConstructor constructor = new CtConstructor(new CtClass[] {pool.get("java.lang.String"), pool.get("java.lang.String")},
ctClass);
constructor.setBody("{$0.username = $1;$0.password = $2;}");
ctClass.addConstructor(constructor);
// 添加getter和setter方法
CtMethod setUsername = new CtMethod(CtClass.voidType, "setUsername", new CtClass[] {pool.get("java.lang.String")},
ctClass);
setUsername.setModifiers(Modifier.PUBLIC);
setUsername.setBody("{$0.username = $1;}");
ctClass.addMethod(setUsername);
CtMethod getUsername = new CtMethod(pool.get("java.lang.String"), "getUsername", new CtClass[] {}, ctClass);
getUsername.setModifiers(Modifier.PUBLIC);
getUsername.setBody("{return $0.username;}");
ctClass.addMethod(getUsername);
CtMethod setPass = CtNewMethod.setter("setPass", pass);
ctClass.addMethod(setPass);
CtMethod getPass = CtNewMethod.getter("getPass", pass);
ctClass.addMethod(getPass);
Class<?> user = ctClass.toClass();
Object instance = user.getDeclaredConstructors()[0].newInstance();
System.out.println(instance);
}
在上面的例子中,我们用到了ClassPool、CtClass、CtField、CtConstructor、CtMethod等工具类,接下来就看看这些工具类的用处吧
2. middle
本小节将介绍Javassist中常用的一些源码级别的API:
- ClassPool:存放CtClass的容器
- CtClass:代表一个class or interface or annotation
- CtField:代表class中的某个字段
- CtConstructor:代表class的构造函数
- CtMethod:代表class的包含的方法
2.1 ClassPool
ClassPool是存放CtClass的容器,CtClass只能通过ClassPool提供的方法获取,其中包括get()&makeClass()方法;ClassPool通过HashTable来缓存已经创建好的CtClass对象,其中键为className,值为CtClass对象
下面是一些常用的方法:
- getDefault():获取ClassPool的单例对象,并且添加SystemClassPath
- get():根据全限定类名获取对应的CtClass对象,如果ClassPool中没有缓存对应的CtClass对象则新建一个CtClass对象缓存并返回
- getAndRename():根据全限定类名获取CtClass对象,并且重命名类名
- find():根据全限定类名获取资源位置
- makeClass():根据全限定类名创造CtClass对象并返回
- appendClassPath&insertClassPath:向类搜索路径列表尾部&头部插入ClassPath
- toClass():将CtClass对象转换为Class对象
- importPackage():将Java包导入到ClassPool中,方便其通过package查找到对应的类
ClassPool中如果缓存大量的CtClass对象,可能会消耗大量缓存;为了避免这种情况的发生,我们可以调用CtClass.detach()方法,该方法将会删除ClassPool中相应的CtClass对象,释放空间;如果我们调用get()方法获取被删除的CtClass对象,那么ClassPool会重新生成相应的CtClass对象
ClassPool可以构造类似于Java类加载器机制的级联ClassPool,当调用get()方法时,先委托给父ClassPool尝试获取CtClass对象,如果父类ClassPool无法获取到CtClass对象则子ClassPool会尝试加载类文件(只存在父子ClassPool的情况下),我们可以通过ClassPool.get()方法发现这个机制:
![image-20220204202458974](https://gitee.com/blog-map-bed/first-bed/raw/master/img/20220212102701.png)
上述代码中parent就相当于父类ClassPool,如果childFirstLookup为false并且parent非空,则会委托给父ClassPool检索CtClass对象
实现该机制非常简单:
public static void parentClassPool() throws NotFoundException {
ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.appendClassPath("D:/java_project/trace/learn-javassist/src/main/java");
}
通过向ClassPool的构造函数传入父ClassPool即可
2.2 CtClass
CtClass实例代表一个Java类,只能通过ClassPool获取它,我们可以通过ClassPool的get()&makeClass()方法来获取CtClass对象
CtClass是一个抽象类,并且它的绝大部分方法都是返回null或者抛出异常;这些方法会被其子类重写。CtClass的子类包括:CtClassType、CtPrimitiveType、CtArray
当我们获取到CtClass对象之后,就可以对其进行修改,可以修改、添加、删除类的字段和方法;其中对于方法的修改可以直接修改整个方法体,或者修改方法体中的某个表达式(也就是某行语句)
首先看看CtClass一些常用的方法:
- isModified():如果类被修改过的话就返回true,否则返回false
- isFrozen():如果类被冻结则返回true,否则返回false;类是否被冻结表明类是否能够修改,如果类被冻结了则不能进行修改,否则可以修改
- freeze():冻结类,使其不能被修改
- defrost():解冻被冻结的类,使其可以被修改
- isPrimitive():是否为Java原始类型:boolean, byte, char, short, int, long, float, double, or void.
- getName():获取CtClass代表的类的全限定类名
- setName():修改类名,必须以全限定类名的形式传递参数
- isInterface()&isAnnotation()&isEnum():CtClass代表的类对象是否为接口、注解、枚举类型
- getField()&getMethod()&getConstructor():获取类的字段、方法、构造函数
- addField()&addMethod()&addConstructor():向类中添加字段、方法、构造函数
- removeField()&removeMethod()&removeConstructor():从类中移除字段、方法、构造函数
- insturment():向类中注册CodeConverter或者ExprEditor,其中CodeConverter用于代码的转换,ExprEditor用于编辑方法体中满足条件的代码块,instrument(ExprEditor)是CtMethod和CtConstructor调用的方法
- toClass()&toBytecoded()&writeFile():将CtClass对象转换为Class对象、bytecode字节码、写入.class文件中,调用这些方法后,CtClass对象将会被冻结,无法进行修改
- detach():从ClassPool中移除CtClass对象,释放内存
对于CodeConverter和ExprEditor来说,可以通过如下例子加深理解:
/*
CodeConverter为方法体的简单转换器
此类的实例指定如何修改表示方法体的字节码
*/
ClassPool cp = ClassPool.getDefault();
CtClass point = cp.get("Point");
CtClass singleton = cp.get("Singleton");
CtClass client = cp.get("Client");
CodeConverter conv = new CodeConverter();
conv.replaceNew(point, singleton, "makePoint");
client.instrument(conv);
上述代码的作用为:将Client类的所有方法中出现的"new Point()“替换为"Singleton.makePoint()”
/*
ExprEditor为方法体转换器
可以定义该类的子类自定义修改方法体,如果调用CtMethod.instrument方法,会从头到尾扫描方法体
当发现表达式,例如方法调用、new Object()等被发现,可以调用ExprEdit.edit()方法修改这些表达式
这些修改会作用到原始方法体
*/
public class Func {
public static String getName() {
String action = "开始";
int length = action.length();
System.out.println(length);
return "张三!!!";
}
public static void updateMethod() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("Func");
CtMethod getName = ctClass.getDeclaredMethod("getName", new CtClass[]{});
getName.instrument(
new ExprEditor() {
@Override
public void edit(MethodCall m) throws CannotCompileException {
System.out.println(m.getClassName());
if (m.getClassName().equals("java.lang.String")
&& m.getMethodName().equals("length")) {
m.replace("$_ = 100;");
}
}
}
);
Class<?> aClass = ctClass.toClass();
Method method = aClass.getMethod("getName", new Class[]{});
Object invoke = method.invoke(aClass, null);
System.out.println(invoke);
}
public static void main(String[] args) {
// 该方法会输出100,因为这里int length = action.length()被改为了int length = 100
updateMethod();
}
}
通过CtClass对象,我们可以根据实际业务需求对真实类对象进行修改
有关更多CtClass对象的用法,我们可以在实际的业务需求中进行探索
2.3 CtField
CtField代表类的字段,我们既可以通过CtClass获取类中已经存在的字段,也可以新建一个CtField对象并添加到CtClass中
CtField的常用方法如下:
- make(String src, CtClass declaring):通过源码的形式构造CtField对象,例如src可以为:“public String name;”,不要忘记";",否则会编译失败,declaring代表我们要添加字段的类
- getName&setName:获取字段名&修改字段名
- getModifiers&setModifiers:获取访问修饰符&设置访问修饰符,其中,设置访问修饰符可以通过Modifier中提供的常量字段设置,并且可以组合设置,例如要设置public static,可以使用如下设置:Modifier.PUBLIC | Modifier.STATIC
- getAnnotation&getAnnotations:获取字段上的注解
- getSignature:返回代表字段类型的字符串,如果是String类型的话,返回结果为:Ljava/lang/String;
- getGenericSignature&setGenericSignature:获取和设置字段的泛型类型
- getConstantValue:只能获取Java基本类型的包装类型对应的常量值,如果是其他类型的常量的话则会返回null
我们在向CtClass类中添加新字段的时候,可以添加字段的初始值,在调用CtClass.addField方法时,向其中传入CtField.Initializer参数即可设置字段对应的初始值,例如:
ctClass.addField(field, CtField.Initializer.constant("张三"));
此外,如果我们不使用CtField.make()方法创造CtField对象的话,我们直接调用CtField的构造函数即可,然后调用setModifiers设置字段的访问修饰符即可:
CtField username = new CtField(pool.get("java.lang.String"), "username", ctClass);
username.setModifiers(Modifier.PRIVATE);
ctClass.addField(username, CtField.Initializer.constant("ZhangSan"));
2.4 CtConstructor
CtConstructor代表类的构造函数,也可能代表类的初始化函数;可以通过isClassInitializer()方法确定是否为类初始化函数。
我们可以通过CtClass对象获取其对应的CtConstructor,也可以直接新建一个CtConstructor并添加到CtClass对象中;新建CtConsturctor有两种方式:
// 方法一,通过new CtConstructor()的方式创建CtConstructor对象
// new CtClass[]{}相当于构造函数的参数,ctClass为要添加构造函数的类对象
CtConstructor none = new CtConstructor(new CtClass[] {}, ctClass);
none.setBody("System.out.println(\"使用javassist产生的默认构造函数\");");
ctClass.addConstructor(none);
// 方法二,通过CtNewConstructor提供的静态方法创建CtConstructor对象
CtNewConstructor.make("public User() {System.out.println(\"使用javassist产生的默认构造函数\");}", ctClass);
CtConstructor常用方法如下:
- getLongName()&getName():获取带有参数的名称(全限定类名和构造函数参数类型),如:javassist.CtConstructor(CtClass[],CtClass);getName只获取构造函数名也就是类名
- isConstructor()&isClassInitializer():是否为构造函数&是否为类的初始化函数
- callSuper():是否调用父类构造函数
- setBody():设置构造函数源码
- insertBeforeBody():在构造函数开头插入代码
- setModifiers():设置访问权限
通过上述方法,我们可以自定义构造函数并添加到CtClass对象中去,也可以访问现有构造函数的相关信息
CtNewConstructor这个工具类也非常有用,我们通常使用它来创建CtConstructor对象,其常用方法如下:
- make():构造CtNewConsturctor对象
- copy():利用原有的CtConstructor对象复制一个新的CtConstructor对象
- defaultConstructor():创建默认构造函数
- skeleton:创建一个带有参数的构造函数,但是该函数体只调用super()方法,其余代码需要自己编写插入
通过上述代码我们很容易自定义构造函数
2.5 CtMethod
CtMethod代表Java类中的方法,我们通常写的Java类是通过各种各样的方法来实现想要的功能的,所以CtMethod至关重要
CtMethod的创建方法和CtConstructor的创建方法类似,也可以通过源码形式直接创建;或者先定义函数名称和参数类型,然后再设置函数体
CtMethod的一些常用方法如下:
- make():通过源码的形式创建CtMethod对象
- getName()&setName():获取函数名,设置函数名(可以用于修改函数名称)
- setBody():设置函数体
- setWrappedBody():通过提供的CtMethod对象的函数体来设置当前CtMethod对象的函数体
- setModifiers():设置函数的访问修饰符
- insertBefore()&insertAfter()&insertAt():在函数最开始|结束位置|任意位置(需要知道代码行号)插入代码
- addCatch()&addLocalVariable():添加try catch语句 | 添加局部变量
- instrument():根据提供的CodeConverter|ExprEditor对函数体中符合条件的代码进行编辑,详情可以参考2.2节
传递给方法 insertBefore() ,insertAfter() ,addCatch() 和 insertAt() 的 String 对象是由Javassist 的编译器编译的。 由于编译器支持语言扩展,以 $ 开头的几个标识符有特殊的含义:
符号 | 含义 |
---|---|
$0,$1,$2,… | $0代表this,$1,$2,…代表方法的第1个参数、第2个参数… |
$args | 方法参数数组,$args的类型是Object[] |
$$ | 代表全部方法参数,例如m($$)=m($1,$2,…) |
$cflow | cflow变量,此只读变量返回特定方法的递归调用的深度 |
$r | 函数返回结果类型,用于强制类型转换 |
$w | 包装类型,用于强制类型转换,例如:Integer i = ($w)5; |
$_ | 函数返回结果 |
$sig | Class类型的数组,代表形参的类型 |
$type | 代表函数返回结果的Class类型 |
$class | 代表当前编辑的类的Class类型 |
$proceed | 代表源码中调用的方法名、构造函数名… |
2.5.1 $cflow
上述标识符中,对于 c f l o w 这 个 标 识 符 , 我 们 初 看 可 能 会 云 里 雾 里 , cflow这个标识符,我们初看可能会云里雾里, cflow这个标识符,我们初看可能会云里雾里,cflow表示控制流,此只读变量返回特定方法的递归调用的深度。
假设下面所示的方法由CtMethod对象cm表示:
int fact(int n) {
if (n <= 1) {
return n;
} else {
return n * fact(n - 1);
}
}
要想使用cflow,首先需要声明使用cflow监视方法fact()的调用:
CtMethod cm = ...;
cm.useCflow("fact");
useCflow()的参数是$cflow变量的标识符,任何有效的Java名称都可以用作标识符。
c f l o w ( f a c t ) 表 示 由 c m 指 定 的 方 法 的 递 归 调 用 深 度 。 cflow(fact)表示由cm指定的方法的递归调用深度。 cflow(fact)表示由cm指定的方法的递归调用深度。cflow(fact)的值在方法第一次调用时为0,而当方法在方法中递归调用时为1.
$cflow的值是当前线程的最顶层堆栈下与cm相关联的堆栈帧数。cflow也可以不在cm方法中访问。
2.5.2 addCatch
如何在方法中插入try catch语句呢,通过下面这个例子就能轻松解决:
CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.io.IOException");
// 在插入的源代码中,异常用$e表示
m.addCatch("{System.out.println($e); throw $e;}", etype);
上述代码转换成的java代码如下(请注意,插入的代码片段必须以 throw 或 return 语句结束):
try {
// the original method body
} catch (java.io.IOException e) {
System.out.println(e);
throw e;
}
2.5.3 相互递归的方法
有时候我们可能需要向代码中插入相互递归的方法,Javassist也帮我们想到了对应的解决方法
在Javassist中不能有这样的方法:如果它调用另一个方法,而另一个方法没有被添加到一个类(Javassist可以编译一个以递归方式调用的方法)。如果要向类添加相互递归的方法,需要使用下面的技巧。
假设我们想要将方法m()和n()添加到由ctClass表示的类中:
CtClass ctClass = ...;
CtMethod m = CtNewMethod.make("public abstract int m(int i);", ctClass);
CtMethod n = CtNewMethod.make("public abstract int n(int i);", ctClass);
ctClass.addMethod(m);
ctClass.addMethod(n);
m.setBody("{ return ($1 <= 0) ? 1 : (n($1 - 1) * $1); }");
n.setBody("{ return m($1); }");
cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);
此时,我们必须先创建两个抽象的方法,并将它们添加到类中,然后设置它们的方法体,最后将类改为非抽象类
3. other
如果事先知道要修改哪些类,那么最简单的方法如下:
- 通过ClassPool.get()获取CtClass对象
- 然后对CtClass对象进行修改
- 调用CtClass对象的writeFile()或者toBytecode()获得修改过的类文件
如果在加载时,可以确定是否要修改某个类,就必须使Javassist与类加载器协作,以便在加载时修改字节码;同时也可以自定义类加载器,或者使用Javassist提供的类加载器
3.1 CtClass.toClass()
CtClass 的 toClass() 方法请求当前线程的上下文类加载器,加载 CtClass 对象所表示的类。要调用此方法,调用者必须具有相关的权限; 否则,可能会抛出 SecurityException。例如:
public class Hello {
public void say() {
System.out.println("Hello");
}
}
public class Test {
public static void main(String[] args) throws Exception {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("Hello");
CtMethod m = cc.getDeclaredMethod("say");
m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
Class c = cc.toClass();
Hello h = (Hello)c.newInstance();
h.say();
}
}
Test.main()向Hello的say()方法中插入一个println(),然后构造一个修改过的Hello类的实例,并在该实例上调用say()方法
上面的程序要正常运行,Hello类在调用toClass()之前就不能被加载;如果JVM在toClass()调用之前加载了原始的Hello类,后续加载修改的Hello类将会失败(抛出LinkageError)
例如,如果Test.main()方法如下:
public static void main(String[] args) throws Exception {
Hello orig = new Hello();
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("Hello");
:
}
那么原始的Hello类在main的第一行被加载,toClass()调用会抛出一个异常,因为类加载器不能同时加载两个不同版本的Hello类。
如果程序在某些应用程序服务器(如JBoss和Tomcat)上运行,toClass() 使用的上下文类加载器可能是不合适的。在这种情况下,你会看到一个意想不到的 ClassCastException。为了避免这个异常,必须给 toClass() 指定一个合适的类加载器。
例如,如果 ‘bean’ 是你的会话 bean 对象,那么下面的代码:
CtClass cc = ...;
Class c = cc.toClass(bean.getClass().getClassLoader());
可以工作。你应该给 toClass() 传递加载了你的程序的类加载器(上例中,bean
对象的类)
3.2 javassist.Loader
Javassit 提供一个类加载器 javassist.Loader。它使用 javassist.ClassPool 对象来读取类文件。
例如,javassist.Loader 可以用于加载用 Javassist 修改过的类。
import javassist.*;
import test.Rectangle;
public class Main {
public static void main(String[] args) throws Throwable {
ClassPool pool = ClassPool.getDefault();
Loader cl = new Loader(pool);
CtClass ct = pool.get("test.Rectangle");
ct.setSuperclass(pool.get("test.Point"));
Class c = cl.loadClass("test.Rectangle");
Object rect = c.newInstance();
:
}
}
如果用户希望在加载时按需修改类,则可以向 javassist.Loader 添加事件监听器。当类加载器加载类时会通知监听器。事件监听器类必须实现以下接口:
public interface Translator {
public void start(ClassPool pool)
throws NotFoundException, CannotCompileException;
public void onLoad(ClassPool pool, String classname)
throws NotFoundException, CannotCompileException;
}
当事件监听器通过 addTranslator() 添加到 javassist.Loader 对象时,start() 方法会被调用。在 javassist.Loader 加载类之前,会调用 onLoad() 方法。可以在 onLoad() 方法中修改被加载的类的定义。
例如,下面的事件监听器在类加载之前,修改方法getId()的返回值。
public class Func {
public static void main(String[] args) throws NoSuchMethodException {
System.out.println(getId());
}
public static Integer getId() {
return 512;
}
}
public class MyTranslator implements Translator {
void start(ClassPool pool) throws NotFoundException, CannotCompileException {}
void onLoad(ClassPool pool, String classname) throws NotFoundException, CannotCompileException {
CtClass ctClass = pool.get(classname);
CtMethod getId = ctClass.getDeclaredMethod("getId");
getId.setBody("return ($w)64;");
}
}
注意,onLoad() 不必调用 toBytecode() 或 writeFile(),因为 javassist.Loader 会调用这些方法来获取类文件。
接下来使用MyTranslator对象运行类Func,main()方法如下:
public static void main(String[] args) throws Throwable {
Translator translator = new MyTranslator();
ClassPool pool = ClassPool.getDefault();
Loader loader = new Loader();
loader.addTranslator(pool, translator);
loader.run("cn.wygandwdn.javassist.learn.Func", args);
}
类Func会被MyTranslator监听,在通过loader加载类Func时会修改其中getId()方法的返回值
javassist.Loader 以不同的顺序从 java.lang.ClassLoader 中搜索类。ClassLoader 首先将加载操作委托给父类加载器,只有当父类加载器无法找到它们时才尝试自己加载类。另一方面,javassist.Loader 尝试在委托给父类加载器之前加载类。它仅在以下情况下进行委派:
- 在 ClassPool 对象上调用 get() 找不到这个类;
- 这些类已经通过 delegateLoadingOf() 来指定由父类加载器加载。
此搜索顺序允许 Javassist 加载修改过的类。但是,如果找不到修改的类,它将委托父类加载器来加载。一旦一个类被父类加载器加载,那个类中引用的其他类也将被父类加载器加载,因此它们是没有被修改的。回想一下,C 类引用的所有类都由 C 的实际加载器加载的。如果你的程序无法加载修改的类,你应该确保所有使用该类的类都是由 javassist 加载的。
3.3 自定义类加载器
自定义类加载器需要继承ClassLoader,并实现findClass方法,这里我们通过ClassPool获取到类名对应的CtClass对象,并生成字节码,通过字节码生成新的Class
import javassist.*;
public class SampleLoader extends ClassLoader {
/* Call MyApp.main(). */
public static void main(String[] args) throws Throwable {
SampleLoader s = new SampleLoader();
Class c = s.loadClass("MyApp");
c.getDeclaredMethod("main", new Class[] { String[].class })
.invoke(null, new Object[] { args });
}
private ClassPool pool;
public SampleLoader() throws NotFoundException {
pool = new ClassPool();
// 插入类的存放路径
pool.insertClassPath("./class"); // MyApp.class must be there.
}
/*
* Finds a specified class.
* The bytecode for that class can be modified.
*/
protected Class findClass(String name) throws ClassNotFoundException {
try {
CtClass cc = pool.get(name);
// *modify the CtClass object here*
byte[] b = cc.toBytecode();
return defineClass(name, b, 0, b.length);
} catch (NotFoundException e) {
throw new ClassNotFoundException();
} catch (IOException e) {
throw new ClassNotFoundException();
} catch (CannotCompileException e) {
throw new ClassNotFoundException();
}
}
}
这是使用 Javassist 的最简单的方法。 但是,如果你编写一个更复杂的类加载器,你可能需要更详细地了解 Java 的类加载机制。
4. example
前面几小节详细介绍了Javassist的各种用法及API,接下来看几个简单的例子
创建一个接口:
public static void createInterface() throws Exception {
ClassPool pool = ClassPool.getDefault();
// 创造接口
CtClass ctClass = pool.makeInterface("cn.wygandwdn.javassist.learn.BlogService");
// 创造虚函数
CtMethod method = CtNewMethod.abstractMethod(pool.get("java.lang.String"), "getBlogById",
new CtClass[]{pool.get("java.lang.Integer")}, new CtClass[]{pool.get("java.lang.Exception")}, ctClass);
// 向接口中添加方法
ctClass.addMethod(method);
ctClass.writeFile("D:/java_project/trace/learn-javassist/src/main/java");
}
创建一个注解:
public static void createAnnotation() throws CannotCompileException, IOException, NotFoundException {
ClassPool pool = ClassPool.getDefault();
CtClass annotation = pool.makeAnnotation("cn.wygandwdn.javassist.learn.Author");
CtMethod name = CtNewMethod.make("String name();", annotation);
annotation.addMethod(name);
CtMethod age = CtNewMethod.make("int age();", annotation);
annotation.addMethod(age);
ClassFile classFile = annotation.getClassFile();
ConstPool constPool = classFile.getConstPool();
AnnotationsAttribute attributes = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
// 设置元注解@Target
Annotation target = new Annotation("java.lang.annotation.Target", constPool);
EnumMemberValue targetEnum = new EnumMemberValue(constPool);
targetEnum.setType("java.lang.annotation.ElementType");
targetEnum.setValue("TYPE");
target.addMemberValue("value", targetEnum);
// 设置元注解@Retention
Annotation retention = new Annotation("java.lang.annotation.Retention", constPool);
EnumMemberValue retentionEnum = new EnumMemberValue(constPool);
retentionEnum.setType("java.lang.annotation.RetentionPolicy");
retentionEnum.setValue("RUNTIME");
retention.addMemberValue("value", retentionEnum);
// 将注解添加到AnnotationAttribute中
attributes.addAnnotation(target);
attributes.addAnnotation(retention);
classFile.addAttribute(attributes);
annotation.writeFile("D:/java_project/trace/learn-javassist/src/main/java");
}
4.1 关于注解的全部用法(类、字段、方法、参数)
接下来我们在第一节的基础之上,看看如何在类、字段、方法、参数上添加注解:
public static void createUser() throws Exception {
ClassPool pool = ClassPool.getDefault();
// 构造一个CtClass对象
CtClass ctClass = pool.makeClass("cn.wygandwdn.javassist.learn.User");
ClassFile classFile = ctClass.getClassFile();
// 获取类的常量池
ConstPool constPool = classFile.getConstPool();
// 添加类注解
AnnotationsAttribute attributes = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
Annotation deprecated = new Annotation(Deprecated.class.getCanonicalName(), constPool);
deprecated.addMemberValue("since", new StringMemberValue("1.1", constPool));
attributes.addAnnotation(deprecated);
classFile.addAttribute(attributes);
// 添加属性字段
CtField username = new CtField(pool.get("java.lang.String"), "username", ctClass);
username.setModifiers(Modifier.PRIVATE);
ctClass.addField(username, CtField.Initializer.constant("ZhangSan"));
CtField pass = new CtField(pool.get("java.lang.String"), "password", ctClass);
pass.setModifiers(Modifier.PRIVATE);
ctClass.addField(pass, CtField.Initializer.constant("123456"));
// 添加字段注解
AnnotationsAttribute field = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
Annotation author = new Annotation(Author.class.getCanonicalName(), constPool);
author.addMemberValue("name", new StringMemberValue("java", constPool));
author.addMemberValue("age", new IntegerMemberValue(constPool, 23));
field.addAnnotation(author);
username.getFieldInfo().addAttribute(field);
pass.getFieldInfo().addAttribute(field);
// 添加构造函数
// 默认构造函数
CtConstructor none = new CtConstructor(new CtClass[] {}, ctClass);
none.setBody("System.out.println(\"使用javassist产生的默认构造函数\");");
ctClass.addConstructor(none);
// 构造函数
CtConstructor constructor = new CtConstructor(new CtClass[] {pool.get("java.lang.String"), pool.get("java.lang.String")},
ctClass);
constructor.setBody("{$0.username = $1;$0.password = $2;}");
ctClass.addConstructor(constructor);
// 添加getter和setter方法
CtMethod setUsername = new CtMethod(CtClass.voidType, "setUsername", new CtClass[] {pool.get("java.lang.String")},
ctClass);
setUsername.setModifiers(Modifier.PUBLIC);
setUsername.setBody("{$0.username = $1;}");
ctClass.addMethod(setUsername);
CtMethod getUsername = new CtMethod(pool.get("java.lang.String"), "getUsername", new CtClass[] {}, ctClass);
getUsername.setModifiers(Modifier.PUBLIC);
getUsername.setBody("{return $0.username;}");
ctClass.addMethod(getUsername);
CtMethod setPass = CtNewMethod.setter("setPass", pass);
ctClass.addMethod(setPass);
CtMethod getPass = CtNewMethod.getter("getPass", pass);
ctClass.addMethod(getPass);
// 添加方法注解
CtMethod test = CtNewMethod.make("public void testAnnotation(String test, String test1) {" +
"System.out.println(\"测试使用Javassist向方法添加注解\");}", ctClass);
AnnotationsAttribute methodAnnotation = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
Annotation methodAuthor = new Annotation(Author.class.getCanonicalName(), constPool);
methodAuthor.addMemberValue("name", new StringMemberValue("java", constPool));
methodAuthor.addMemberValue("age", new IntegerMemberValue(constPool, 23));
methodAnnotation.addAnnotation(methodAuthor);
test.getMethodInfo().addAttribute(methodAnnotation);
// 添加参数注解
ParameterAnnotationsAttribute parameter = new ParameterAnnotationsAttribute(constPool,
ParameterAnnotationsAttribute.visibleTag);
Annotation parameterAnno = new Annotation(Author.class.getCanonicalName(), constPool);
parameterAnno.addMemberValue("name", new StringMemberValue("java", constPool));
parameterAnno.addMemberValue("age", new IntegerMemberValue(constPool, 23));
// 这里二维数组的含义如下:行数为参数的个数,列数为每个参数上注解的个数
// 二维数组对应位置就是某个参数的注解,如果行数小于参数个数,那么注解就从第一个参数开始标注,直到使用完
Annotation[][] paramArrays = new Annotation[1][1];
paramArrays[0][0] = parameterAnno;
parameter.setAnnotations(paramArrays);
test.getMethodInfo().addAttribute(parameter);
ctClass.addMethod(test);
ctClass.writeFile("D:/java_project/trace/learn-javassist/src/main/java");
}
上述代码总结了如何使用Javassist添加注解的各种用法,有相关需求的小伙伴模仿着来就可以
5. summary
Javassist学习起来相对比较轻松,不需要掌握太多关于字节码和类文件格式的知识就能够轻松对Java类进行编辑,以此来完成我们的业务需求
Javassist大部分API都提供了一定的示例代码,如果遇到一些新的API而不知其作用,那么阅读它的API文档或者源码是个不错的选择,因为这里面都提供了示例
加油!
有什么问题可以评论区讨论哦!
参考:
Javassist生成class(生成类,方法,字段,注解)
利用Javassist编译期创建注解类(Annotation Class) - 掘金 (juejin.cn)
Javassist注解(Annotation)的使用:CXF WebService动态生成 - coshaho - 博客园 (cnblogs.com)
Javassist 使用指南(一) - 简书 (jianshu.com)
欢迎访问我的个人博客:风在哪个人博客