Aspectwerkz 2.0开发企业AOP快速入门

今天,面向方面的程序设计(aspect-oriented programming,AOP) 框架试图在企业环境中获得立足之地。这些框架为了得到普遍采用,必须与企业系统中已经在使用的其他框架良好地集成。本文向开发人员展示了,如何将AspectWerkz AOP框架与一些现在常用的框架(如Log4J、Atlassian 性能剖析器、Hibernate和Tapestry)相集成。

  本文从一个现有的Tapestry web应用程序开始。这个应用程序实现两个单独的关注点:日志记录和性能剖析。每个项目都有这些需求,许多项目用Log4J做日志记录而用Atlassian剖析器框架做性能分析。然后最初的非AOP实现被重构,以便使用AspectWerkz框架来分离每个关注点的实现。产生的应用程序代码将会更简单,更容易维护,而最重要的是,更自然,表达能力更强。

  本文中描述的所有应用程序和源代码都可以下载得到。

  简介

  为了证明AOP的强大,我们从一个非AOP Web应用程序开始,并重构它以使用AOP。应用程序的前后映像将说明AOP是多么易于使用,及方面作为Java语言的扩展是多么有用。

  示例程序实现了一个示例blog,以允许用户在已有文章上张贴新的文章和评论。它是由多种框架实现的,包括用于web层的Tapestry,作为O/R Mapping解决方案的Hibernate,以及把各部分结合起来的Sping框架。日志记录是用Log4J框架实现的,而性能是用Atlassian剖析器来监控的。

  出于演示目的,blog应用程序保持尽可能地简单。尽管很简单,blog应用程序包含了使它像一个“真正的”应用程序的足够功能,因此文中的代码可以应用于现有的企业项目。

  本文假定读者对AOP概念有基本的理解。AOP的初学者应该阅读下面引用的文章和教程。所有的例子都将用运行在BEA WebLogic JRockit 1.4.2 SDK上的AspectWerk 2.0实现。另外一些可选的环境请参见AspectWerk主站点。

  运行示例程序

  要运行示例应用程序,需要一个数据库和一个servlet容器。本文假定使用MySQL。

  安装完必需的软件后,下载并解压示例代码到一个临时目录中。该发行版中有三个文件:

  • blog-ddl.sql
  • blog-preaop.war
  • blog-postaop.war

  首先,设置MySQL来得到所需的数据库。把数据库命名为“blog”,并设置一个口令为“password”的用户id“blog”。然后通过执行blog-ddl.sql脚本来定义模式(在MySQL提示符下键入sourceblog-ddl.sql)。

  现在我们可以部署应用程序了。Blog应用程序的两个版本都被打包成WAR文件,所以可以直接了当地部署到servlet容器中。

  现在可以通过访问网址http://localhost:7001/blog-preaop/blog和http://localhost:7001/blog-postaop/blog来运行应用程序了。试着运行应用程序的一些功能并检查WEB-INF/classes目录中的源代码。本文的其余部分将详细分析两种实现并着重说明AOP方法的优点。

  分析最初的blog应用程序

  blog应用程序实现了两个横切关注点:日志记录和性能剖析。这些关注点要在贯穿整个应用程序的所有类中实现。让我们看看,利用AOP出现以前可用的标准Java工具是如何实现这两个关注点的。

  日志记录

  日志记录的目的是能够在不打开调试器的情况下调试生产应用程序。在我的项目中已经证明很有价值的有,通过记录每个方法的入口和出口来跟踪代码。例如,在HibernateEntryDao类中,下面的代码是用来查找所有blog入口的:
private static final Log log = Log.getLog(EntryHibernateDao.class); 
public Entry[] findAll() { 
   log.enter("findAll"); 
   List entries = getHibernateTemplate().find("from Entry"); 
   log.exit("findAll"); 
   return (Entry[])entries.toArray(new Entry[] {}); 
} 

  当用于整个代码时,将对完全的用户请求产生下面的日志输出:

com.tss.blog.web.ApplicationServlet INFO : >service: ’/blog’ 
com.tss.blog.service.BlogSvcImpl INFO : >findAllEntries 
com.tss.blog.persistence.EntryHibernateDao INFO : >findAll 
com.tss.blog.persistence.EntryHibernateDao INFO : <findAll 
com.tss.blog.service.BlogSvcImpl INFO :   <findAllEntries 
com.tss.blog.web.ApplicationServlet INFO : <service:’/blog’

  尽管非常冗长,但它对调试生产系统非常有帮助。Log4J在优化日志书写方面做得很好,所以性能很少成为问题。因为所有的enter/exit调用都被赋予INFO优先级,如果web层的性能真的成为问题,那么需要做的只不过是把log4j.preperties中的日志记录优先级阈值更改为WARN或更高,而且Log4J会丢弃跟踪信息。

 

