利用動態代理的 Java 驗證

從業務對象實現中去耦驗證過程

驗證是許多企業應用程序的關鍵部分。大多數業務方法都包含驗證邏輯,以確保在執行業務邏輯之前,前提條件得到滿足。對用戶界面輸入的值進行處理的代碼,要執行驗證邏輯,在執行可能影響應用程序其他部分或者其他用戶的操作之前,確保用戶輸入的值是正確的。對於利用其他鬆散耦合的組件以及斷言不嚴格的服務,並與之交互的應用程序來說,驗證是一個特別重要的組件。

驗證與業務應用程序的安全性和功能一樣重要,核心的應用程序邏輯通常和驗證程序混雜在一起。驗證過程經常散落在方法調用裡,這樣就造成很難區分驗證邏輯和核心業務邏輯。在大多數情況下,業務對象和方法必須瞭解驗證過程的細節,並在它們的實現裡直接處理它們 -- 例如,一個業務對象可能直接從業務方法裡拋出驗證異常(或者直接編碼在方法裡,或者作為調用驗證服務的結果)。但是,在這種情況下,驗證異常實際是驗證過程的副產品,理想情況下,應該隱藏業務對象的實現。

在這篇文章裡,我將向您展示一種更加去耦、更加集中的驗證方法,它利用 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()方法上執行的驗證,被配置到 驗證服務上,而不是由方法自己來執行。這種鬆散耦合,在許多情況下,可以解決前面例子的弱點:

代碼生成工具如何呢?

開發人員有時使用代碼生成工具把 boilerplate 驗證代碼插入業務方法。與動態代理方法類似,代碼生成工具讓您在編寫實現時不必考慮驗證。這二種方法之間的區別在於,與 動態代理不同,生成的代碼總是出現在業務對象裡,不能切換到其他實現(例如調用處理程序)裡。使用動態代理允許您在運行時動修改調用處理程序,不必重新編譯代碼或者重新部署應用程序。

  • 驗證規則有可能重用,因為它們可以只編寫一次,在裡面定義不同對象所使用的方法。例如,我可以寫一個驗證規則,用於斷言指定參數不為 null,然後在所有需要同樣規則的方法中重用它。
  • 驗證規則也可能是可配置的。例如,我可以用 XML 文檔初始化驗證服務,在 XML 文檔裡描述針對具體方法或對象需要執行的規則。我也可以把 API 公開到這個配置裡,這樣我就可以在運行時修改驗證規則。

雖然我們朝著正確的方向走了一小步,但是這種方法仍有不足。當我開發方法的時候,我不得不確保調用 驗證服務,確保方法實現聲明瞭ValidationException異常。這些都是驗證服務的工作,和方法的核心邏輯實際沒有任何關係。我實際想要的,是一種編寫 User接口實現的方法,這樣它就不需要知道這類事情了。

動態代理方法

動態代理是這樣一種類,它可以實現在運行時指定的一組接口。對代理類的方法調用,被分配給調用處理程序,而調用處理程序是在代理類生成的時候指定的。動態代理類有許多應用程序中使用的接口,其中一個可以用統一的方式有效地處理方法前和方法後的調用操作。因為驗證通常是方法前調用操作,所以動態代理為我們提供了針對前面示例所指出的問題的解決方案。 動態代理類給了我們一種以統一方式方便地處理任何方法上的驗證途徑,同時把所有的驗證邏輯完全與核心業務邏輯分離開。

因為在許多框架中,都存在針對主要業務對象和服務的接口,所以您對於交換進和交換出這類接口的不同實現應該有所體驗。使用動態 代理類與其非常類似,區別在於,客戶並不直接處理接口的實現,而是與代理類打交道,代理類負責實現接口、執行驗證、把方法調用委託給實現類。使用動態代理 方法,所有的驗證邏輯對於代理的客戶,都應當是透明的。因此,實現新的驗證方案應當會非常簡單:對於使用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類來委託所有的業務方法調用。

