网站首页 新闻首页 网页设计图形动画软件编程网站开发办公软件操作系统数据库网络技术认证考试范文资料黑客攻防 书籍教程 进入论坛

使用 AOP 来维护遗留 Java 应用程序

http://www.diybl.com/ 2007-2-6  网络 点击:  [ 评论 ]
文章搜索:    【点击打包该文章】

处理复杂和不熟悉 Java 代码的技术
级别:中级


Abhijit Belapurkar(abhijit_belapurkar@infosys.com)
高级技术架构师,Infosys Technologies Limited
2004 年 3 月

如果您曾经接管并且必须维护某个基于 Java 的应用程序,那么本文就是为您准备的。作者 Abhijit Belapurkar 将向您展示如何使用面向方面编程(aspect-oriented programming,AOP)来对即使最不透明的遗留应用程序获得前所未有的见解。
软件系统通常从一组有限的得到良好理解的需求开始。然而,随着大多数成功系统的演进,它们承担起越来越多的需求,体现在无数的功能和非功能性方面。在一个企业环境中,您最终很容易向这个混乱的模块组合添加许多第三方库和框架,它们全都彼此交互,并在系统日常工作的表面之下相互配合。实际上,用不了多少年,最初具有很简单、可管理的需求集的系统就会变成庞然大物:难于控制和笨拙的代码。

于是步入这种环境的 Java 开发人员就有了一个日常维护和改进的新任务。如果您就是这个开发人员,那么您的第一个任务就是深刻理解该系统的结构。理解结构将是增强系统和诊断不可避免会发生的问题的关键。当然,第一次探究任何未知的系统都是说起来容易做起来难。在某些情况下,您能够咨询原先的开发人员,而在其他情况下却不能。但是即使能够找到原先的开发团队,有些系统也会因为太过庞大,而无法在没有机械帮助下熟悉和理解它。

虽然有许多可用的工具能够帮助您理解复杂的程序(请参阅 参考资料),但是大多数工具都很昂贵、学习起来很耗时间,并且功能范围有限(也就是说,如果该工具无法满足需要,您将求助无门)。在本文中,我将建议一种替代的方法。面向方面编程是成熟的编程范型,它可以应用于广泛的编程场景,包括遗留应用程序的理解和维护。

请注意,本文假设您大致熟悉 AspectJ 之下的 AOP,特别是 AspectJ 的静态和动态横切技术。虽然我将在下一节提供关于 AOP 横切的简要概述,但是您应该参考 参考资料,获取更多信息。

总体概述
基于 Java 的 AOP 使用了灵活而丰富的表达语言,您可以使用它以近乎无限种方式来分解复杂的应用程序。基于 Java 的 AOP 的语法类似于 Java 语言,您应该很容易就会掌握它。一旦掌握,AOP 就是一种具有许多应用的编程技术。除了理解遗留系统内部细节外,您还可以使用 AOP 来非强制性地重构和增强这样的系统。虽然本文将完全使用 AspectJ,不过这里讨论的大多数技术都可移植到其他流行的基于 Java 的 AOP 实现,比如 AspectWerkz 和 JBossAOP(请参阅 参考资料)。

关于横切
任何应用程序都由多个功能性和系统性关注点(concern)组成。功能性 关注点与应用程序的日常使用相关,而 系统性 关注点则与系统的整体健康和维护相关。例如,一个银行应用程序的功能性关注点包括账户维护和允许借/贷操作,它的系统性关注点包括安全、事务、性能和审计日志记录。即使使用最好的编程方法学来开发应用程序,您最终也会发现它的功能性和系统性关注点会以跨越多个应用程序模块的形式相互混杂在一起。

横切 是一种 AOP 技术,用于确保独立的关注点保持模块化,同时仍然足够灵活地在整个应用程序中的不同点应用。横切包括静态和动态两种类别。动态横切 体现为通过在感兴趣的特定点织入(weave in)新的行为来改变对象的执行行为。静态横切 允许我们通过注入(inject in)附加的方法和/或属性来直接改变对象的结构。

静态横切的语法与动态横切很不相同。以下术语适用于动态横切:


连接点(join point)是 Java 程序中的某个特定执行点,比如某个类中的一个方法。


切入点(pointcut)是特定于语言的结构,它表示或捕捉某个特定的连接点。


通知(advice)是在到达某个特定的切入点时要执行的一段代码(通常是一个横切功能)。


方面(aspect)是定义切入点和通知以及它们之间的映射的一个结构。方面由 AOP 编译器用来在现有对象中的特定执行点织入附加功能。

本文中的所有代码演示都将利用动态横切。请参阅 参考资料,获得关于静态横切的更多信息。

AspectJ 之下的 AOP
为了学习本文中的例子,您应该熟悉以下特定于 AspectJ 之下的 AOP 的特性。


AspectJ 提供一个名为 ajc 的编译器/字节代码织入器,它编译 AspectJ 和 Java 语言文件。ajc 根据需要将方面交织在一起,以产生与任何 Java 虚拟机(1.1 或更高版本)相容的 .class 文件。


AspectJ 支持如下这样的方面,即这些方面规定某个特定的连接点应该永远不会到达。如果 ajc 进程判断出情况不是这样,它将发出一个编译时警告或错误(具体取决于该方面)。

