在类载入时重构字节码
更好的方法可能是延迟字节码重构操作,直到字节码被载入的时候才进行重构。使用这种方法的时候,重构的字节码不用保存起来。我们的应用程序启动时刻的性能可能会受到影响,但是你却可以基于自己的系统属性或运行时配置数据来控制进行什么操作。
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.显示在错误对话框中的堆栈跟踪信息
为了实现这些功能,我必须对已有的代码进行稍微的修改。首先,为了确保在运行时@Status注解是可以看到的,我就必须再次更新@Retention,把它设置为@Retention(RetentionPolicy.RUNTIME)。请记住,@Retention控制着JVM什么时候抛弃注解信息。这样的设置意味着注解不仅可以被编译器插入字节码中,还能够使用新的Method.getAnnotation(Class)方法通过反射来进行访问。
现在我需要安排接收代码中没有明确处理的任何异常的通知了。在Java 1.4中,处理任何特定线程上未处理异常的最好方法是使用ThreadGroup子类并给该类型的ThreadGroup添加自己的新线程。但是Java 1.5提供了额外的功能。我可以定义UncaughtExceptionHandler接口的一个实例,并为任何特定的线程(或所有线程)注册它。
请注意,在例子中为特定异常注册可能更好,但是在Java 1.5.0beta1(#4986764)中有一个bug,它使这样操作无法进行。但是为所有线程设置一个处理程序是可以工作的,因此我就这样操作了。
现在我们拥有了一种截取未处理异常的方法了,并且这些异常必须被报告给用户。在GUI应用程序中,典型情况下这样的操作是通过弹出一个包含整个堆栈跟踪信息或简单消息的模式对话框来实现的。在例子中,我希望在产生异常的时候显示一个消息,但是我希望提供堆栈的@Status描述而不是类和方法的名称。为了实现这个目的,我简单地在Thread的StackTraceElement数组中查询,找到与每个框架相关的java.lang.reflect.Method对象,并查询它的堆栈注解列表。不幸的是,它只提供了方法的名称,没有提供方法的特征量(signature),因此这种技术不支持名称相同的(但@Status注解不同的)重载方法。
实现这种方法的示例代码可以在peekinginside-pt2.tar.gz文件的/code/04_exceptions目录中找到。
|
|