性能剖析  blog应用程序实现的另一个关注点是性能剖析。在许多项目中,应用程序要先进行性能剖析才能投入生产,而且只有当用户抱怨时才重做剖析。有了Atlassian剖析器,我们可以采取更为前摄的方法,跟踪每种方法所花的时间,并在每个请求的结尾报告结果。如果有哪一个请求花的时间比预期的多,我们就把剖析信息记录为ERROR以引起注意。将Log4J配置为有任何记录错误就向开发团队发送电子邮件,如果应用程序运行过慢的话我们立刻就能知道。凭我的经验,这在查找应用程序的程序设计问题(如死锁和编写得糟糕的事务)方面非常有价值。为了实现剖析器,我们可以沿用跟踪代码所使用的方法。在每一种方法中,代码中都有很多调用来启动和停止剖析器。把这与跟踪代码相结合,就得到了下面的代码:

public Entry[] findAll() { 
   log.enter("findAll"); 
   Profiler.push("findAll"); 
   List entries =  
       getHibernateTemplate().find("from Entry"); 
   Profiler.pop("findAll"); 
   log.exit("findAll"); 
   return (Entry[]) 
       entries.toArray(new Entry[] {}); 
} 

  这就对完全的用户请求产生了下面的日志输出:

com.tss.common.Profiler INFO :  
   [2373ms] - service: '/blog' 
     [150ms] - findAllEntries 
       [150ms] - findAll 

  尽管该信息非常有用,但您可以看到代码变得多么冗长。剖析器是如此易于侵入,以至于几乎不可能让整个开发团队都使用它。因为我们还没有使用AOP,我们别无选择只有引进一些hack——把对剖析器的调用结合在日志记录代码中。现在下面的日志方法会处理性能剖析:

public void enter(Object method) { 
   if (!l.isInfoEnabled()) return; 

   l.info(">" + method); 
   Profiler.push(method.toString()); 
} 

public void exit(Object method) { 
   if (!l.isInfoEnabled()) return; 

   l.info("<" + method); 
   Profiler.pop(method.toString()); 
} 

  当这发挥作用时,开发人员现在只需要调用日志记录代码,我们把性能剖析关注点和日志记录关注点紧密联系起来了。这种紧密联系降低了灵活性。

  示例应用程序实现中的问题

  我已经暗示过blog应用程序实现的一些问题,但我想再次强调一下,并解释一下为什么这些是问题:

  • 过于冗长

  为了使记录器和剖析器能正常工作,必须在每个方法中包含enter/exit代码。在某些情况下,需要改变代码风格以允许在从方法返回之前调用一个exit。例如,想要剖析器正确记录方法的时间,必须在抛出异常或从方法返回之前调用exit。这给开发人员造成了负担,并使代码变得膨胀。

  • 不易重构代码

  重构代码以使它变得更为自然是一个最佳实践,任何使重构变得更复杂的事情,开发人员都会尽力避免。更改方法名称的事情经常发生,这使静态声明的enter/exit方法名称不再正确。情况好的话,只需要手动更改;情况糟糕的话,不正确的名称会遗留在代码中,并将在调试过程中引起混乱。

  • 不经代码审查无法坚持正确的用法

  除了进行常规代码审查,没有其他方法可确保团队中的每个人都遵循项目的日志记录指导原则。如果不一致遵循指导原则的话,性能剖析和跟踪信息就失去意义。

  • 日志记录关注点和性能剖析关注点的紧密联系

  把性能剖析关注点融入到日志记录代码中会使剖析某些特定的代码路径非常困难。例如,分析所有的ServletRequest并且在任一个请求所花费的时间超过5秒时记录一个错误,这是我们想要的结果。其他的任务,如初始化Application servlet,应该允许花费超过5秒的时间而不触发错误,但是出于信息目的,我们仍然想把它记录下来。如果在初始化过程中产生错误,那么很可能把日志记录代码注释掉或者把最大花费时间增为10秒。(非常不幸,这是我的一个非AOP项目中真实发生的事情,那次初始化花了8秒钟。)

  上面1,2,3点可能成为开发人员真正的负担。除了对日志记录和性能剖析的额外关注外,开发人员还有很多要担心的。当然了,开发人员会定义模板来自动完成enter/exit方法调用,但是他们必须记得始终要这样做而且还要修复重构引起的错误。

  为了阐明这种实现将成为多大的负担,考虑一下在项目的整个生存期为实现记录在每个类上花费多长时间。在最近的一次会议上,Adrian Colyer,AspectJ项目的主要开发人员,估测了一下在大中型项目上实现不间断日志记录所投入的开发时间。他估计在整个项目中,开发人员对每个类要花15分钟来实现日志记录。(即使用代码完成,也必须考虑重构造成的影响。)在一个相对小一些、具有500个类的项目中,项目进度表中居然有3周半是花在实现日志记录上!下面通过引入一些方面来实现日志记录以减少花费的时间。

  进行重构以使用AOP

  在使用AOP之前先决定要使用哪种框架。当前有好几种可选择的AOP框架,我认为,AspectWerkz和AspectJ,基于它们的功能性和全面普及,是最引人注意的。本文将演示如何使用AspectWerkz框架,使用的是2.0版本。

  在AspectWerkz和AspectJ之间做出选择是很困难的。幸好,最近AspectWerkz和AspectJ同意联合起来并合作发布AspectJ 5。当前的AspectWerkz 2.0版本将是最后版本,所有的新发展都将在AspectJ分支中进行。AOP领域的强强联合对开发人员来说最终还是一件好事,因为当前平台的多样性会使初学者望而生畏。此外,将两个强大的开发团队统一成一个更强的统一体将会有助于推广AOP。

  在深入研究把方面应用于代码基之前,需要在如何使用AspectWerkz上做一些决定。

 

