体验Java 1.5中面向(AOP)编程

AOP

  对于一个能够访问源代码的经验丰富的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();
   }
  }

  这些代码实现了我们所需要功能,但是在代码库中数十次、甚至于数百次地复制这些代码之后,它看起来就有些混乱了。此外,如果我们希望用一些其它的方式访问这些消息该怎么办呢?在本文的后面部分中,我将定义一个用户友好的异常处理程序,它共享了相同的消息。问题是我把状态消息隐藏在方法的实现之中了,而没有把消息放在消息所属的接口中。

  面向属性编程

  我真正想实现的操作是把对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) {
  ...
  }

  请注意,我们在编译这段代码的时候必须使用-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():  

  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文件,而且还必须决定使用哪一个。

时间: 2024-10-12 11:08:57

体验Java 1.5中面向(AOP)编程的相关文章

体验Java 1.5中面向方面(AOP)编程

编程 摘自久久学院对于一个能够访问源代码的经验丰富的Java开发人员来说,任何程序都可以被看作是博物馆里透明的模型.类似线程转储(dump).方法调用跟踪.断点.切面(profiling)统计表等工具可以让我们了解程序目前正在执行什么操作.刚才做了什么操作.未来将做什么操作.但是在产品环境中情况就没有那么明显了,这些工具一般是不能够使用的,或最多只能由受过训练的开发者使用.支持团队和最终用户也需要知道在某个时刻应用程序正在执行什么操作. 为了填补这个空缺,我们已经发明了一些简单的替代品,例如日志

Spring Framework中的AOP编程之入门篇

编程 作为这个介绍Spring框架中的面向方面编程(Aspect-Oriented Programming,AOP)的系列的第一部分,本文介绍了使您可以使用Spring中的面向方面特性进行快速开发的基础知识.使用跟踪和记录方面(面向方面领域的HelloWorld)作为例子,本文展示了如何使用Spring框架所独有的特性来声明切入点和通知以便应用方面.本系列的第二部分将更深入地介绍如何运用Spring中的所有通知类型和切入点来实现更实用的方面和面向方面设计模式. 本文的目的不是要介绍构成模块化J2

Java实现AOP面向切面编程的实例教程_java

介绍 众所周知,AOP(面向切面编程)是Spring框架的特色功能之一.通过设置横切关注点(cross cutting concerns),AOP提供了极高的扩展性.那AOP在Spring中是怎样运作的呢?当你只能使用core java,却需要AOP技术时,这个问题的解答变得极为关键.不仅如此,在高级技术岗位的面试中,此类问题也常作为考题出现.这不,我的朋友最近参加了一个面试,就被问到了这样一个棘手的问题--如何在不使用Spring及相关库,只用core Java的条件下实现AOP.因此,我将在

AOP面向切编程及在Spring中的使用方法

AOP 简介 AOP(Aspect-Oriented Programming, 面向切面编程): 是一种新的方法论, 是对传统的 OOP(Object-Oriented Programming, 面向对象编程)的补充 AOP 的主要编程对象是切面(aspect) 在应用 AOP 编程时, 仍然需要定义公共功能, 但可以明确的定义这个功能在哪里, 以什么方式应用, 并且不必修改受影响的类 AOP 的好处: 每个事物逻辑位于一个位置, 代码不分散, 便于维护和升级 业务模块更简洁, 只包含核心业务代

在.NET项目中使用PostSharp,实现AOP面向切面编程处理

PostSharp是一种Aspect Oriented Programming 面向切面(或面向方面)的组件框架,适用在.NET开发中,本篇主要介绍Postsharp在.NET开发中的相关知识,以及一些如日志.缓存.事务处理.异常处理等常用的切面处理操作. AOP(Aspect-Oriented Programming)是一种将函数的辅助性功能与业务逻辑相分离的编程泛型(programming paradigm),其目的是将横切关注点(cross-cutting concerns)分离出来,使得

Spring Framework中的面向方面编程

编程 作为这个介绍Spring框架中的面向方面编程(Aspect-Oriented Programming,AOP)的系列的第一部分,本文介绍了使您可以使用Spring中的面向方面特性进行快速开发的基础知识.使用跟踪和记录方面(面向方面领域的HelloWorld)作为例子,本文展示了如何使用Spring框架所独有的特性来声明切入点和通知以便应用方面.本系列的第二部分将更深入地介绍如何运用Spring中的所有通知类型和切入点来实现更实用的方面和面向方面设计模式.对于AOP的更一般性的介绍,请查看O

AOP编程入门--Java篇

编程 Aspect Oriented Programming(AOP),面向切面编程,是一个比较热门的话题.AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果.比如我们最常见的就是日志记录了,举个例子,我们现在提供一个服务查询学生信息的,但是我们希望记录有谁进行了这个查询.如果按照传统的OOP的实现的话,那我们实现了一个查询学生信息的服务接口(StudentInfoService)和其实现类(Student

Spring中反向控制和面向切面编程的应用

引言 在J2EE的整个发展历程中,现在正是一个非常时刻.从很多方面来说,J2EE都是一个伟大的成功:它成功地在从前没有标准的地方建立了标准:大大提升了企业级软件的开放程度,并且得到了整个行业和开发者的广泛认可.然而,J2EE在一些方面已经开始捉襟见肘.J2EE应用开发的成本通常很高.J2EE应用项目至少和从前的非J2EE项目一样容易失败--如果不是更容易失败的话.这样的失败率高得让人难以接受.在这样的失败率之下,软件开发几乎变成了碰运气.而在J2EE遭遇失败的场景中,EJB通常都扮演着重要的角色

面向切面编程(AOP)的理解

  在传统的编写业务逻辑处理代码时,我们通常会习惯性地做几件事情:日志记录.事务控制及权限控制等,然后才是编写核心的业务逻辑处理代码.当代码编写完成回头再看时,不禁发现,扬扬洒洒上百行代码中,真正用于核心业务逻辑处理才那么几行,如图6-4所示.方法复方法,类复类,就这样子带着无可奈何遗憾地度过了多少个春秋.这倒也罢,倘若到了项目的尾声,突然决定在权限控制上需要进行大的变动时,成千上万个方法又得一一"登门拜访",痛苦"雪上加霜". 如果能把图6-4中众多方法中的所有共