| | | | | | | [文章信息] | | | 作者: | 陶刚编译 | | 时间: | 2004-12-24 | | 出处: | 天极网 | | 责任编辑: | 方舟 | |
| [文章导读] | | | 在本文中我将考虑使用嵌入GUI应用程序中的状态条组件的情形 | |
| |
|
| | | |
|
|
|
|
|
取样(Sampling)
我现在有办法把StackTraceElement数组转换为@Status注解堆栈。这种操作比表明看到的更加有用。Java 1.5中的另一个新特性--线程反省(introspection)--使我们能够从当前正在运行的线程中得到准确的StackTraceElement数组。有了这两部分信息之后,我们就可以构造JstatusBar的另一种实现。StatusManager将不会在发生方法调用的时候接收通知,而是简单地启动一个附加的线程,让它负责在正常的间隔期间抓取堆栈跟踪信息和每个步骤的状态。只要这个间隔期间足够短,用户就不会感觉到更新的延迟。
下面使"sampler"线程背后的代码,它跟踪另一个线程的经过:
class StatusSampler implements Runnable { private Thread watchThread;
public StatusSampler (Thread watchThread) { this.watchThread = watchThread; }
public void run () { while (watchThread.isAlive()) { // 从线程中得到堆栈跟踪信息 StackTraceElement[] stackTrace =watchThread.getStackTrace(); // 从堆栈跟踪信息中提取状态消息 List<Status> statusList =StatusFinder.getStatus(stackTrace); Collections.reverse(statusList); // 用状态消息建立某种状态 StatusState state = new StatusState(); for (Status s : statusList) { String message = s.value(); state.push(message); }
// 更新当前的状态 StatusManager.setState(watchThread,state); //休眠到下一个周期 try { Thread .sleep(SAMPLING_DELAY); } catch (InterruptedException ex) {} }
//状态复位 StatusManager.setState(watchThread,new StatusState()); } } | 与增加方法调用、手动或通过重构相比,取样对程序的侵害性(invasive)更小。我根本不需要改变建立过程或命令行参数,或修改启动过程。它也允许我通过调整SAMPLING_DELAY来控制占用的开销。不幸的是,当方法调用开始或结束的时候,这种方法没有明确的回调。除了状态更新的延迟之外,没有原因要求这段代码在那个时候接收回调。但是,未来我能够增加一些额外的代码来跟踪每个方法的准确的运行时。通过检查StackTraceElement是可以精确地实现这样的操作的。
通过线程取样实现JStatusBar的代码可以在peekinginside-pt2.tar.gz文件的/code/05_sampling目录中找到。
在执行过程中重构字节码
通过把取样的方法与重构组合在一起,我能够形成一种最终的实现,它提供了各种方法的最佳特性。默认情况下可以使用取样,但是应用程序的花费时间最多的方法可以被个别地进行重构。这种实现根本不会安装ClassTransformer,但是作为代替,它会一次一个地重构方法以响应取样过程中收集到的数据。
为了实现这种功能,我将建立一个新类InstrumentationManager,它可以用于重构和不重构独立的方法。它可以使用新的Instrumentation.redefineClasses方法来修改空闲的类,同时代码则可以不间断执行。前面部分中增加的StatusSampler线程现在有了额外的职责,它把任何自己"发现"的@Status方法添加到集合中。它将周期性地找出最坏的冒犯者并把它们提供给InstrumentationManager以供重构。这允许应用程序更加精确地跟踪每个方法的启动和终止时刻。
前面提到的取样方法的一个问题是它不能区分长时间运行的方法与在循环中多次调用的方法。由于重构会给每次方法调用增加一定的开销,我们有必要忽略频繁调用的方法。幸运的是,我们可以使用重构解决这个问题。除了简单地更新StatusManager之外,我们将维护每个重构的方法被调用的次数。如果这个数值超过了某个极限(意味着维护这个方法的信息的开销太大了),取样线程将会永远地取消对该方法的重构。
理想情况下,我将把每个方法的调用数量存储在重构过程中添加到类的新字段中。不幸的是,Java 1.5中增加的类转换机制不允许这样操作;它不能增加或删除任何字段。作为代替,我将把这些信息存储在新的CallCounter类的Method对象的静态映射中。
这种混合的方法可以在示例代码的/code/06_dynamic目录中找到。
概括
图4提供了一个矩形,它显示了我给出的例子相关的特性和代价。
 图4.重构方法的分析 | 你可以发现,动态的(Dynamic)方法是各种方案的良好组合。与使用重构的所有示例类似,它提供了方法开始或终止时刻的明确的回调,因此你的应用程序可以准确地跟踪运行时并立即为用户提供反馈信息。但是,它还能够取消某种方法的重构(它被过于频繁地调用),因此它不会受到其它的重构方案遇到的性能问题的影响。它没有包含编译时步骤,并且它没有增加类载入过程中的额外的工作。
未来的趋势
我们可以给这个项目增加大量的附件特性,使它更加适用。其中最有用的特性可能是动态的状态信息。我们可以使用新的java.util.Formatter类把类似printf的模式替换(pattern substitution)用于@Status消息中。例如,我们的connectToDB(String url)方法中的@Status("Connecting to %s")注解可以把URL作为消息的一部分报告给用户。
在源代码重构的帮助下,这可能显得微不足道,因为我将使用的Formatter.format方法使用了可变参数(Java 1.5中增加的"魔术"功能)。重构过的版本类似下面的情形:
public void connectToDB (String url) { Formatter f = new Formatter(); String message = f.format("Connecting to %s", url);
StatusManager.push(message); try { ... } finally { StatusManager.pop(); } } | 不幸的是,这种"魔术"功能是完全在编译器中实现的。在字节码中,Formatter.format把Object[]作为参数,编译器明确地添加代码来包装每个原始的类型并装配该数组。如果BCEL没有加紧弥补,而我又需要使用字节码重构,我将不得不重新实现这种逻辑。
由于它只能用于重构(这种情况下方法参数是可用的)而不能用于取样,你可能希望在启动的时候重构这些方法,或最少使动态实现偏向于任何方法的重构,还可以在消息中使用替代模式。
你还可以跟踪每个重构的方法调用的启动次数,因此你还可以更加精确地报告每个方法的运行次数。你甚至于可以保存这些次数的历史统计数据,并使用它们形成一个真正的进度条(代替我使用的不确定的版本)。这种能力将赋予你在运行时重构某种方法的一个很好的理由,因为跟踪任何独立的方法的开销都是很能很明显的。
你可以给进度条增加"调试"模式,它不管方法调用是否包含@Status注解,报告取样过程中出现的所有方法调用。这对于任何希望调试死锁或性能问题的开发者来说都是无价之宝。实际上,Java 1.5还为死锁(deadlock)检测提供了一个可编程的API,在应用程序锁住的时候,我们可以使用该API把进程条变成红色。
本文中建立的基于注解的重构框架组件可能很有市场。一个允许字节码在编译时(通过Ant事务)、启动时(使用ClassTransformer)和执行过程中(使用Instrumentation)进行重构的工具对于少量其它新项目来说毫无疑问地非常有价值。
总结
在这几个例子中你可以看到,元数据编程(meta-programming)可能是一种非常强大的技术。报告长时间运行的操作的进程仅仅是这种技术的应用之一,而我们的JStatusBar仅仅是沟通这些信息的一种媒介。我们可以看到,Java 1.5中提供的很多新特性为元数据编程提供了增强的支持。特别地,把注解和运行时重构组合在一起为面向属性的编程提供了真正动态的形式。我们可以进一步使用这些技术,使它的功能超越已有的框架组件(例如XDoclet提供的框架组件的功能)。
|
|
|
|
|
|
|
|
|