内容


利用动态代理的 Java 验证

从业务对象实现中去耦验证过程

Comments

验证是许多企业应用程序的关键部分。大多数业务方法都包含验证逻辑,以确保在执行业务逻辑之前,前提条件得到满足。对用户界面输入的值进行处理的代码,要执行验证逻辑,在执行可能影响应用程序其他部分或者其他用户的操作之前,确保用户输入的值是正确的。对于利用其他松散耦合的组件以及断言不严格的服务,并与之交互的应用程序来说,验证是一个特别重要的组件。

验证与业务应用程序的安全性和功能一样重要,核心的应用程序逻辑通常和验证程序混杂在一起。验证过程经常散落在方法调用里,这样就造成很难区分验证逻辑和核心业务逻辑。在大多数情况下,业务对象和方法必须了解验证过程的细节,并在它们的实现里直接处理它们 -- 例如,一个业务对象可能直接从业务方法里抛出验证异常(或者直接编码在方法里,或者作为调用验证服务的结果)。但是,在这种情况下,验证异常实际是验证过程的副产品,理想情况下,应该隐藏业务对象的实现。

在这篇文章里,我将向您展示一种更加去耦、更加集中的验证方法,它利用 Java 平台 1.3 版本中引入的动态代理工具来实现。通过在整篇文章里处理同一个示例,我将展示紧密耦合和松散耦合验证方案的弱点,然后向您展示动态代理如何可以帮助您改进紧密耦合和松散耦合。

紧密耦合的验证

在整篇文章里,我要使用一个简单的 User对象,其中定义了在系统中处理用户的方法。清单 1 列出了 User接口及其方法。

清单 1. User 接口及其方法
 /** 
 * Gets the username of the User. 
 */ 
 String getUsername(); 
 /** 
 * Sets the username of the User. 
 * @throws ValidationException indicates that validation of the proposed 
 * username variable failed.  Contains information about what went wrong. 
 */ 
 void setUsername(String username) throws ValidationException; 
 /** 
 * Gets the password of the User. 
 */ 
 String getPassword(); 
 /** 
 * Sets the password of the User. 
 * @throws ValidationException indicates that validation of the proposed 
 * password variable failed.  Contains information about what went wrong. 
 */ 
 void setPassword(String password) throws ValidationException;

在紧密耦合的数据验证方案中,我会把 验证代码直接插入接口的方法实现中,如清单 2 所示。注意,在设置实例变量之前,验证逻辑被硬编码到方法中。

清单 2. 紧密耦合的验证方案
 public void setPassword(String password) throws ValidationException { 
    if ((password == null) || (password.length() < MIN_PASSWORD_LENGTH)) { 
        throw new ValidationException("INVALID_PASSWORD", 
                                      "The password must be at least " + 
                                      MIN_PASSWORD_LENGTH + 
                                      " characters long"); 
    } 
    this.password = password; 
 }

在这个示例里,验证逻辑和使用它的对象紧密耦合在一起。这种方法的弱点应当是相当明显的:

  • 它没有引入可重用的验证代码。虽然示例里包含了在应用程序其他许多地方都可使用的长度和 null 检查,但是它们采用了无法重用的方式进行编码。
  • 验证规则无法用任何方法进行配置。例如,如果要向 setPassword()方法中加入另一条验证规则,我只能修改方法本身,重新编译,也可能要重新部署。

虽然不理想,但是紧密耦合的验证代码相当普遍,特别是在比较老的应用程序里。幸运的是,紧密耦合不是我们在编写 User接口的验证逻辑时的唯一选项。

松散耦合的验证

您可以让接口实现调用一个独立的服务来执行它的验证逻辑,从而避开紧密耦合。通常情况下,这个服务会有一组验证规则,分配给特定对象的特定方法。因为验证规则从接口的业务逻辑去耦,所以可以在许多对象的许多方法上重用它们。您也可以在外部定义验证规则,在这种情况下,修改验证逻辑,就只是修改验证服务配置的问题了。

清单 3 显示了如何用验证服务把验证逻辑从核心业务逻辑实现中去耦。

清单 3. 使用验证服务
 public void setPassword(String password) throws ValidationException { 
    BusinessObjectValidationService.validate(this, "setPassword", 
                                             new Object[] {password}); 
    this.password = password; 
 }

在这里,验证逻辑作为调用对象的外部服务运行。具体来说,在 setPassword()方法上执行的验证,被配置到 验证服务上,而不是由方法自己来执行。这种松散耦合,在许多情况下,可以解决前面例子的弱点:

  • 验证规则有可能重用,因为它们可以只编写一次,在里面定义不同对象所使用的方法。例如,我可以写一个验证规则,用于断言指定参数不为 null,然后在所有需要同样规则的方法中重用它。
  • 验证规则也可能是可配置的。例如,我可以用 XML 文档初始化验证服务,在 XML 文档里描述针对具体方法或对象需要执行的规则。我也可以把 API 公开到这个配置里,这样我就可以在运行时修改验证规则。

