Javassist初见
1diot9 Lv4

前言

在写序列化数据生成工具时,TemplatesImpl里的bytes[][]往往都是由javassist动态生成的。javassist依赖相较于asm依赖,使用起来更加简单,因为javassist更像是在操作类,而asm就是在操作字节码了。这里介绍一下javassist的基本用法,另外自己在使用javassist时,遇到了不少坑,这里也来总结一下。

基本使用

直接看代码,我写注释了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.idiot9;

import javassist.*;


public class CreateMyTest {
public String address;

public CreateMyTest() {
System.out.println("ctConstrutor");
}

public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass MyTest = pool.makeClass("MyTest");

//添加字段
CtField name = new CtField(pool.get("java.lang.String"), "name", MyTest);
name.setModifiers(Modifier.PRIVATE);
MyTest.addField(name, CtField.Initializer.constant("1diot9"));

CtField address = CtField.make("public String address;", MyTest);
MyTest.addField(address);

//添加构造方法
CtConstructor ctConstructor = new CtConstructor(new CtClass[]{}, MyTest);
ctConstructor.setBody("System.out.println(\"ctConstrutor\");");
MyTest.addConstructor(ctConstructor);



//添加方法
CtMethod hello = new CtMethod(CtClass.voidType, "hello", new CtClass[]{}, MyTest);
hello.setModifiers(Modifier.PUBLIC);
hello.setBody("System.out.println(\"hello\" + name);");
MyTest.addMethod(hello);

CtMethod tmp = CtMethod.make(" public void tmp(){\n" +
" System.out.println(\"tmp\");\n" +
" }", MyTest);
MyTest.addMethod(tmp);

//添加静态代码块
CtConstructor staticBlock = MyTest.makeClassInitializer();
staticBlock.setBody("System.out.println(\"staticBlock\");");

String s = "System.out.println(\"staticBlock2\");";
MyTest.makeClassInitializer().insertAfter(s);


//添加父类
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
MyTest.setSuperclass(superClass);


//输出字节码
MyTest.writeFile("./src/main/java/");


}

public void tmp(){
System.out.println("tmp");
}
}

这里介绍了几种常用的方法:创建字段,创建方法,创建构造方法,创建静态代码块,继承父类

类搜索相关

javassist必须要找到对应的类才能进行动态创建,所以搞清楚它的默认搜索路径和怎么添加路径很重要。下面来简单介绍一下。

首先,会加载rt.jar中的核心包,包括:java.lang包的所有类,java.util,java.net,java.math,java.nio,java.swing,java.awt,java.lang.reflect,java.lang.invoke。注意,这上面的都是加载单个包下的类,不包括子包里的内,这一点很重要。

然后介绍一下第三方依赖的搜索情况。

如果你在pom里导入了依赖,那么你可以通过 pool.get(“org.apache.commons.beanutils.BeanComparator”); 的方式直接获取到类。但是,如果你没有在pom里导入依赖,就需要手动添加类搜索路径,比如添加一个jar包路径:pool.insertClassPath(“D:\repository\commons-beanutils\commons-beanutils\1.9.4\commons-beanutils-1.9.4.jar”); 这样javassist就能找到这个包中的所有类。

除了在get类的时候有搜索问题,在动态创建方法时也会有问题。首先看一下下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CreateOutPackage {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
//insertClassPath影响类搜索范围,pom里导入的依赖,默认在搜索范围
pool.insertClassPath("D:\\repository\\commons-beanutils\\commons-beanutils\\1.9.4\\commons-beanutils-1.9.4.jar");

CtClass ctClass = pool.makeClass("Test");

String staticBlock = "BeanComparator beanComparator = new BeanComparator();";
ctClass.makeClassInitializer().insertAfter(staticBlock);

}
}

是不是觉得没有任何问题?但是运行后会发现报错:img

报错原因为:找不到BeanComparator类。这里你可能会想,上面不都insertClassPath了吗,为什么还找不到呢?这是因为,我们没有写全限定名。举个例子,存在两个同名类,com.A和net.A,那么,如果你只写A,javassist怎么知道是哪一个呢?所以我们修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CreateOutPackage {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
//insertClassPath影响类搜索范围,pom里导入的依赖,默认在搜索范围
pool.insertClassPath("D:\\repository\\commons-beanutils\\commons-beanutils\\1.9.4\\commons-beanutils-1.9.4.jar");

ctClass = pool.makeClass("Test");

String staticBlock = "org.apache.commons.beanutils.BeanComparator beanComparator = new org.apache.commons.beanutils.BeanComparator();";
ctClass.makeClassInitializer().insertAfter(staticBlock);

}
}

这样一来就没有报错了。

不过,每次都要写全限定名是不是觉得很麻烦?所以,javassist还提供了另外一个方法:pool.importPackage(“org.apache.commons.beanutils”); 这个的效果就相当于每次在类最顶上进行import,回忆一下,当我们import后,是不是只需要写SimpleName就行了?这里importPackage的效果是一样的。

于是,我们可以再修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CreateOutPackage {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
//insertClassPath影响类搜索范围,pom里导入的依赖,默认在搜索范围
pool.insertClassPath("D:\\repository\\commons-beanutils\\commons-beanutils\\1.9.4\\commons-beanutils-1.9.4.jar");
//importPackage影响代码写法,是写全限定名还是写SimpleName
pool.importPackage("org.apache.commons.beanutils");

CtClass ctClass = pool.makeClass("Test");

String staticBlock = "BeanComparator beanComparator = new BeanComparator();";
ctClass.makeClassInitializer().insertAfter(staticBlock);

}

}

这样看起来是不是简洁很多?

遇到的坑

在写SpringInterceptor内存马时,遇到一个很奇怪的点。我定义了一个command方法,并在preHandle方法中调用:img

但是这样会报错:img

当我把最底下的command方法删除后就行了。我一开始以为是command方法的问题,但是后来发现直接照搬方法体到preHandle里竟然可以,于是也不是command方法的问题。

后来不知怎么灵机一动,把command调用上面的String command改成了String cmd,就好了:img

原来这里同一个类里的变量名和方法名不能一样。

参考

https://drun1baby.top/2023/12/07/Java-Agent-%E5%86%85%E5%AD%98%E9%A9%AC%E5%AD%A6%E4%B9%A0/#Javassist

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 52.9k 访客数 访问量