javaassist_⽤Javassist进⾏类转换
讲过了 Java 类格式和利⽤反射进⾏的运⾏时访问后,本系列到了进⼊更⾼级主题的时候了。本⽉我将开始本系列的第⼆部分,在这⾥Java 类信息只不过是由应⽤程序操纵的另⼀种形式的数据结构⽽已。我将这个主题的整个内容称为 classworking 。
我将以 Javassist 字节码操作库作为对 classworking 的讨论的开始。Javassist 不仅是⼀个处理字节码的库,⽽且更因为它的另⼀项功能使得它成为试验 classworking 的很好的起点。这⼀项功能就是:可以⽤ Javassist 改变 Java 类的字节码,⽽⽆需真正了解关于字节码或者 Java 虚拟机(Java virtual machine JVM)结构的任何内容。从某⽅⾯将这⼀功能有好处也有坏处 — 我⼀般不提倡随便使⽤不了解的技术 — 但是⽐起在单条指令⽔平上⼯作的框架,它确实使字节码操作更可具有可⾏性了。
Javassist 基础
Javassist 使您可以检查、编辑以及创建 Java ⼆进制类。检查⽅⾯基本上与通过 Reflection API 直接在 Java 中进⾏的⼀样,但是当想要修改类⽽不只是执⾏它们时,则另⼀种访问这些信息的⽅法就很有⽤了。这是因为 JVM 设计上并没有提供在类装载到 JVM 中后访问原始类数据的任何⽅法,这项⼯作需要在 JVM 之外完成。
不要错过本系列的其余部分
第⼀部分:” 类和类装⼊”(2003年4⽉)
第⼆部分:” 引⼊反射” (2003年6⽉)
第三部分:” 应⽤返射” (2003年7⽉)
Javassist 使⽤ javassist.ClassPool 类跟踪和控制所操作的类。这个类的⼯作⽅式与 JVM 类装载器⾮常相似,但是有⼀个重要的区别是它不是将装载的、要执⾏的类作为应⽤程序的⼀部分链接,类池使所装载的类可以通过 Javassist API 作为数据使⽤。可以使⽤默认的类池,它是从 JVM 搜索路径中装载的,也可以定义⼀个搜索您⾃⼰的路径列表的类池。甚⾄可以直接从字节数组或者流中装载⼆进制类,以及从头开始创建新类。
装载到类池中的类由 javassist.CtClass 实例表⽰。与标准的 Java java.lang.Class 类⼀样, CtClass 提供了检查类数据(如字段和⽅法)的⽅法。不过,这只是 CtClass 的部分内容,它还定义了在类中添加新字段、⽅法和构造函数、以及改变类、⽗类和接⼝的⽅法。奇怪的是,Javassist 没有提供删除⼀个类中字段、⽅法或者构造函数的任何⽅法。
字段、⽅法和构造函数分别由 javassist.CtField、 javassist.CtMethod 和 javassist.CtConstructor 的实例表⽰。这些类定义了修改由它们所表⽰的对象的所有⽅法的⽅法,包括⽅法或者构造函数中的实际字节码内容。
所有字节码的源代码
Javassist 让您可以完全替换⼀个⽅法或者构造函数的字节码正⽂,或者在现有正⽂的开始或者结束位置选择性地添加字节码(以及在构造函数中添加其他⼀些变量)。不管是哪种情况,新的字节码都作为类 Java 的源代码声明或者 String 中的块传递。Javassist ⽅法将您提供的源代码⾼效地编译为 Java 字节码,然后将它们插⼊到⽬标⽅法或者构造函数的正⽂中。
Javassist 接受的源代码与 Java 语⾔的并不完全⼀致,不过主要的区别只是增加了⼀些特殊的标识符,⽤于表⽰⽅法或者构造函数参数、⽅法返回值和其他在插⼊的代码中可能⽤到的内容。这些特殊标识符以符号 $ 开头,所以它们不会⼲扰代码中的其他内容。
对于在传递给 Javassist 的源代码中可以做的事情有⼀些限制。第⼀项限制是使⽤的格式,它必须是单条语句或者块。在⼤多数情况下这算不上是限制,因为可以将所需要的任何语句序列放到块中。下⾯是⼀个使⽤特殊 Javassist 标识符表⽰⽅法中前两个参数的例⼦,这个例⼦⽤来展⽰其使⽤⽅法:
{
System.out.println("Argument 1: " + $1);
System.out.println("Argument 2: " + $2);
}Show moreShow more icon
对于源代码的⼀项更实质性的限制是不能引⽤在所添加的声明或者块外声明的局部变量。这意味着如果在⽅法开始和结尾处都添加了代码,那么⼀般不能将在开始处添加的代码中的信息传递给在结尾处添加的代码。有可能绕过这项限制,但是绕过是很复杂的 — 通常需要设法将分别插⼊的代码合并为⼀个块。
⽤ Javassist 进⾏ Classworking
作为使⽤ Javassist 的⼀个例⼦,我将使⽤⼀个通常直接在源代码中处理的任务:测量执⾏⼀个⽅法所花费的时间。这在源代码中可以容易地完成,只要在⽅法开始时记录当前时间、之后在⽅法结束时再次检查当前时间并计算两个值的差。如果没有源代码,那么得到这种计时信息就要困难得多。这就是 classworking ⽅便的地⽅ — 它让您对任何⽅法都可以作这种改变,并且不需要有源代码。
清单 1 显⽰了⼀个(不好的)⽰例⽅法,我⽤它作为我的计时试验的实验品: StringBuilder 类的 buildString ⽅法。这个⽅法使⽤⼀种所有Java 性能优化的⾼⼿都会叫您 不 要使⽤的⽅法构造⼀个具有任意长度的 String— 它通过反复向字符串的结尾附加单个字符来产⽣更长的字符串。因为字符串是不可变的,所以这种⽅法意味着每次新的字符串都要通过⼀个循环来构造:使⽤从⽼的字符串中拷贝的数据并在结尾添加新的字符。最终的效果是⽤这个⽅法产⽣更长的字符串时,它的开销越来越⼤。
清单 1. 需要计时的⽅法
public class StringBuilder
{
private String buildString(int length) {
String result = "";
for (int i = 0; i < length; i++) {
result += (char)(i%26 + 'a');
}
return result;
}
public static void main(String[] argv) {
StringBuilder inst = new StringBuilder();
for (int i = 0; i < argv.length; i++) {
String result = inst.buildString(Integer.parseInt(argv[i]));
System.out.println("Constructed string of length " +
result.length());
}
}
}Show moreShow more icon
添加⽅法计时
因为有这个⽅法的源代码,所以我将为您展⽰如何直接添加计时信息。它也作为使⽤ Javassist 时的⼀个模型。清单 2 只展⽰了buildString() ⽅法,其中添加了计时功能。这⾥没有多少变化。添加的代码只是将开始时间保存为局部变量,然后在⽅法结束时计算持续时间并打印到控制台。
清单 2. 带有计时的⽅法
private String buildString(int length) {
long start = System.currentTimeMillis();
String result = "";
for (int i = 0; i < length; i++) {
result += (char)(i%26 + 'a');
}
System.out.println("Call to buildString took " +
(System.currentTimeMillis()-start) + " ms.");
return result;
}Show moreShow more icon
java replace方法⽤ Javassist 来做
来做 使⽤ Javassist 操作类字节码以得到同样的效果看起来应该不难。Javassist 提供了在⽅法的开始和结束位置添加代码的⽅法,别忘了,我在为该⽅法中加⼊计时信息就是这么做的。
不过,还是有障碍。在描述 Javassist 是如何让您添加代码时,我提到添加的代码不能引⽤在⽅法中其他地⽅定义的局部变量。这种限制使我不能在 Javassist 中使⽤在源代码中使⽤的同样⽅法实现计时代码,在这种情况下,我在开始时添加的代码中定义了⼀个新的局部变量,并在结束处添加的代码中引⽤这个变量。
那么还有其他⽅法可以得到同样的效果吗?是的,我 可以 在类中添加⼀个新的成员字段,并使⽤这个字段⽽不是局部变量。不过,这是⼀种糟糕的解决⽅案,在⼀般性的使⽤中有⼀些限制。例如,考虑在⼀个递归⽅法中会发⽣的事情。每次⽅法调⽤⾃⾝时,上次保存的开始时间值就会被覆盖并且丢失。
幸运的是有⼀种更简洁的解决⽅案。我可以保持原来⽅法的代码不变,只改变⽅法名,然后⽤原来的⽅法名增加⼀个新⽅法。这个 (interceptor)⽅法可以使⽤与原来⽅法同样的签名,包括返回同样的值。清单 3 展⽰了通过这种⽅法改编后源代码看上去的样⼦:
清单 3. 在源代码中添加⼀个⽅法
private String buildString$impl(int length) {
String result = "";
for (int i = 0; i < length; i++) {
result += (char)(i%26 + 'a');
}
return result;
}
private String buildString(int length) {
long start = System.currentTimeMillis();
String result = buildString$impl(length);
System.out.println("Call to buildString took " +
(System.currentTimeMillis()-start) + " ms.");
return result;
}Show moreShow more icon
通过 Javassist 可以很好地利⽤这种使⽤⽅法的⽅法。因为整个⽅法是⼀个块,所以我可以毫⽆问题地在正⽂中定义并且使⽤局部变量。为⽅法⽣成源代码也很容易 — 对于任何可能的⽅法,只需要⼏个替换。
运⾏拦截
实现添加⽅法计时的代码要⽤到在 Javassist 基础 中描述的⼀些 Javassist API。清单 4 展⽰了该代码,它是⼀个带有两个命令⾏参数的应⽤程序,这两个参数分别给出类名和要计时的⽅法名。 main() ⽅法的正⽂只给出类信息,然后将它传递给 addTiming() ⽅法以处理实际的修改。 addTiming() ⽅法⾸先通过在名字后⾯附加”$impl” 重命名现有的⽅法,接着⽤原来的⽅法名创建该⽅法的⼀个拷贝。然后它⽤含有对经过重命名的原⽅法的调⽤的计时代码替换拷贝⽅法的正⽂。
清单 4. ⽤ Javassist 添加⽅法
public class JassistTiming
{
public static void main(String[] argv) {
if (argv.length == 2) {
try {
// start by getting the class file and method
CtClass clas = Default().get(argv[0]);
if (clas == null) {
} else {
// add timing interceptor to the class
addTiming(clas, argv[1]);
clas.writeFile();
System.out.println("Added timing to method " +
argv[0] + "." + argv[1]);
}
} catch (CannotCompileException ex) {
ex.printStackTrace();
} catch (NotFoundException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
} else {
System.out.println("Usage: JassistTiming class method-name"); }
}
private static void addTiming(CtClass clas, String mname) throws NotFoundException, CannotCompileException {
// get the method information (throws exception if method with // given name is not declared directly by this class, returns
// arbitrary choice if more than one with the given name) CtMethod mold = DeclaredMethod(mname);
// rename old method to synthetic name, then duplicate the
// method with original name for use as interceptor
String nname = mname+"$impl";
mold.setName(nname);
CtMethod mnew = py(mold, mname, clas, null); // start the body text generation by saving the start time
// to a local variable, then call the timed method; the
// actual code generated needs to depend on whether the
// timed method returns a value
String type = ReturnType().getName();
StringBuffer body = new StringBuffer();
body.append("{\nlong start = System.currentTimeMillis();\n");
if (!"void".equals(type)) {
body.append(type + " result = ");
}
body.append(nname + "($$);\n");
// finish body text generation with call to print the timing
// information, and return saved value (if not void)
body.append("System.out.println(\"Call to method " + mname + " took \" +\n (System.currentTimeMillis()-start) + " +
"\" ms.\");\n");
if (!"void".equals(type)) {
body.append("return result;\n");
}
body.append("}");
// replace the body of the interceptor method with generated
// code block and add it to class
mnew.String());
clas.addMethod(mnew);
// print the generated code block just to show what was done System.out.println("Interceptor method body:");
System.out.String());
}
}Show moreShow more icon

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。