虽然我们朝着正确的方向走了一小步,但是这种方法仍有不足。当我开发方法的时候,我不得不确保调用 验证服务,确保方法实现声明了 ValidationException异常。这些都是验证服务的工作,和方法的核心逻辑实际没有任何关系。我实际想要的,是一种编写 User接口实现的方法,这样它就不需要知道这类事情了。

动态代理方法

动态代理是这样一种类,它可以实现在运行时指定的一组接口。对代理类的方法调用,被分配给调用处理程序,而调用处理程序是在代理类生成的时候指定的。动态代理类有许多应用程序中使用的接口,其中一个可以用统一的方式有效地处理方法前和方法后的调用操作。因为验证通常是方法前调用操作,所以动态代理为我们提供了针对前面示例所指出的问题的解决方案。 动态代理类给了我们一种以统一方式方便地处理任何方法上的验证途径,同时把所有的验证逻辑完全与核心业务逻辑分离开。

因为在许多框架中,都存在针对主要业务对象和服务的接口,所以您对于交换进和交换出这类接口的不同实现应该有所体验。使用动态 代理类与其非常类似,区别在于,客户并不直接处理接口的实现,而是与代理类打交道,代理类负责实现接口、执行验证、把方法调用委托给实现类。使用动态代理 方法,所有的验证逻辑对于代理的客户,都应当是透明的。因此,实现新的验证方案应当会非常简单:对于使用 User接口的代码,我一行也不用修改。

总体来说,我要建立一个执行验证规则的客户调用处理程序。调用处理程序中会包含一个实际的实现类的实例,把它作为实例变量。它首先验证方法调用的方法参数,然后把方法调用委托给实现类。当应用程序需要业务对象实例时,它实际会接收到一个动态代理类的实例。正如您稍后会看到的,这允许业务对象实现类完全独立于那些只与验证过程有关的代码。

调用处理程序

调用处理程序类是处理所有数据验证逻辑的地方。调用处理程序类还会把方法调用委托到真正的实现类,以便处理核心业务逻辑。清单 4 显示了一个调用处理程序,它没有绑定到任何具体的业务对象,这样就能把它用于任何需要被验证的业务对象。请注意,在下面的 invoke()方法中的验证代码,几乎与 清单 3中的代码完全一样。实际上,在这里可以使用与前面完全一样的验证器服务。

清单 4. 调用处理程序
 /** 
 * This is the object to which methods are delegated if they are not 
 * handled directly by this invocation handler.  Typically, this is the 
 * real implementation of the business object interface. 
 */ 
 private Object delegate = null; 
 /** 
 * Create a new invocation handler for the given delegate. 
 * @param delegate the object to which method calls are delegated if 
 * they are not handled directly by this invocation handler. 
 */ 
 public BusinessObjectInvocationHandler(Object delegate) { 
    this.delegate = delegate; 
 } 
 /** 
 * Processes a method call. 
 * @param proxy the proxy instance upon which the method was called. 
 * @param method the method that was invoked. 
 * @param args the arguments to the method call. 
 */ 
 public Object invoke(Object proxy, Method method, Object[] args) 
 throws Throwable { 
    // call the validator: 
    BusinessObjectValidationService.validate(proxy, method.getName(), args); 
    // could perform any other method pre-processing routines here... 
    /* validation succeeded, so invoke the method on the delegate.  I 
       only catch the InvocationTargetException here so that I can 
       unwrap it and throw the contained target exception.  If a checked 
       exception is thrown by this method that is not assignable to any of 
       the exception types declared in the throws clause of the interface 
       method, then an UndeclaredThrowableException containing the 
       exception that was thrown by this method will be thrown by the 
       method invocation on the proxy instance. 
    */ 
    Object retVal = null; 
    try { 
        retVal = method.invoke(delegate, args); 
    } catch (InvocationTargetException ite) { 
        /* the method invocation threw an exception, so "unwrap" it and 
           throw it. 
        */ 
        throw ite.getTargetException(); 
    } 
    // could do method post-processing routines here if necessary... 
    return retVal; 
 }

您可以看到,调用处理程序的这种实现,利用了通用的验证器服务,与 清单 3里相同。 作为替代解决方案,我建立一个看起来很像 清单 2的调用处理程序,而验证代码直接在调用处理程序里运行。在这种情况下,我让调用处理程序自己检查调用它的方法是不是 setPassword()方法,而长度和 null 检查也直接在处理程序中进行。虽然这种方法可以把接口的验证逻辑从它的核心业务代码去耦,但是它的可重用性和可配置性不是很强。 在下一节中,我会继续采用通用验证器的实现,在那里您会真正开始发现可重用和可配置代码的价值。

业务对象实现

下一步是把业务对象实现指定给调用处理程序的构造函数。出于本示例的需要,我会采用不受验证限制的方式实现 User接口,因为验证逻辑是在调用处理程序中处理的。实现类中的相关代码如清单 5 所示。