動態代理的缺點

不幸的是,使用動態代理類確實有一個重要不足:性能。對動態代理類的方法調用,不會像直接調用對象的方法那樣好。所以,在應用程序框架中對動態代理的使用,取決於什麼對您更重要:更整潔的架構還是更好的性能。在應用程序的許多方面,性能損失可能是值得的,但是在其他方面,性能則有可能是至關重要的。所以,有一種解決方案,就是在有些地方用動態代理,在其他地方則不用。如果您決定走這條路,一定要記住,除了驗證之外,調用處理程序還能做其他的事,這允許您在運行時或在代碼部署之後改變您的業務對象行為。

動態代理的其他用途

動態代理類在業務對象框架中有許多用途,不僅僅是用一致的方式對方法進行驗證。我前面建立的簡單調用處理程序 可以用於任何方法前和方法後的調用處理。例如,我可以很容易對業務方法計時,只要在業務對象的實現方法調用之前和之後插入代碼,就可計算方法經歷的時間。我還可以插入方法後調用邏輯,把狀態的變化通知給有興趣的偵聽者。

用 bean 進行驗證

除了本文中討論的方法之外,您還可以用 JavaBean 架構的受限屬性功能進行驗證。簡單點說,在受限屬性發生變化之前,可以通知任意數量的有興趣的偵聽者。如果有任何一個偵聽者不同意變化,它就可以拋出java.beans.PropertyVetoException異常,這意味著屬性的變化是不可接受的。

這個模型實際上和動態代理相處得非常好,因為整個消息傳遞機制都可以放在調用處理程序中。與使用本文前面描述的方法一樣,真實的業務對象實現不需要了解或者關心正在進行什麼類型的驗證。實際上,這類驗證可以遲一些引入,不會改變受限屬性所包含的任何方法實現。

在示例中,我只為一個接口建立了動態代理類,這個接口是: User。我可以很容易地指定動態代理類在運行時要實現的多個接口。用這種方式,靜態工廠方法返回的對象可以實現建立代理時所定義的任意數量的接口。調用處理程序類必須知道如何處理所有接口類型的方法調用。

您還應當注意到,在示例裡我一直調用實際的實現類來執行業務邏輯。這不是必需的。例如,代理類可以把方法調用委託其他任何對象,甚至是處理程序本身。一個簡單的例子就是有這樣一個接口,它通過 set/get 方法公開了大量 JavaBean 屬性。我還建立了一個專門的調用處理程序,它維持了一個 Map,在映射裡,鍵是屬性名稱,值是屬性的值。在 get 調用上,我替換 Map中保存的值,這就消除了為這些簡單的 JavaBean 類實現編寫代碼的需要。

結束語

使用動態代理類進行驗證是從應用程序的核心邏輯去耦驗證程序的簡單而有效的方法。與緊密耦合方法不同,使用動態代理給您帶來了可以重用、可以配置的驗證代碼。

在這篇文章裡,您看到了用調用處理使用動態代理的好處。因為動態代理類上的方法調用都可以通過公共的調用處理程序進行傳遞,您可以非常容易地修改處理程序執行的邏輯,甚至在已經部署的代碼中修改或者在運行時動態修改。還可以重構調用處理程序,讓它處理其他橫跨不同對象類型的方法調用的操作。

Java 平臺的動態代理功能不是從核心代碼的業務邏輯中去耦驗證程序的唯一選項。在某些情況下,例如在性能是應用程序的主要考慮因素的地方,就可能不是最佳選項。雖然本文側重在動態代理上,我還是討論了其他一些選項,包括 JavaBean 的受限屬性功能,以及代碼生成工具的使用。使用任何一種技術,您都應當仔細評估替代品,只有當動態代理是應用程序的最佳解決方案時才使用它。

利用動態代理的 Java 驗證


分享到:


相關文章: