在类载入时重构字节码
更好的方法可能是延迟字节码重构操作,直到字节码被载入的时候才进行重构。使用这种方法的时候,重构的字节码不用保存起来。我们的应用程序启动时刻的性能可能会受到影响,但是你却可以基于自己的系统属性或运行时配置数据来控制进行什么操作。
Java 1.5之前,我们使用定制的类载入程序可能实现这种类文件维护操作。但是Java 1.5中新增加的java.lang.instrument程序包提供了少数附加的工具。特别地,它定义了ClassFileTransformer的概念,在标准的载入过程中我们可以使用它来重构一个类。
为了在适当的时候(在载入任何类之前)注册ClassFileTransformer,我需要定义一个premain方法。Java在载入主类(main class)之前将调用这个方法,并且它传递进来对Instrumentation对象的引用。我还必须给命令行增加-javaagent参数选项,告诉Java我们的premain方法的信息。这个参数选项把我们的agent class(代理类,它包含了premain方法)的全名和任意字符串作为参数。在例子中我们把Instrumentor类的全名作为参数(它必须在同一行之中):
-javaagent:boxpeeking.instrument.InstrumentorAdaptor=
boxpeeking.status.instrument.StatusInstrumentor
现在我已经安排了一个回调(callback),它在载入任何含有注解的类之前都会发生,并且我拥有Instrumentation对象的引用,可以注册我们的ClassFileTransformer了:
public static void premain (String className,
Instrumentation i)
throws ClassNotFoundException,
InstantiationException,
IllegalAccessException
{
Class instClass = Class.forName(className);
Instrumentor inst = (Instrumentor)instClass.newInstance();
i.addTransformer(new InstrumentorAdaptor(inst));
}
我们在此处注册的适配器将充当上面给出的Instrumentor接口和Java的ClassFileTransformer接口之间的桥梁。
public class InstrumentorAdaptor
implements ClassFileTransformer
{
public byte[] transform (ClassLoader cl,String className,Class classBeingRedefined,
ProtectionDomain protectionDomain,byte[] classfileBuffer)
{
try {
ClassParser cp =new ClassParser(new ByteArrayInputStream(classfileBuffer),className + ".java");
JavaClass jc = cp.parse();
ClassGen cg = new ClassGen(jc);
for (Annotation an : getAnnotations(jc.getAttributes())) {
instrumentor.instrumentClass(cg, an);
}
for (org.apache.bcel.classfile.Method m : cg.getMethods()) {
for (Annotation an : getAnnotations(m.getAttributes())) {
ConstantPoolGen cpg =cg.getConstantPool();
MethodGen mg =new MethodGen(m, className, cpg);
instrumentor.instrumentMethod(cg, mg, an);
mg.setMaxStack();
mg.setMaxLocals();
cg.replaceMethod(m, mg.getMethod());
}
}
JavaClass jcNew = cg.getJavaClass();
return jcNew.getBytes();
} catch (Exception ex) {
throw new RuntimeException("instrumenting " + className, ex);
}
}
...
}
这种在启动时重构字节码的方法位于在示例的/code/03_startup目录中。
异常的处理
文章前面提到,我希望编写附加的代码使用不同目的的@Status注解。我们来考虑一下一些额外的需求:我们的应用程序必须捕捉所有的未处理异常并把它们显示给用户。但是,我们不是提供Java堆栈跟踪,而是显示拥有@Status注解的方法,而且还不应该显示任何代码(类或方法的名称或行号等等)。
例如,考虑下面的堆栈跟踪信息:
java.lang.RuntimeException: Could not load data for symbol IBM
at boxpeeking.code.YourCode.loadData(Unknown Source)
at boxpeeking.code.YourCode.go(Unknown Source)
at boxpeeking.yourcode.ui.Main+2.run(Unknown Source)
at java.lang.Thread.run(Thread.java:566)
Caused by: java.lang.RuntimeException: Timed out
at boxpeeking.code.YourCode.connectToDB(Unknown Source)
... 更多信息
这将导致图1中所示的GUI弹出框,上面的例子假设你的YourCode.loadData()、YourCode.go()和YourCode.connectToDB()都含有@Status注解。请注意,异常的次序是相反的,因此用户最先得到的是最详细的信息。
图3.显示在错误对话框中的堆栈跟踪信息