注释与XML

  AspectWerkz在定义切入点(pointcut)时给出了两个选择:Java类文件中的注释,或使用名为aop.xml的外部文件中定义的XML。利用注释在代码中直接定义切入点非常好,但是如果不是使用JDK 5,就必须包含额外的构建步骤来产生aop.xml文件。当使用JDK 5时,我选择在XML中定义方面,并对切入点使用注释。这样,切入点就与代码结合起来了,而方面可以很容易地通过编辑aop.xml来更改或删除。我希望本文能对最大范围的读者有用,因此假定JDK 5不可用。

  因为我非常不喜欢在开发阶段需要编译时步骤,我选择直接在aop.xml中定义一切。这个决定使我们必须用aop.xml符号定义相当复杂的切入点,但我认为这比使开发周期变慢要好。本文将只使用aop.xml定义。

  在线与脱机编写

  AspectWerkz的很多灵活性是因为它能以在线和脱机两种方式运行。在在线方式下,AspectWerkz在运行时动态地编写类。在脱机方式下,需要一个额外的编译步骤来在构建阶段编写类。这种灵活性使开发人员能够为一个特定任务选择最优方式。

  例如,在程序开发阶段,在线方式较好。在开发周期中不需要编译或编写的步骤。同样,因为是开发阶段,您对JVM运行时具有完全的控制权,因此不必留意定制JVM启动,而这对于在线编写是必需的。

  需要的时候,可以切换为脱机方式。下面是您想这么做的两个主要原因。首先,预编写的类性能非常好,它们与包含同样功能的“普通”类执行起来是类似的。其次,没有必要改变JVM设置。这一点对于驻留环境或者对付保守的管理员很重要。脱机方式的缺点是,您只能把方面应用于自己打包的代码。这意味着您不能把方面应用于任何您可能正在使用的第三方库(除非您把它们重新打包)。

  我在开发阶段使用在线方式,然后在部署时切换为脱机方式。这使我省掉了额外的构建步骤,而且因为是开发阶段,不用担心性能或改变JVM设置。对于本文,脱机编写用于打包的WAR文件,以便读者不必再定制JVM来测试应用程序。

  为了使用AspectWerkz来脱机编写类,把下面的目标结合到ant脚本中:

<target name="weave"> 
  <property name="AW_LIB"  
      value="c:/opt/aspectwerkz-2.0/lib"/> 
  <taskdef name="awc"  
    classname="org.codehaus.aspectwerkz.compiler.AspectWerkzCTask"> 
    <classpath> 
      <pathelement  
          path="/aspectwerkz-2.0.jar"/> 
      <pathelement  
          path="/aspectwerkz-core-2.0.jar"/> 
      <pathelement  
          path="/aspectwerkz-extensions-2.0.jar"/> 
    </classpath> 
  </taskdef> 
   
  <awc verbose="true" 
      targetdir="webWEB-INF asses"> 
    <classpath> 
      <fileset dir="lib"/> 
    </classpath> 
  </awc> 
</target> 

  把日志记录方面应用于示例应用程序

  我们都看过早期的简单跟踪方面,它是AOP的雏形。它只是在每个方法调用之前和之后用一个对函数System.out.println()的调用把所有的方法调用包装起来。当然了,这对于生产环境是完全没有意义的。下面看一下Log4J的优点。

  正如在前面的示例代码中见到的,要用Log4J进行记录,需要用一个类别名来实例化记录器。惯例是用全限定类作为类别名,如下:

private static final Log log = Log.getLog(EntryHibernateDao.class); 

  我们想要一个与上面的定义有相同特征的方面,也即:

  1.对每个类,Log被实例化一次,且只有一次。

  2.每个类有自己的Log,它使用全限定类名作为类别名。

  为了达到上面的要求,仅仅把日志记录方面应用于切入点是不够的。必须用一种mixin(混入)方法在类中“注入”日志定义。Mixin提供了给一组类增加额外功能的方法,这对于需要在多个类中实现样板文件代码的情况尤其有用。使用mixin,我们可以指示AspectWerkz对类进行修改以给类添加行为。在本例中,我们将让AspectWerkz修改类来实现一个Loggable接口,这个接口用来返回一个满足上述要求的Log实例。下面的代码定义了Loggable接口和它的实现:

public interface Loggable { 
    Log getLog(); 
} 

public class LoggableImpl implements Loggable { 
    private final Log log; 
    public LoggableImpl(Class targetClass) { 
        log = Log.getLog(targetClass); 
    } 

    public Log getLog() { return log; } 
} 

  现在我们想要指示AspectWerkz对类进行修改以实现Loggable接口。为此,我们把下面的XML定义添加到aop.xml中:

<mixin class="com.handyware.aop.LoggableImpl"  
  deployment-model="perClass" 
  bind-to="within(com.tss..*)  
            AND avoidTrace"/>  

  这个mixin指示AspectWerkz向我们想要记录的系统中的每个类添加一个LoggableImpl域。要想看看这都做了什么,可以对产生的类进行反编译。下面就是编写的EntryHibernateDao类所包含的代码:

public class EntryHibernateDao  
         extends HibernateDaoSupport 
         implements Loggable, IEntryDao 
{ 
   private static final LoggableImpl aw;  
   public Log getLog() { 
     return aw.getLog(); 
   } 
   static { 
     aw = Class.forName( 
       "com.tss.blog.service.EntryHibernateDao"); 
     aw = (LoggableImpl)Mixins.mixinOf( 
       "com.tss.blog.aop.LoggableImpl",  
       aw); 
   } 
     
   .... 
} 

  正如你所看到的,AspectWerkz修改了初始的类以使它包含一个LoggableImpl静态域,并把它初始化成一个特定于类的Log实例。既然每个类都有了一个getLog()方法,定义Log4J日志记录方面就非常简单了:

public class LoggingIdiom { 
    public Object traceWithParams( 
                 JoinPoint jp, Loggable loggable) 
                 throws Throwable { 
        loggable.getLog().enter(enterTrace(jp)); 
        Object result = jp.proceed(); 
        loggable.getLog().exit(exitTrace(jp, result)); 
        return result; 
    } 
} 

  为了把这个方面编入到代码中,我们把下面的内容添加到aop.xml定义中:

<aspect class="com.tss.aop.LoggingIdiom"> 
  <pointcut name="p2"  
      expression="execution(* com.tss..*.*(..)) 
                        AND avoidTrace" /> 
  <advice name="traceWithParams(JoinPoint jp,  
                 com.tss.aop.Loggable loggable)"  
               type="around"  
               bind-to="p2 AND target(loggable)" /> 
</aspect> 

  您会注意到,在上面的日志记录方面定义中定义了一个avoidTrace切入点,它允许我们排除那些不想做日志记录的类。切入点模型模型非常强大,因为它使您可以自由选择如何应用方面。

  例如,Hibernate使用一种动态生成子类的技术,而且默认情况下,日志记录方面将应用于这些新类,而这可能并不是您想要的。可以把这些类排除在日志之外以更简化。为此,只需排除所有与Hibernate相关的切入点即可。其他的例子包括JavaBean存取器和日志记录方面本身——否则就会陷入日志记录记录器本身的死循环中。

 

题外话:用StaticJoinPoint来最优化

  AspectWerkz 2.0支持StaticJoinPoint的概念,与普通的JoinPoint不同的是,StaticJoinPoint不提供对运行时类型信息(Runtime Type Information,RTTI)的访问。通过除去RTTI,编写者能优化对方面的调用,因为不再需要为JoinPoint收集动态数据并使其保持有效。

例如,前一节中的日志记录方面记录了每个方法调用的参数值,因此需要访问RTTI。如果不想记录参数,可以使用StaticJoinPoint来提高性能。下面的方面代码和定义正是这么做的:

public class LoggingIdiom { 
    public Object trace(StaticJoinPoint jp,  
                               Loggable loggable)  
                               throws Throwable { 
        CodeSignature cs =  
               (CodeSignature) jp.getSignature(); 
        loggable.getLog().enter( 
               methodSignature(cs)); 
        Object result = jp.proceed(); 
        loggable.getLog().exit( 
               exitTrace(jp, result)); 
        return result; 
    } 
} 

... 

<aspect class="com.tss.aop.LoggingIdiom"> 
  <pointcut name="p2"  
      expression="execution(* com.tss..*.*(..))  
                        AND avoidTrace" /> 
  <advice name="trace(StaticJoinPoint jp,  
                  com.tss.aop.Loggable loggable)" 
              type="around"  
  bind-to="p2 AND target(loggable)" /> 
</aspect> 

实现性能剖析方面

  实现性能剖析方面要比实现日志记录方面容易得多,因为剖析器可以静态调用。这使我们可以定义下面的简单方面,而不需要mixin:

public Object profile(StaticJoinPoint jp)  
                   throws Throwable { 
   Profiler.push(methodSig(jp)); 
   Object result = jp.proceed(); 
   Profiler.pop(methodSig(jp)); 
   return result; 
} 

  这段代码非常简单明了。但是,aop.xml中的方面定义更有趣。我们只是想剖析那些特定于web请求的切入点,所以选择切入点时使用cflow表达式。

<aspect class="com.tss.aop.ProfilingIdiom"> 
  <pointcut name="p3" expression="cflow( 
     execution(* com.tss.blog.web. 
                 ApplicationServlet.service(..))) 
     AND execution(* com.tss..*(..))  
     AND avoidTrace"/>  
  <advice name="profile(StaticJoinPoint jp)"  
     type="around" bind-to="p3"/> 
</aspect> 

  cflow表达式允许我们只选择Tapestry页的控制流中的那些切入点。换句话说,每个在web请求的执行中进行的调用都会被剖析。这是一个非常强大的表达式,它允许我们解决避免剖析初始化代码的问题。我们可以只剖析那些感兴趣的代码。实际上,我们可以定义多个剖析器实例,它们基于剖析的内容有不同的行为。例如,花费多于5秒的web请求可能触发一个错误,而任何预定的后台任务花费超过10秒钟也可能产生一个错误。

  结束语

  我们已经看到,把AOP应用于示例应用程序使代码基要易于编写得多,同样,灵活性和一致性也更好。通过使用独立的方面来实现日志记录关注点和性能剖析关注点,我们解决了原始应用程序中凸现的所有问题。开发人员不再需要把这些关注点直接结合到代码中。阅读完本文并浏览完随文所附的源代码后,用不到一天的时间就可以把这些方面结合到您的代码基中去。如果您的项目早点这么做,您就会意识到节省了很多成本,而您团队中的开发人员会非常感激您。

  重要的是,在使用AOP的过程中要注重实效,非必要不用,尤其是在已经存在一个同等好的非AOP方案时。据说,如果某人故意太长时间不用方面考虑问题,像把剖析器和记录器结合起来这样的实现hack也开始似乎正常了。当代码只是因为您对在所有的类中输入相同的代码感到厌倦而开始变得紧密联系时,AOP能够真正帮助您简化代码基。

  即将出现的Aspect 5版本将支持本文用到的所有功能。这里所描述的一切都将自然迁移到Aspect 5中。首次发布可能在今年晚些时间。

阅读更多
个人分类: spring
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