应用程序和系统分析
在下面几节中,您将学习两种使用 AOP 的不同的应用程序和系统分析机制。第一种机制我称之为 静态分析,它要求您做以下事情:

从 CVS(或您所使用的其他任何代码版本控制系统)中把整个应用程序代码库签出到本地区域中。


修改生成文件(build file)以使其使用 AspectJ 编译器(ajc)。


在适当的位置包括方面(aspect)类。


执行一次完整的系统生成过程。

第二种机制我称之为 动态分析,它要求您不仅在本地区域生成(build)系统,而且要运行该系统的具体用例(use case),同时将运行时反射中的信息收集到运行系统中。在下面几节中,我将详细讨论每种机制,同时使用代码例子来说明关键概念。

静态分析
我将研究许多对遗留应用程序执行静态分析的技术,并将它们应用于三种常见的维护场景,如下所示:

评估接口变更所带来的影响。
识别死的或不可到达的代码。
创建松散耦合。

下面就让我们开始吧!

评估接口变更所带来的影响
面向对象的遗留应用程序或系统应该包含许多模块,它们向客户端公开定义良好的接口。假设您接受了一个整合新的需求的任务,这个任务需要改变现有的接口。由于不熟悉代码,您首先面临的挑战就是弄清改变这个接口将对系统的客户端带来的影响。为方便说明这里的例子,这里隐含的影响只不过就是简单地改变每个客户端,以使方法调用符合改变后的签名。因此,对于确定实现新需求的最佳方法以及确定该需求对于给定的系统是否值得来说,知道该接口的所有客户端也许是有帮助的。我将首先使用静态分析来确定该接口的客户端,然后评估接口变更给系统带来的影响。

这种技术基于这样一些方面,它们在发现特定的连接点可达时发出编译时警告。在这种情况下,您将编写连接点来捕捉针对该特定接口的所有方法的调用。您必须首先在本地区域中提取出应用程序代码库,修改生成文件以使其使用 AspectJ 编译器以及在适当的位置包括方面类,然后运行系统的完整生成文件。如果正确地捕捉到了连接点,您预期会看到关于向代码库中的目标方法发出调用的编译时警告。

如清单 1 所示的编译时方面检测并显示调用接口 com.infosys.setl.apps.AppA.InterfaceA(及其实现)的所有类,同时执行(一个或多个)实现者(implementor)类本身。 因此对于示例 InterfaceA、它的实现者类 ClassA 以及调用者类 ClassB,该方面将生成一个关于调用 ClassB 中的 a.methodA() 的编译时警告。

清单 1. 使用 InterfaceA 的类

package com.infosys.setl.apps.AppA;
public interface InterfaceA
{
public String methodA();
public int methodB();
public void methodC();
}

package com.infosys.setl.apps.AppA;
public class ClassA implements InterfaceA
{
public String methodA()
{
return "Hello, World!";
}
public int methodB()
{
return 1;
}
public void methodC()
{
System.out.println("Hello, World!");
}
}

package com.infosys.setl.apps.AppB;
import com.infosys.setl.apps.AppA.*;
public class ClassB
{
public static void main(String[] args)
{
try
{
InterfaceA a =
(InterfaceA)Class.forName("com.infosys.setl.apps.AppA.ClassA").newInstance();
System.out.println(a.methodA());
}
catch (Exception e)
{
e.printStackTrace();
}
}
}

package com.infosys.setl.aspects;
public aspect InterfaceCallersAspect
{
pointcut callerMethodA(): call (* com.infosys.setl.apps.AppA.InterfaceA.methodA(..)) &&
!within(com.infosys.setl.apps.AppA.InterfaceA+);
pointcut callerMethodB(): call (* com.infosys.setl.apps.AppA.InterfaceA.methodB(..)) &&
!within(com.infosys.setl.apps.AppA.InterfaceA+);
pointcut callerMethodC(): call (* com.infosys.setl.apps.AppA.InterfaceA.methodC(..)) &&
!within(com.infosys.setl.apps.AppA.InterfaceA+);
declare warning: callerMethodA(): "Call to InterfaceA.methodA";
declare warning: callerMethodB(): "Call to InterfaceA.methodB";
declare warning: callerMethodC(): "Call to InterfaceA.methodC";
}



接口变更对实现者类的影响可通过如清单 2 所示的方面来确定。对于前述清单中的示例类,这个方面为 ClassA 生成了一个编译时警告。

清单 2. 实现 InterfaceA 的类

package com.infosys.setl.aspects;
public aspect InterfaceImplAspect
{
pointcut impls(): staticinitialization (com.infosys.setl.apps.AppA.InterfaceA+) &&
!(within(com.infosys.setl.apps.AppA.InterfaceA));
declare warning: impls(): "InterfaceA Implementer";
}



识别死代码
系统随着增强请求断断续续地加入而变得零碎。每当对系统或应用程序的某个部分作出更改,清楚这样的变更对其他部分的影响是很重要的。例如,重构模块 A 中的代码可能导致其他某个地方的代码(不管是某个类中的方法还是整个类本身)变成不可到达(或死的),因为控制/数据流已被更新而绕过了它。

文章整理:DIY部落 http://www.diybl.com (本站)   【点击打包该文章】
如果图片或页面不能正常显示请点击这里 站内搜索:   

文章评论

请您留言

 

最新新闻