http://dev2dev.bea.com.cn/bbs/jishudata/ArticleShow.jsp?Id=17
作者:sdj21,rocksun(dev2dev ID)
摘要:
本文介紹了如何使用JDK5.0的新特性Annotation和Spring框架來實現一種簡單的JavaControl框架,主要實現了成員變量的自動帶入,以及方法調用的權限檢查控制,同時實現了一個JavaDatabaseControl的擴展,模仿workshop中的對應功能。
目錄:
環境
背景資料
JavaControl最基本的功能--聲明注入
使用動態代理來增加橫切
Control方法的權限檢查
一個DatabaseControl的特例
關于Demo程序
框架如何的加強
感受
代碼下載
參考文檔
環境 (目錄)
由于使用了Annotation,所以必須準備好JDK5.0
背景知識 (目錄)
1)Annotation簡介
在Java領域,最早的Annotation就是JavaDoc了,將文檔直接寫在源程序里極大的方便了文檔的編寫,后來出現了許多有這種同步需求的工作,大家便發明了XDoclet,使用類似于JavaDoc的語法撰寫描述信息,并使用工具生成描述文件。到JDK5.0出現以前這種需求已經更多了,許多工具和框架已經通過各種方式實現自己的這種標記,.NET更是率先推出了語言級的支持,所以JDK5.0終于推出了自己的Annotation。
以下是兩個簡單的Annotation定義,定義的方式類似于接口,只是在interface前面多了個"@"
public @interface SampleAnnotation{
String someValue;
}
public @interface NormalAnnotation{
String value1;
int value2;
}
然后我們在程序里可以這樣使用Annotation,我們的編程工具或者是運行中的框架程序可以讀取這些內容,生成代碼或者是添加動作。
@SampleAnnotation (someValue ="Actual Value used in the program”)
public void annotationUsingMethod(){
…
}
Annotation主要是給工具開發商和框架開發者使用的工具,一般的編程人員可能僅僅是使用其他人開發的Annotation。Workshop在兩年前已經開始嘗試在開發工具里運用Annotaion,但當時沒有語言級的支持,所有的Annotation都是以注釋的形式出現,這樣雖然靈活但是不嚴謹,也不適于推廣,所以Annotation的出現可以大大方便開發商的工作,使得許多小開發商以及一般的架構設計人員也可以利用這種方式編程。
注:本文中Annotation可能對應的名字是“注釋”或者是“說明”
2)Spring和DI(IOC)簡介
在我看來,Spring和Annotation能完成很多相似的工作,它們之間的區別是在解決問題的位置并不相同。
這是一個Spring的配置文件的信息,里面定義了三個bean,其中的exampleBean的屬性中引用了其他的bean
[bean="yetAnotherBean"/>]
1
我們用以下方式調用
InputStream is = new FileInputStream("beans.xml");
XmlBeanFactory factory = new XmlBeanFactory(is);
Object o = factory.get("exampleBean");
通過以上方式我們可以得到一個exampleBean的實例,并且里面的一些屬性已經被預先注入了。
在Spring結合了一些動態代理的以后,我們完全可以實現Annotation所能做到的許多功能。但是我們可以看出兩種方式是不一樣的,使用Spring面臨著同步問題,維護配置文件比較的麻煩;而使用Annotation時,我們通常不容易在不改變原代碼的時候改變一些特性,而且有時候也面臨著代碼復用的問題。本文并不討論這些內容,本文里Spring只是一個bean的容器,用來存放JavaControl的注冊信息,不會涉及依賴注入,而使用Annotation來完成相應的功能。
注:在本文里使用了兩個配置文件,bean.xml放置了我們測試用的Control信息,在實際環境中可能需要不斷添加新的Control。另一個配置文件是config.xml,里面是我們定義的ControlWrapper,每當我們增加一種Control的時候,如DatabaseControl的時候,就需要添加一個Wrapper。你應該首先看看這兩個文件,里面有demo程序的配置以及注冊的Control。
JavaControl最基本的功能--聲明注入(目錄)
在JavaControl里編程的時候,我們通常并不會顯式的初始化JavaControl,因為具體的實現不應該在程序里綁定,而應該完全的面向接口編程,我們只是簡單的在聲明Control的地方加一句簡單的注釋:
/**
* @common:control
*/
private myJavaControl.JCSecond jCSecond;
然后在我們的Control里就可以直接使用jCSecond,如下:
/**
* @common:operation
*/
public String serviceA()
{
System.out.println("This is serviceA");
return "This is serviceA"+":"+jCSecond.serviceB();
}
Workshop>平臺會根據這些注釋,自動生成代碼,來完成初始化jCSecond的動作,來完成注入的工作。而我們的程序會在程序運行中讀取注釋信息,來完成注入工作,這與workshop不同。
至于這個接口具體要使用哪個實現類,大家可以看看workshop的打包文件的META-INFjc-jar.xml,里面有所有JavaControl的說明,包括接口和實現,這說明了我們以后可以改變實現。
下面我們實現自己的聲明注入,這里我們需要一個JavaControlFactory類,作為進入Control環境的接口。兩個Annotation,用來說明Control的實現類和包裝器。JavaControlFactory類來讀取Control中的注釋信息,完成包裝等工作。
1)首先我們需要定義一個修飾成員變量的Annotation,所有被當作Control的成員變量都可以使用這個annotation來說明,完整的定義如下:
package net.rocksun.tiffany.annotation;
import java.lang.annotation.*;
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({java.lang.annotation.ElementType.FIELD})
public @interface Control {
public String name();
}
這是個簡單的annotation,@Retention說明本Annotation要保留到什么時候,因為我們需要在程序運行時動態的讀取,所以設置為RUNTIME。@Target說明了本annotation所針對的對象,是FIELD。
我們在Control里這樣使用這個annotation:
@Control(name="second")
private SecondControl second;
name是SecondControl的實現類名稱,我們在這里說明,然后通過JavaControlFactory來根據名字自動的將SecondControl實例化。
2)我們還需要一個說明類的ControlType,用來注釋所有的Control類:
package net.rocksun.tiffany.annotation;
import java.lang.annotation.*;
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({java.lang.annotation.ElementType.TYPE})
public @interface ControlType {
public String name();
}
它也是運行級別保留,但它針對的對象是類,只有類可以使用這個annotation注釋。name表示這個類需要的包裝器,也就是用來對這個類進行特殊處理的類。
使用方式如下
.............
@ControlType(name="DefaultControl")
public class FirstControlImpl implements FirstControl {
.............
@ControlType(name="DefaultControl")說明這個類需要使用DefaultControl類型的包裝器包裝一下。
3)ControlFactory實現
我們希望從JavaControl中取得類的方式為FirstControl first = (FirstControl)factory.getControl("first");并且first中所有的標記為Control的成員變量都可以被正確的初始化。
所以我們的ControlFactory首先應該遍歷類first的所有成員變量,當遇到使用Control注釋的成員變量,根據它的類型進行實例化。以下是遍歷所有成員變量的過程:
for (int i = 0; i < fields.length; i++) {
//判斷是否為Control,也就是檢查是否使用@Control注釋
if(isControl(fields[i])){
Object tempBean = getControlInstance(fields[i]);
if(tempBean==null){
throw new IllegalArgumentException(bean.getClass()+":"+fields[i].getName()+"’s annotation error");
}else{
//如果檢查配置沒有問題,就設置這個值,同時檢查這個Bean的成員是不是也需要實例化
wrappedBean.setPropertyValue(fields[i].getName(), tempBean);
initBean(tempBean);
}
}
}
需要注意的是,所有的Control應該使用JavaBean的方式,所有成員變量應該有無參的構造方法,并且成員都有get和set方法。
其中isControl方法如下:
private boolean isControl(Field field){
Annotation[] annotation = field.getDeclaredAnnotations();
for (int i = 0; i < annotation.length; i++){
if(annotation[i] instanceof Control){
return true;
}
}
return false;
}
我們遍歷這個字段的所有annotation,來檢查有沒有Control,如果有就執行getControlInstance,來實例化這個Control。getControlInstance會檢查我們在類上作的ControlType注釋,來選擇包裝器,進行bean的包裝。
4)在實例化完成后,根據類的ControlType Annotation我們可以對類進行包裝,每一個包裝類實現如下接口:
public interface ControlWrapper {
public Object wrapBean(Object bean);
}
這樣根據指定的ControlType的不同,我們還可以另外使用我們自定義的Control包裝器,進行特殊的操作,我們首先實現了一個默認的DefaultControlType,不作任何操作。
到目前,我們的JavaControl最基本框架已經完成了,我們可以測試結果,可以看到,我們通過注釋說明,就可以實現對成員變量的自動初始化,我們通過外部文件的配置就可以在以后方便的修改實現類。
可以察看源代碼中ControlFactoryTest的testGetControl的運行結果。
使用動態代理來增加橫切(目錄)
對于每一個方法被執行的時候,我們希望可以進行一種橫切,如每一個方法執行前我們要記一個Log,這就需要添加一個動態代理,當然我們也可以使用其他種方式,但是動態代理是最優雅的。
我們增加DefaultControlProxy類來處理橫切的動作,DefaultControlProxy是InvocationHandler的子類,實現了ivoke方法,代碼如下:
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
log.info("method "+method.getName()+" invoked");
return method.invoke(getDelegate(),args);
}
我們不作額外的操作,只是log被執行的方法,這樣,所有的Control方法在執行的時候都會紀錄log,這里的delegate是我們原來的對象,我們保留這個還有很多用處?! 榱送瓿蛇@些橫切,我們必須修改DefaultControlWrapper,加入代碼如下
public Object wrapBean(Object bean) {
DefaultControlProxy proxy = new DefaultControlProxy();
proxy.setDelegate(bean);
System.out.println("wrap................"+bean.getClass());
return Proxy.newProxyInstance(this.getClass().getClassLoader(),bean.getClass().getInterfaces(),proxy);
}
重新運行我們的測試,結果并不影響。只是在每個Control的方法執行以前,打印了句Log信息。
Control方法的權限檢查(目錄)
workshop可以指定一個方法的角色,我們已經有了橫切,所以這步工作也并不困難。首先定義一個Annotation,名字是Role,代碼如下:
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target( {java.lang.annotation.ElementType.METHOD})
public @interface Role {
public String name();
}
沒有好說的,與前面及唯一的區別就是對象變成了方法,我們可以這樣聲明來表示這個方法必需要角色admin來參與:
@Role(name="admin")
public String doTaskWithRole(){
return "admin";
}
對于每一個Control必須在建立的時候,告訴他所擁有的角色,所以修改所有JavaControl的基類BaseControl如下:
private String role;
public BaseControl() {
}
public void setRole(String role) {
this.role = role;
}
public String getRole() {
return role;
}
有了這些修改,我們可以在我們的橫切代碼那里增加檢查權限的功能
Method m = delegate.getClass().getMethod(method.getName(),classes);
Annotation[] annotation = m.getAnnotations();
for (int i = 0; i < annotation.length; i++) {
if(annotation[i] instanceof Role){
log.info("Method "+method.getName()+" should check role");
if(((BaseControl)getDelegate()).getRole().equals(((Role)annotation[i]).name())){
}else{
throw new IllegalAccessException("Method "+method.getName()+" requier Role "+((Role)annotation[i]).name());
}
}
}
以上代碼就是增加的檢查,我們使用Method m = delegate.getClass().getMethod(method.getName())得到新的Method,因為傳遞給我們的Method對象是代理過的,所以我們必須使用原始的類的定義,當出現權限不足時,一個例外就會拋出。對于我們的測試用例,我們增加testHasRoleCheckFailed()>和testHasRoleCheckSuccess()>用例,分別測試在沒有和有權限的時候會不會有例外發生。
一個DatabaseControl的特例 (目錄)
我們可以擴展我們的JavaControl了,我們定義一個新的Annotation----DatabaseMethod
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({java.lang.annotation.ElementType.METHOD})
public @interface DatabaseMethod {
public String sql();
public String dataSource() default "dataSource";
}
這個Annotation有兩個成員,其中dataSource()有缺省值,也就是程序里注釋的時候,可以不輸入dataSource參數。
@ControlType(name="DatabaseControl")
public class DatabaseControl1Impl extends BaseControl implements DatabaseControl1{
@DatabaseMethod(sql="insert into user values(:0,:1)")
public void insertUser(int id , String name) {
}
}
這就是一個使用DatabaseControl的例子,首先說明這個Control是一個DatabaseControl,需要使用net.rocksun.tiffany.wrapper.DatabaseControlWrapper來進行包裝。再就是對應的我們要修改DatabaseControlWrapper類,同時參照DefaultControl,我們要對DatabaseControlProxy進行修改,使之可以處理sql:
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
log.info(delegate.getClass()+ "’s method "+method.toString()+" invoked");
Class[] classes = null;
if(args!=null){
classes = new Class[args.length];
for (int i = 0; i < args.length; i++) {
classes[i] = args[i].getClass();
if(args[i] instanceof Integer){
classes[i] = Integer.TYPE;
}
}
}
Method m = delegate.getClass().getMethod(method.getName(),classes);
Annotation[] annotation = m.getAnnotations();
for (int i = 0; i < annotation.length; i++) {
if(annotation[i] instanceof DatabaseMethod){
String sql = ((DatabaseMethod)annotation[i]).sql();
String dataSource = ((DatabaseMethod)annotation[i]).dataSource();
for (int j = 0; j < args.length; j++) {
if(args[j] instanceof Integer){
sql = sql.replace(":"+j,args[j].toString());
}else{
sql = sql.replace(":"+j,"’"+args[j].toString()+"’");
}
}
log.info(" will execute sql ’"+ sql +"’ for dataSource ’"+dataSource+"’");
}
}
return method.invoke(getDelegate(),args);
}
這是整個方法的定義,并沒有考慮到所有的類型,也沒有實際執行sql,只是告訴大家我們這個時候已經有了操作數據庫的能力了。運行測試用例的testInsertUser,我們就可以看到我們把sql處理成可以執行的狀態。
關于Demo程序
所有的Demo Control都在net.rocksun.tiffany.demo下,里面有FirstControl,SecondControl,DatabaseControl1三個Control和它們的實現,所有的Control實現都是BaseControl的子類,BaseControl幫助它的子類保存角色信息。
FirstControl中有SecondControl和DatabaseControl1的一個實例,FirstControl的三個方法,分別為doTask,doTaskWithRole,insertUser,其中第一個演示了成員變量的自動注入,第二個包括了權限檢查,第三個是調用DatabaseControl控件。
有了這些,我們就可以使用ControlFactoryTest進行測試
框架如何的加強(目錄)
作為框架程序,還有許多事情要做。如數據庫控件,我們可以添加一種事物控制標示,在需要啟動事物的方法前添加一種annotation,方法里所有DatabaseControl使用相同的數據庫連接,并且根據拋出的Exception來選擇提交和會滾。有時候建立一種通用的控件框架是比較麻煩的,但如果在一個自己寫的框架下,在方法前增加注釋來說明一些額外的操作也是很方便的選擇。
由于沒有工具,所以使用我們自己的Control框架并不容易,需要自己寫接口,然后是實現,然后是配置信息,而使用workshop則可以只關心寫實現,接口和配置都交給workshop自動的完成。使用Control最方便的地方就是可以很好的與編程工具結合,并且可以使代碼更規范,但如果沒有工具,就比較難說了。
目前這個框架還有很多問題,注冊新控件太麻煩,包裝程序重復工作太多,還沒有很好的復用。再就是BaseControl應該更好的包裝,空間聲明應該使用新Annotation類型,而不應該使用名稱作為參數的方式,因為使用新類型,可以在編譯時檢查,減少錯誤發生的可能。
對于頁面流等技術我們已可以自己實現,但核心思想是一樣的。
感受
記得看《XDoclet in Action》的時候,說XDoclet生成配置文件的時候,有兩種目的,一種是直接可以用,另一種是作為模版,第一種方式更好一些,第二種通常是沒有辦法時的選擇。Annotation和Spring其實就是XDoclet的兩種狀態,當Annotation是代碼級的時候,我們可以用工具得到對應的Spring配置文件,但是這就意味著,我們可能最好不要直接修改Spring的配置文件,因為下一次生成就把這些修改覆蓋了,但這樣就失去了通過配置來靈活改變一些設置的可能性。同樣,如果我們完全依賴自己配置文件,可能會很麻煩。
但我覺得使用配置雖然麻煩一些,但是確實是更清楚一些,如果有可能的話應該結合使用,實現靈活性與簡單性的結合。
Annotation,DIIOC)等等,這些東西出現的目的都差不多,都是為了減少這些橫切代碼的重復工作。我也越來越感覺萬變不離其宗了。JavaControl就是想提供這樣一個環境,在這個環境里可以自動的讀取Annotation來完成橫切的工作,這比使用spring確實方便一些,因為沒有這樣一個環境,Spring只能通過配置實現,這樣的重復工作也太多。
本文代碼下載 (請選擇目標另存為 tiffany.rar)
參考文檔
這一次幾乎沒參考什么,只是看了些Annotation的參考,但是忘記了地址。
如果有就是這個了:oreilly_.java.1.5.tiger.a.developers.notebook.(2004)
關于作者:
sdj21,rocksun(dev2dev ID),軟件工程師,
郵件地址:sdj21@sina.com , daijun@gmail.com ,sundaijun@126.com