体验Java 1.5中面向方面(AOP)编程
副标题#e#
对付一个可以或许会见源代码的履历富厚的Java开拓人员来说,任何措施都可以被看作是博物馆里透明的模子。雷同线程转储(dump)、要领挪用跟踪、断点、切面(profiling)统计表等东西可以让我们相识措施今朝正在执行什么操纵、适才做了什么操纵、将来将做什么操纵。可是在产物情况中环境就没有那么明明晰,这些东西一般是不可以或许利用的,或最多只能由受过练习的开拓者利用。支持团队和最终用户也需要知道在某个时刻应用措施正在执行什么操纵。
为了填补这个空白,我们已经发现了一些简朴的替代品,譬喻日志文件(典范环境下用于处事器处理惩罚)和状态条(用于GUI应用措施)。可是,由于这些东西只能捕获和陈诉可用信息的一个很小的子集,而且凡是必需把这些信息用容易领略的方法表示出来,所以措施员趋向于把它们明晰地编写到应用措施中。而这些代码会缠绕着应用措施的业务逻辑,当开拓者试图调试或相识焦点成果的时候,他们必需"环绕这些代码事情",并且还要记得成果产生改变后更新这些代码。我们但愿实现的真正成果是把状态陈诉会合在某个位置,把单个状态动静作为元数据(metadata)来打点。
在本文中我将思量利用嵌入GUI应用措施中的状态条组件的景象。我将先容多种实现这种状态陈诉的差异要领,从传统的硬编码习惯开始。随后我会先容Java 1.5的大量新特性,包罗注解(annotation)和运行时字节码重构(instrumentation)。
状态打点器(StatusManager)
我的主要方针是成立一个可以嵌入GUI应用措施的JStatusBar Swing组件。图1显示了一个简朴的Jframe中状态条的样式。
图1.我们动态生成的状态条
由于我不但愿直接在业务逻辑中引用任何GUI组件,我将成立一个StatusManager(状态打点器)来充当状态更新的进口点。实际的通知会被委托给StatusState工具,因此今后可以扩展它以支持多个并发的线程。图2显示了这种布置。
图2. StatusManager和JstatusBar
此刻我必需编写代码挪用StatusManager的要领来陈诉应用措施的历程。典范环境下,这些要领挪用都分手地贯串于try-finally代码块中,凡是每个要领一个挪用。
public void connectToDB (String url) {
StatusManager.push("Connecting to database");
try {
...
} finally {
StatusManager.pop();
}
}
这些代码实现了我们所需要成果,可是在代码库中数十次、甚至于数百次地复制这些代码之后,它看起来就有些杂乱了。另外,假如我们但愿用一些其它的方法会见这些动静该怎么办呢?在本文的后头部门中,我将界说一个用户友好的异常处理惩罚措施,它共享了沟通的动静。问题是我把状态动静埋没在要领的实现之中了,而没有把动静放在动静所属的接口中。
#p#副标题#e#
面向属性编程
我真正想实现的操纵是把对StatusManager的引用都放到代码外面的某个处所,并简朴地用我们的动静标志这个要领。接着我可以利用代码生成(code-generation)或运行时反省(introspection)来执行真正的事情。XDoclet项目把这种要领归纳为面向属性编程(Attribute-Oriented Programming),它还提供了一个框架组件,可以把自界说的雷同Javadoc的标志转换到源代码之中。
可是,JSR-175包括了这样的内容,Java 1.5为了包括真实代码中的这些属性提供了一种布局化水平更高的名目。这些属性被称为"注解(annotations)",我们可以利用它们为类、要领、字段或变量界说提供元数据。它们必需被显式声明,并提供一组可以包括任意常量值(包罗原语、字符串、列举和类)的名称-值对(name-value pair)。
注解(Annotations)
为了处理惩罚状态动静,我但愿界说一个包括字符串值的新注解。注解的界说很是雷同接口的界说,可是它用@interface要害字取代了interface,而且只支持要领(尽量它们的成果更像字段):
public @interface Status {
String value();
}
与接口雷同,我把@interface放入一个叫做Status.java的文件中,并把它导入到任何需要引用它的文件中。
对我们的字段来说,value大概是个奇怪的名称。雷同message的名称大概更适合;可是,value对付Java来说具有非凡的意义。它答允我们利用@Status("…")取代@Status(value="…")来界说注解,这明明越发简便。
我此刻可以利用下面的代码界说本身的要领:
@Status("Connecting to database")
public void connectToDB (String url) {
...
}
#p#分页标题#e#
请留意,我们在编译这段代码的时候必需利用-source 1.5选项。假如你利用Ant而不是直接利用javac呼吁行成立应用措施,那么你需要利用Ant 1.6.1以上版本。
作为类、要领、字段和变量的增补,注解也可以用于为其它的注解提供元数据。出格地,Java引入了少量注解,你可以利用这些注解来定制你本身的注解的事情方法。我们用下面的代码从头界说本身的注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Status {
String value();
}
@Target注解界说了@Status注解可以引用什么内容。抱负环境下,我但愿标志大块的代码,可是它的选项只有要领、字段、类、当地变量、参数和其它注解。我只对代码感乐趣,因此我选择了METHOD(要领)。
@Retention注解答允我们指定Java什么时候可以自主地丢弃动静。它大概是SOURCE(在编译时丢弃)、CLASS(在类载入时丢弃)或RUNTIME(不丢弃)。我们先选择SOURCE,可是在本文后部我们会更新它。
重构源代码
此刻我的动静都被编码放入元数据中了,我必需编写一些代码来通知状态监听措施。假设在某个时候,我继承把connectToDB要领生存源代码控件中,可是却没有对StatusManager的任何引用。可是,在编译这个类之前,我但愿插手一些须要的挪用。也就是说,我但愿自动地插入try-finally语句和push/pop挪用。
XDoclet框架组件是一种Java源代码生成引擎,它利用了雷同上述的注解,可是把它们存储在Java源代码的注释(comment)中。XDoclet生成整个Java类、设置文件或其它成立的部门的时候很是完美,可是它不支持对已有Java类的修改,而这限制了重构的有效性。作为取代,我可以利用阐明东西(譬喻JavaCC或ANTLR,它提供了阐明Java源代码的语法基本),可是这需要耗费大量精神。
看起来没有什么可以用于Java代码的源代码重构的很好的东西。这类东西大概有市场,可是你在本文的后头部门可以看到,字节码重构大概是一种更强大的技能。 重构字节码
不是重构源代码然后编译它,而是编译原始的源代码,然后重构它所发生的字节码。这样的操纵大概比源代码重构更容易,也大概越发巨大,而这依赖于需要的精确转换。字节码重构的主要利益是代码可以在运行时被修改,不需要利用编译器。
尽量Java的字节码名目相对简朴,我照旧但愿利用一个Java类库来执行字节码的阐明和生成(这可以把我们与将来Java类文件名目标改变隔分开来)。我选择了利用Jakarta的Byte Code Engineering Library(字节码引擎类库,BCEL),可是我还可以选用CGLIB、ASM或SERP。
由于我将利用多种差异的方法重构字节码,我将从声明重构的通用接口开始。它雷同于执行基于注解重构的简朴框架组件。这个框架组件基于注解,将支持类和要领的转换,因此该接口有雷同下面的界说:
public interface Instrumentor
{
public void instrumentClass (ClassGen classGen,Annotation a);
public void instrumentMethod (ClassGen classGen,MethodGen methodGen,Annotation a);
}
ClassGen和MethodGen都是BCEL类,它们利用了Builder模式(pattern)。也就是说,它们为改变其它不行变的(immutable)工具、以及可变的和不行变的表示(representation)之间的转换提供了要领。
此刻我需要为接口编写实现,它必需用得当的StatusManager挪用改换@Status注解。前面提到,我但愿把这些挪用包括在try-finally代码块中。请留意,要到达这个方针,我们所利用的注解必需用@Retention(RetentionPolicy.CLASS)举办标志,它指示Java编译器在编译进程中不要丢弃注解。由于在前面我把@Status声明为@Retention(RetentionPolicy.SOURCE)的,我必需更新它。
在这种环境下,重构字节码明明比重构源代码更巨大。其原因在于try-finally是一种仅仅存在于源代码中的观念。Java编译器把try-finally代码块转换为一系列的try-catch代码块,并在每一个返回之前插入对finally代码块的挪用。因此,为了把try-finally代码块添加到已有的字节码中,我也必需执行雷同的事务。
下面是表示一个普通要领挪用的字节码,它被StatusManager更新环抱着:
0: ldc #2; //字符串动静
2: invokestatic #3; //要领StatusManager.push:(LString;)V
5: invokestatic #4; //要领 doSomething:()V
8: invokestatic #5; //要领 StatusManager.pop:()V
11: return
下面是沟通的要领挪用,可是位于try-finally代码块中,因此,假如它发生了异常会挪用StatusManager.pop():
#p#分页标题#e#
0: ldc #2; //字符串动静
2: invokestatic #3; //要领 StatusManager.push:(LString;)V
5: invokestatic #4; //要领 doSomething:()V
8: invokestatic #5; //要领 StatusManager.pop:()V
11: goto 20
14: astore_0
15: invokestatic #5; //要领 StatusManager.pop:()V
18: aload_0
19: athrow
20: return
Exception table:
from to target type
5 8 14 any
14 15 14 any
你可以发明,为了实现一个try-finally,我必需复制一些指令,并添加了几个跳转和异常表记录。幸运的是,BCEL的InstructionList类使这种事情相当简朴。
在运行时重构字节码
此刻我拥有了一个基于注解修改类的接口和该接口的详细实现了,下一步是编写挪用它的实际框架组件。实际上我将编写少量的框架组件,先从运行时重构所有类的框架组件开始。由于这种操纵会在build进程中产生,我抉择为它界说一个Ant事务。build.xml文件中的重构方针的声明应该如下:
<instrument class="com.pkg.OurInstrumentor">
<fileset dir="$(classes.dir)">
<include name="**/*.class"/>
</fileset>
</instrument>
为了实现这种事务,我必需界说一个实现org.apache.tools.ant.Task接口的类。我们的事务的属性和子元素(sub-elements)都是通过set和add要领挪用通报进来的。我们挪用执行(execute)要领来实现事务所要执行的事情–在示例中,就是重构<fileset>中指定的类文件。
public class InstrumentTask extends Task {
...
public void setClass (String className) { ... }
public void addFileSet (FileSet fileSet) { ... }
public void execute () throws BuildException {
Instrumentor inst = getInstrumentor();
try {
DirectoryScanner ds =fileSet.getDirectoryScanner(project);
// Java 1.5 的"for" 语法
for (String file : ds.getIncludedFiles()) {
instrumentFile(inst, file);
}
} catch (Exception ex) {
throw new BuildException(ex);
}
}
...
}
用于该项操纵的BCEL 5.1版本有一个问题–它不支持阐明注解。我可以载入正在重构的类并利用反射(reflection)查察注解。可是,假如这样,我就不得不利用RetentionPolicy.RUNTIME来取代RetentionPolicy.CLASS。我还必需在这些类中执行一些静态的初始化,而这些操纵大概载入当地类库或引入其它的依赖干系。幸运的是,BCEL提供了一种插件(plugin)机制,它答允客户端阐明字节码属性。我编写了本身的AttributeReader的实现(implementation),在呈现注解的时候,它知道如何阐明插入字节码中的RuntimeVisibleAnnotations和RuntimeInvisibleAnnotations属性。BCEL将来的版本应该会包括这种成果而不是作为插件提供。
编译时刻的字节码重构要领显示在示例代码的code/02_compiletime目次中。
可是这种要领有许多缺陷。首先,我必需给成立进程增加特另外步调。我不能基于呼吁行配置或其它编译时没有提供的信息来抉择打开或封锁重构操纵。假如重构的或没有重构的代码需要同时在产物情况中运行,那么就必需成立两个单独的.jars文件,并且还必需抉择利用哪一个。
在类载入时重构字节码
更好的要领大概是延迟字节码重构操纵,直到字节码被载入的时候才举办重构。利用这种要领的时候,重构的字节码不消生存起来。我们的应用措施启动时刻的机能大概会受到影响,可是你却可以基于本身的系统属性或运行时设置数据来节制举办什么操纵。
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
#p#分页标题#e#
此刻我已经布置了一个回调(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目次中找到。
取样(Sampling)
#p#分页标题#e#
我此刻有步伐把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.重构要领的阐明
#p#分页标题#e#
你可以发明,动态的(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提供的框架组件的成果)。