清单 5. 不受验证限制的 User 实现
 /** 
 * The username of this User. 
 */ 
 private String username = null; 
 /** 
 * The password of this User. 
 */ 
 private String password = null; 
 /** 
 * Gets the username of the User. 
 */ 
 public String getUsername() { 
    return username; 
 } 
 /** 
 * Sets the username of the User. 
 */ 
 public void setUsername(String username) { 
    this.username = username; 
 } 
 /** 
 * Gets the password of the User. 
 */ 
 public String getPassword() { 
    return password; 
 } 
 /** 
 * Sets the password of the User. 
 */ 
 public void setPassword(String password) { 
    this.password = password; 
 }

如果您将 setPassword()方法体中的代码与前面 清单 2中的方法进行对比,您会看到它没有包含专门用于验证的代码。验证过程的细节现在完全由调用处理程序来处理。

业务对象工厂

我可以把所有这些捆绑在一起,形成最后的代码片断,这段代码会实际建立动态代理类,为它加上正确的调用处理程序。最简单的方法就是在工厂模式中把代理类的建立封装起来。

许多业务对象框架采用工厂模式来建立业务对象接口的具体实现,例如 User接口。这样,建立一个新业务对象实例就仅仅是调用工厂方法的问题了:对象建立背后的全部细节,都留给工厂,客户端对于实际上如何构建实现是毫不知情的。清单 6 显示了如何为 User接口建立动态代理(用 UserImpl实现类),同时通过调用处理程序传递所有的方法调用。

清单 6.UserFactory
 /** 
 * Creates a new User instance. 
 */ 
 public static User create() { 
    return(User)Proxy.newProxyInstance(User.class.getClassLoader(), 
        new Class[] {User.class}, 
        new BusinessObjectInvocationHandler(new UserImpl())); 
 }

请注意: Proxy.newProxyInstance()方法有三个参数:动态代理定义的类加载器 classloader; Class数组,里面包含动态代理要实现的所有接口(虽然我可以指定任何要实现的接口,但是在工厂中只实现了 User接口);以及处理方法调用的调用处理程序。我还建立了 UserImpl实现类的新实例,并把实例传递给调用处理程序。调用处理程序会使用 UserImpl类来委托所有的业务方法调用。

动态代理的缺点

不幸的是,使用动态代理类确实有一个重要不足:性能。对动态代理类的方法调用,不会像直接调用对象的方法那样好。所以,在应用程序框架中对动态代理的使用,取决于什么对您更重要:更整洁的架构还是更好的性能。在应用程序的许多方面,性能损失可能是值得的,但是在其他方面,性能则有可能是至关重要的。所以,有一种解决方案,就是在有些地方用动态代理,在其他地方则不用。如果您决定走这条路,一定要记住,除了验证之外,调用处理程序还能做其他的事,这允许您在运行时或在代码部署之后改变您的业务对象行为。

动态代理的其他用途

动态代理类在业务对象框架中有许多用途,不仅仅是用一致的方式对方法进行验证。我前面建立的简单调用处理程序 可以用于任何方法前和方法后的调用处理。例如,我可以很容易对业务方法计时,只要在业务对象的实现方法调用之前和之后插入代码,就可计算方法经历的时间。我还可以插入方法后调用逻辑,把状态的变化通知给有兴趣的侦听者。

在示例中,我只为一个接口建立了动态代理类,这个接口是: User。我可以很容易地指定动态代理类在运行时要实现的多个接口。用这种方式,静态工厂方法返回的对象可以实现建立代理时所定义的任意数量的接口。调用处理程序类必须知道如何处理所有接口类型的方法调用。

您还应当注意到,在示例里我一直调用实际的实现类来执行业务逻辑。这不是必需的。例如,代理类可以把方法调用委托其他任何对象,甚至是处理程序本身。一个简单的例子就是有这样一个接口,它通过 set/get 方法公开了大量 JavaBean 属性。我还建立了一个专门的调用处理程序,它维持了一个 Map,在映射里,键是属性名称,值是属性的值。在 get 调用上,我替换 Map中保存的值,这就消除了为这些简单的 JavaBean 类实现编写代码的需要。

结束语

使用动态代理类进行验证是从应用程序的核心逻辑去耦验证程序的简单而有效的方法。与紧密耦合方法不同,使用动态代理给您带来了可以重用、可以配置的验证代码。

在这篇文章里,您看到了用调用处理使用动态代理的好处。因为动态代理类上的方法调用都可以通过公共的调用处理程序进行传递,您可以非常容易地修改处理程序执行的逻辑,甚至在已经部署的代码中修改或者在运行时动态修改。还可以重构调用处理程序,让它处理其他横跨不同对象类型的方法调用的操作。

Java 平台的动态代理功能不是从核心代码的业务逻辑中去耦验证程序的唯一选项。在某些情况下,例如在性能是应用程序的主要考虑因素的地方,就可能不是最佳选项。虽然本文侧重在动态代理上,我还是讨论了其他一些选项,包括 JavaBean 的受限属性功能,以及代码生成工具的使用。使用任何一种技术,您都应当仔细评估替代品,只有当动态代理是应用程序的最佳解决方案时才使用它。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=53583
ArticleTitle=利用动态代理的 Java 验证
publish-date=09142004