隨筆 - 63  文章 - 0  trackbacks - 0
          <2009年5月>
          262728293012
          3456789
          10111213141516
          17181920212223
          24252627282930
          31123456

          常用鏈接

          留言簿(2)

          隨筆分類

          隨筆檔案

          搜索

          •  

          最新評(píng)論

          閱讀排行榜

          評(píng)論排行榜

          UserType and CompositeUserType為Hibernate中提供的用戶類型自定義接口。根據(jù)這個(gè)接口,可以實(shí)現(xiàn)自定義的數(shù)據(jù)類型。 

          最近看<<深入淺出Hibernate>>,作者在書中介紹了Hibernate 提供的用戶自定義數(shù)據(jù)類型,于是效仿書中的方法,在數(shù)據(jù)庫(kù)表中添加一個(gè)字段用于記錄所有好友的userid,每個(gè)userid之間用";"加以分隔,同時(shí)將該字段映射為一個(gè)特殊的List集合,利用UserType interface實(shí)現(xiàn)String解析后將各個(gè)userid封裝在List中,將List中的記錄封裝成以";"分隔的String。
          使用UserType接口,實(shí)現(xiàn)了較好的設(shè)計(jì)風(fēng)格,以及更好的重用性。
          /*
          * 用戶自定義的數(shù)據(jù)類型,對(duì)應(yīng)數(shù)據(jù)庫(kù)中的一個(gè)字段,在該字段中,保存了
          * 多個(gè)用戶需要的信息,之間用";"加以分隔.
          * @Author:Paul
          * @Date:April 18th,2008
          */
          package com.globalhands.hibernate.userTypes;

          import java.io.Serializable;
          import java.sql.PreparedStatement;
          import java.sql.ResultSet;
          import java.sql.SQLException;
          import java.util.ArrayList;
          import java.util.List;
          import java.sql.Types;

          import org.hibernate.Hibernate;
          import org.hibernate.HibernateException;
          import org.hibernate.usertype.UserType;

          public class SpecialList implements UserType {
          private List specialList;

          private static final char SPLITTER = ';';

          private static final int[] TYPES = new int[] { Types.VARCHAR };

          public String assemble(Serializable arg0, Object arg1)
          throws HibernateException {
          return null;
          }

          /*
          * 將List封裝為一個(gè)String對(duì)象
          */
          public String assemble(List specialList) throws HibernateException {
          StringBuffer sb = new StringBuffer();
          for (int i = 0; i < specialList.size() - 1; i++) {
          sb.append(specialList.get(i)).append(this.SPLITTER);
          }
          sb.append(specialList.get(specialList.size() - 1));
          return sb.toString();
          }

          /*
          * 創(chuàng)建一個(gè)新的List實(shí)例,包含原有的List實(shí)例中的所有元素.
          */
          public Object deepCopy(Object value) throws HibernateException {
          List sourceList = (List) value;
          List targetList = new ArrayList();
          targetList.addAll(sourceList);
          return targetList;
          }

          public Serializable disassemble(Object arg0) throws HibernateException {
          return null;
          }

          /*
          * 判斷specialList是否發(fā)生變化
          */
          public boolean equals(Object x, Object y) throws HibernateException {
          if (x == y) {
          return true;
          }
          if (x != null && y != null) {
          List xList = (List) x;
          List yList = (List) y;

          if (xList.size() != yList.size()) {
          return false;
          }

          for (int i = 0; i <= xList.size() - 1; i++) {
          String str1 = (String) xList.get(i);
          String str2 = (String) yList.get(i);
          if (!xList.equals(yList)) {
          return false;
          }
          }
          return true;
          }
          return false;
          }

          public int hashCode(Object arg0) throws HibernateException {
          return 0;
          }

          public boolean isMutable() {
          return false;
          }

          /*
          * 從resultset中取出email字段,并將其解析為L(zhǎng)ist類型后返回
          */
          public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
          throws HibernateException, SQLException {
          String value = (String) Hibernate.STRING.nullSafeGet(rs, names[0]);
          if (value != null) {
          return parse(value);
          } else {
          return null;
          }
          }

          /*
          * 將以";"分隔的字符串解析為一個(gè)字符串?dāng)?shù)組
          */
          private List parse(String value) {
          String[] strs = value.split(";");
          List specialList = new ArrayList();
          for (int i = 0; i <= strs.length - 1; i++) {
          specialList.add(strs[i]);
          }
          return specialList;
          }

          /*
          * 將List型的email信息組裝成字符串之后保存到email字段
          */
          public void nullSafeSet(PreparedStatement st, Object value, int index)
          throws HibernateException, SQLException {
          if (value != null) {
          String str = assemble((List) value);
          Hibernate.STRING.nullSafeSet(st, str, index);
          } else {
          Hibernate.STRING.nullSafeSet(st, value, index);
          }
          }

          public Object replace(Object arg0, Object arg1, Object arg2)
          throws HibernateException {
          return null;
          }

          public Class returnedClass() {
          return List.class;
          }

          public int[] sqlTypes() {
          return TYPES;
          }

          }
          同時(shí),修改相應(yīng)的[ormapping_filename].hbm.xml中相應(yīng)字段的映射信息
          <property name="buddy" type="com.globalhands.hibernate.userTypes.SpecialList">
                      <column name="buddy" length="2000" not-null="true" />
                  </property>
          之后,修改POJO類中該字段的返回類型為L(zhǎng)ist。
          使用JUnit測(cè)試程序。
          posted @ 2009-05-24 19:57 lanxin1020 閱讀(602) | 評(píng)論 (0)編輯 收藏
          posted @ 2009-05-21 22:13 lanxin1020 閱讀(161) | 評(píng)論 (0)編輯 收藏

          什么是 ASM?

          ASM 是一個(gè) Java 字節(jié)碼操控框架。它能被用來(lái)動(dòng)態(tài)生成類或者增強(qiáng)既有類的功能。ASM 可以直接產(chǎn)生二進(jìn)制 class 文件,也可以在類被加載入 Java 虛擬機(jī)之前動(dòng)態(tài)改變類行為。Java class 被存儲(chǔ)在嚴(yán)格格式定義的 .class 文件里,這些類文件擁有足夠的元數(shù)據(jù)來(lái)解析類中的所有元素:類名稱、方法、屬性以及 Java 字節(jié)碼(指令)。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。

          與 BCEL 和 SERL 不同,ASM 提供了更為現(xiàn)代的編程模型。對(duì)于 ASM 來(lái)說(shuō),Java class 被描述為一棵樹(shù);使用 “Visitor” 模式遍歷整個(gè)二進(jìn)制結(jié)構(gòu);事件驅(qū)動(dòng)的處理方式使得用戶只需要關(guān)注于對(duì)其編程有意義的部分,而不必了解 Java 類文件格式的所有細(xì)節(jié):ASM 框架提供了默認(rèn)的 “response taker”處理這一切。

          為什么要?jiǎng)討B(tài)生成 Java 類?

          動(dòng)態(tài)生成 Java 類與 AOP 密切相關(guān)的。AOP 的初衷在于軟件設(shè)計(jì)世界中存在這么一類代碼,零散而又耦合:零散是由于一些公有的功能(諸如著名的 log 例子)分散在所有模塊之中;同時(shí)改變 log 功能又會(huì)影響到所有的模塊。出現(xiàn)這樣的缺陷,很大程度上是由于傳統(tǒng)的 面向?qū)ο缶幊套⒅匾岳^承關(guān)系為代表的“縱向”關(guān)系,而對(duì)于擁有相同功能或者說(shuō)方面 (Aspect)的模塊之間的“橫向”關(guān)系不能很好地表達(dá)。例如,目前有一個(gè)既有的銀行管理系統(tǒng),包括 Bank、Customer、Account、Invoice 等對(duì)象,現(xiàn)在要加入一個(gè)安全檢查模塊, 對(duì)已有類的所有操作之前都必須進(jìn)行一次安全檢查。


          圖 1. ASM – AOP
          圖 1. ASM – AOP

          然而 Bank、Customer、Account、Invoice 是代表不同的事務(wù),派生自不同的父類,很難在高層上加入關(guān)于 Security Checker 的共有功能。對(duì)于沒(méi)有多繼承的 Java 來(lái)說(shuō),更是如此。傳統(tǒng)的解決方案是使用 Decorator 模式,它可以在一定程度上改善耦合,而功能仍舊是分散的 —— 每個(gè)需要 Security Checker 的類都必須要派生一個(gè) Decorator,每個(gè)需要 Security Checker 的方法都要被包裝(wrap)。下面我們以 Account 類為例看一下 Decorator:

          首先,我們有一個(gè) SecurityChecker 類,其靜態(tài)方法 checkSecurity 執(zhí)行安全檢查功能:

          public class SecurityChecker {   public static void checkSecurity() {    System.out.println("SecurityChecker.checkSecurity ...");    //TODO real security check   }   }       

          另一個(gè)是 Account 類:

          public class Account {   public void operation() {    System.out.println("operation...");    //TODO real operation   }  }       

          若想對(duì) operation 加入對(duì) SecurityCheck.checkSecurity() 調(diào)用,標(biāo)準(zhǔn)的 Decorator 需要先定義一個(gè) Account 類的接口:

          public interface Account {   void operation();   }       

          然后把原來(lái)的 Account 類定義為一個(gè)實(shí)現(xiàn)類:

          public class AccountImpl extends Account{   public void operation() {    System.out.println("operation...");    //TODO real operation   }  }        

          定義一個(gè) Account 類的 Decorator,并包裝 operation 方法:

          public class AccountWithSecurityCheck implements Account {    private  Account account;   public AccountWithSecurityCheck (Account account) {    this.account = account;   }   public void operation() {    SecurityChecker.checkSecurity();    account.operation();   }  }       

          在這個(gè)簡(jiǎn)單的例子里,改造一個(gè)類的一個(gè)方法還好,如果是變動(dòng)整個(gè)模塊,Decorator 很快就會(huì)演化成另一個(gè)噩夢(mèng)。動(dòng)態(tài)改變 Java 類就是要解決 AOP 的問(wèn)題,提供一種得到系統(tǒng)支持的可編程的方法,自動(dòng)化地生成或者增強(qiáng) Java 代碼。這種技術(shù)已經(jīng)廣泛應(yīng)用于最新的 Java 框架內(nèi),如 Hibernate,Spring 等。

          為什么選擇 ASM?

          最直接的改造 Java 類的方法莫過(guò)于直接改寫 class 文件。Java 規(guī)范詳細(xì)說(shuō)明了class 文件的格式,直接編輯字節(jié)碼確實(shí)可以改變 Java 類的行為。直到今天,還有一些 Java 高手們使用最原始的工具,如 UltraEdit 這樣的編輯器對(duì) class 文件動(dòng)手術(shù)。是的,這是最直接的方法,但是要求使用者對(duì) Java class 文件的格式了熟于心:小心地推算出想改造的函數(shù)相對(duì)文件首部的偏移量,同時(shí)重新計(jì)算 class 文件的校驗(yàn)碼以通過(guò) Java 虛擬機(jī)的安全機(jī)制。

          Java 5 中提供的 Instrument 包也可以提供類似的功能:?jiǎn)?dòng)時(shí)往 Java 虛擬機(jī)中掛上一個(gè)用戶定義的 hook 程序,可以在裝入特定類的時(shí)候改變特定類的字節(jié)碼,從而改變?cè)擃惖男袨椤5瞧淙秉c(diǎn)也是明顯的:

          • Instrument 包是在整個(gè)虛擬機(jī)上掛了一個(gè)鉤子程序,每次裝入一個(gè)新類的時(shí)候,都必須執(zhí)行一遍這段程序,即使這個(gè)類不需要改變。
          • 直接改變字節(jié)碼事實(shí)上類似于直接改寫 class 文件,無(wú)論是調(diào)用 ClassFileTransformer. transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer),還是 Instrument.redefineClasses(ClassDefinition[] definitions),都必須提供新 Java 類的字節(jié)碼。也就是說(shuō),同直接改寫 class 文件一樣,使用 Instrument 也必須了解想改造的方法相對(duì)類首部的偏移量,才能在適當(dāng)?shù)奈恢蒙喜迦胄碌拇a。

          盡管 Instrument 可以改造類,但事實(shí)上,Instrument 更適用于監(jiān)控和控制虛擬機(jī)的行為。

          一種比較理想且流行的方法是使用 java.lang.ref.proxy。我們?nèi)耘f使用上面的例子,給 Account 類加上 checkSecurity 功能:

          首先,Proxy 編程是面向接口的。下面我們會(huì)看到,Proxy 并不負(fù)責(zé)實(shí)例化對(duì)象,和 Decorator 模式一樣,要把 Account 定義成一個(gè)接口,然后在 AccountImpl 里實(shí)現(xiàn) Account 接口,接著實(shí)現(xiàn)一個(gè) InvocationHandler Account 方法被調(diào)用的時(shí)候,虛擬機(jī)都會(huì)實(shí)際調(diào)用這個(gè) InvocationHandlerinvoke 方法:

          class SecurityProxyInvocationHandler implements InvocationHandler {   private Object proxyedObject;   public SecurityProxyInvocationHandler(Object o) {    proxyedObject = o;   }       public Object invoke(Object object, Method method, Object[] arguments)    throws Throwable {       if (object instanceof Account && method.getName().equals("opertaion")) {     SecurityChecker.checkSecurity();    }    return method.invoke(proxyedObject, arguments);   }  }    

          最后,在應(yīng)用程序中指定 InvocationHandler 生成代理對(duì)象:

          public static void main(String[] args) {   Account account = (Account) Proxy.newProxyInstance(    Account.class.getClassLoader(),    new Class[] { Account.class },    new SecurityProxyInvocationHandler(new AccountImpl())   );   account.function();  }   

          其不足之處在于:

          • Proxy 是面向接口的,所有使用 Proxy 的對(duì)象都必須定義一個(gè)接口,而且用這些對(duì)象的代碼也必須是對(duì)接口編程的:Proxy 生成的對(duì)象是接口一致的而不是對(duì)象一致的:例子中 Proxy.newProxyInstance 生成的是實(shí)現(xiàn) Account 接口的對(duì)象而不是 AccountImpl 的子類。這對(duì)于軟件架構(gòu)設(shè)計(jì),尤其對(duì)于既有軟件系統(tǒng)是有一定掣肘的。
          • Proxy 畢竟是通過(guò)反射實(shí)現(xiàn)的,必須在效率上付出代價(jià):有實(shí)驗(yàn)數(shù)據(jù)表明,調(diào)用反射比一般的函數(shù)開(kāi)銷至少要大 10 倍。而且,從程序?qū)崿F(xiàn)上可以看出,對(duì) proxy class 的所有方法調(diào)用都要通過(guò)使用反射的 invoke 方法。因此,對(duì)于性能關(guān)鍵的應(yīng)用,使用 proxy class 是需要精心考慮的,以避免反射成為整個(gè)應(yīng)用的瓶頸。

          ASM 能夠通過(guò)改造既有類,直接生成需要的代碼。增強(qiáng)的代碼是硬編碼在新生成的類文件內(nèi)部的,沒(méi)有反射帶來(lái)性能上的付出。同時(shí),ASM 與 Proxy 編程不同,不需要為增強(qiáng)代碼而新定義一個(gè)接口,生成的代碼可以覆蓋原來(lái)的類,或者是原始類的子類。它是一個(gè)普通的 Java 類而不是 proxy 類,甚至可以在應(yīng)用程序的類框架中擁有自己的位置,派生自己的子類。

          相比于其他流行的 Java 字節(jié)碼操縱工具,ASM 更小更快。ASM 具有類似于 BCEL 或者 SERP 的功能,而只有 33k 大小,而后者分別有 350k 和 150k。同時(shí),同樣類轉(zhuǎn)換的負(fù)載,如果 ASM 是 60% 的話,BCEL 需要 700%,而 SERP 需要 1100% 或者更多。

          ASM 已經(jīng)被廣泛應(yīng)用于一系列 Java 項(xiàng)目:AspectWerkz、AspectJ、BEA WebLogic、IBM AUS、OracleBerkleyDB、Oracle TopLink、Terracotta、RIFE、EclipseME、Proactive、Speedo、Fractal、EasyBeans、BeanShell、Groovy、Jamaica、CGLIB、dynaop、Cobertura、JDBCPersistence、JiP、SonarJ、Substance L&F、Retrotranslator 等。Hibernate 和 Spring 也通過(guò) cglib,另一個(gè)更高層一些的自動(dòng)代碼生成工具使用了 ASM。





          回頁(yè)首


          Java 類文件概述

          所謂 Java 類文件,就是通常用 javac 編譯器產(chǎn)生的 .class 文件。這些文件具有嚴(yán)格定義的格式。為了更好的理解 ASM,首先對(duì) Java 類文件格式作一點(diǎn)簡(jiǎn)單的介紹。Java 源文件經(jīng)過(guò) javac 編譯器編譯之后,將會(huì)生成對(duì)應(yīng)的二進(jìn)制文件(如下圖所示)。每個(gè)合法的 Java 類文件都具備精確的定義,而正是這種精確的定義,才使得 Java 虛擬機(jī)得以正確讀取和解釋所有的 Java 類文件。


          圖 2. ASM – Javac 流程
          圖 2. ASM – Javac 流程

          Java 類文件是 8 位字節(jié)的二進(jìn)制流。數(shù)據(jù)項(xiàng)按順序存儲(chǔ)在 class 文件中,相鄰的項(xiàng)之間沒(méi)有間隔,這使得 class 文件變得緊湊,減少存儲(chǔ)空間。在 Java 類文件中包含了許多大小不同的項(xiàng),由于每一項(xiàng)的結(jié)構(gòu)都有嚴(yán)格規(guī)定,這使得 class 文件能夠從頭到尾被順利地解析。下面讓我們來(lái)看一下 Java 類文件的內(nèi)部結(jié)構(gòu),以便對(duì)此有個(gè)大致的認(rèn)識(shí)。

          例如,一個(gè)最簡(jiǎn)單的 Hello World 程序:

          public class HelloWorld {   public static void main(String[] args) {    System.out.println("Hello world");   }  }  

          經(jīng)過(guò) javac 編譯后,得到的類文件大致是:


          圖 3. ASM – Java 類文件
          圖 3. ASM – Java 類文件

          從上圖中可以看到,一個(gè) Java 類文件大致可以歸為 10 個(gè)項(xiàng):

          • Magic:該項(xiàng)存放了一個(gè) Java 類文件的魔數(shù)(magic number)和版本信息。一個(gè) Java 類文件的前 4 個(gè)字節(jié)被稱為它的魔數(shù)。每個(gè)正確的 Java 類文件都是以 0xCAFEBABE 開(kāi)頭的,這樣保證了 Java 虛擬機(jī)能很輕松的分辨出 Java 文件和非 Java 文件。
          • Version:該項(xiàng)存放了 Java 類文件的版本信息,它對(duì)于一個(gè) Java 文件具有重要的意義。因?yàn)?Java 技術(shù)一直在發(fā)展,所以類文件的格式也處在不斷變化之中。類文件的版本信息讓虛擬機(jī)知道如何去讀取并處理該類文件。
          • Constant Pool:該項(xiàng)存放了類中各種文字字符串、類名、方法名和接口名稱、final 變量以及對(duì)外部類的引用信息等常量。虛擬機(jī)必須為每一個(gè)被裝載的類維護(hù)一個(gè)常量池,常量池中存儲(chǔ)了相應(yīng)類型所用到的所有類型、字段和方法的符號(hào)引用,因此它在 Java 的動(dòng)態(tài)鏈接中起到了核心的作用。常量池的大小平均占到了整個(gè)類大小的 60% 左右。
          • Access_flag:該項(xiàng)指明了該文件中定義的是類還是接口(一個(gè) class 文件中只能有一個(gè)類或接口),同時(shí)還指名了類或接口的訪問(wèn)標(biāo)志,如 public,private, abstract 等信息。
          • This Class:指向表示該類全限定名稱的字符串常量的指針。
          • Super Class:指向表示父類全限定名稱的字符串常量的指針。
          • Interfaces:一個(gè)指針數(shù)組,存放了該類或父類實(shí)現(xiàn)的所有接口名稱的字符串常量的指針。以上三項(xiàng)所指向的常量,特別是前兩項(xiàng),在我們用 ASM 從已有類派生新類時(shí)一般需要修改:將類名稱改為子類名稱;將父類改為派生前的類名稱;如果有必要,增加新的實(shí)現(xiàn)接口。
          • Fields:該項(xiàng)對(duì)類或接口中聲明的字段進(jìn)行了細(xì)致的描述。需要注意的是,fields 列表中僅列出了本類或接口中的字段,并不包括從超類和父接口繼承而來(lái)的字段。
          • Methods:該項(xiàng)對(duì)類或接口中聲明的方法進(jìn)行了細(xì)致的描述。例如方法的名稱、參數(shù)和返回值類型等。需要注意的是,methods 列表里僅存放了本類或本接口中的方法,并不包括從超類和父接口繼承而來(lái)的方法。使用 ASM 進(jìn)行 AOP 編程,通常是通過(guò)調(diào)整 Method 中的指令來(lái)實(shí)現(xiàn)的。
          • Class attributes:該項(xiàng)存放了在該文件中類或接口所定義的屬性的基本信息。

          事實(shí)上,使用 ASM 動(dòng)態(tài)生成類,不需要像早年的 class hacker 一樣,熟知 class 文件的每一段,以及它們的功能、長(zhǎng)度、偏移量以及編碼方式。ASM 會(huì)給我們照顧好這一切的,我們只要告訴 ASM 要改動(dòng)什么就可以了 —— 當(dāng)然,我們首先得知道要改什么:對(duì)類文件格式了解的越多,我們就能更好地使用 ASM 這個(gè)利器。





          回頁(yè)首


          ASM 3.0 編程框架

          ASM 通過(guò)樹(shù)這種數(shù)據(jù)結(jié)構(gòu)來(lái)表示復(fù)雜的字節(jié)碼結(jié)構(gòu),并利用 Push 模型來(lái)對(duì)樹(shù)進(jìn)行遍歷,在遍歷過(guò)程中對(duì)字節(jié)碼進(jìn)行修改。所謂的 Push 模型類似于簡(jiǎn)單的 Visitor 設(shè)計(jì)模式,因?yàn)樾枰幚碜止?jié)碼結(jié)構(gòu)是固定的,所以不需要專門抽象出一種 Vistable 接口,而只需要提供 Visitor 接口。所謂 Visitor 模式和 Iterator 模式有點(diǎn)類似,它們都被用來(lái)遍歷一些復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。Visitor 相當(dāng)于用戶派出的代表,深入到算法內(nèi)部,由算法安排訪問(wèn)行程。Visitor 代表可以更換,但對(duì)算法流程無(wú)法干涉,因此是被動(dòng)的,這也是它和 Iterator 模式由用戶主動(dòng)調(diào)遣算法方式的最大的區(qū)別。

          在 ASM 中,提供了一個(gè) ClassReader 類,這個(gè)類可以直接由字節(jié)數(shù)組或由 class 文件間接的獲得字節(jié)碼數(shù)據(jù),它能正確的分析字節(jié)碼,構(gòu)建出抽象的樹(shù)在內(nèi)存中表示字節(jié)碼。它會(huì)調(diào)用 accept 方法,這個(gè)方法接受一個(gè)實(shí)現(xiàn)了 ClassVisitor 接口的對(duì)象實(shí)例作為參數(shù),然后依次調(diào)用 ClassVisitor 接口的各個(gè)方法。字節(jié)碼空間上的偏移被轉(zhuǎn)換成 visit 事件時(shí)間上調(diào)用的先后,所謂 visit 事件是指對(duì)各種不同 visit 函數(shù)的調(diào)用,ClassReader 知道如何調(diào)用各種 visit 函數(shù)。在這個(gè)過(guò)程中用戶無(wú)法對(duì)操作進(jìn)行干涉,所以遍歷的算法是確定的,用戶可以做的是提供不同的 Visitor 來(lái)對(duì)字節(jié)碼樹(shù)進(jìn)行不同的修改。ClassVisitor 會(huì)產(chǎn)生一些子過(guò)程,比如 visitMethod 會(huì)返回一個(gè)實(shí)現(xiàn) MethordVisitor 接口的實(shí)例,visitField 會(huì)返回一個(gè)實(shí)現(xiàn) FieldVisitor 接口的實(shí)例,完成子過(guò)程后控制返回到父過(guò)程,繼續(xù)訪問(wèn)下一節(jié)點(diǎn)。因此對(duì)于 ClassReader 來(lái)說(shuō),其內(nèi)部順序訪問(wèn)是有一定要求的。實(shí)際上用戶還可以不通過(guò) ClassReader 類,自行手工控制這個(gè)流程,只要按照一定的順序,各個(gè) visit 事件被先后正確的調(diào)用,最后就能生成可以被正確加載的字節(jié)碼。當(dāng)然獲得更大靈活性的同時(shí)也加大了調(diào)整字節(jié)碼的復(fù)雜度。

          各個(gè) ClassVisitor 通過(guò)職責(zé)鏈 (Chain-of-responsibility) 模式,可以非常簡(jiǎn)單的封裝對(duì)字節(jié)碼的各種修改,而無(wú)須關(guān)注字節(jié)碼的字節(jié)偏移,因?yàn)檫@些實(shí)現(xiàn)細(xì)節(jié)對(duì)于用戶都被隱藏了,用戶要做的只是覆寫相應(yīng)的 visit 函數(shù)。

          ClassAdaptor 類實(shí)現(xiàn)了 ClassVisitor 接口所定義的所有函數(shù),當(dāng)新建一個(gè) ClassAdaptor 對(duì)象的時(shí)候,需要傳入一個(gè)實(shí)現(xiàn)了 ClassVisitor 接口的對(duì)象,作為職責(zé)鏈中的下一個(gè)訪問(wèn)者 (Visitor),這些函數(shù)的默認(rèn)實(shí)現(xiàn)就是簡(jiǎn)單的把調(diào)用委派給這個(gè)對(duì)象,然后依次傳遞下去形成職責(zé)鏈。當(dāng)用戶需要對(duì)字節(jié)碼進(jìn)行調(diào)整時(shí),只需從 ClassAdaptor 類派生出一個(gè)子類,覆寫需要修改的方法,完成相應(yīng)功能后再把調(diào)用傳遞下去。這樣,用戶無(wú)需考慮字節(jié)偏移,就可以很方便的控制字節(jié)碼。

          每個(gè) ClassAdaptor 類的派生類可以僅封裝單一功能,比如刪除某函數(shù)、修改字段可見(jiàn)性等等,然后再加入到職責(zé)鏈中,這樣耦合更小,重用的概率也更大,但代價(jià)是產(chǎn)生很多小對(duì)象,而且職責(zé)鏈的層次太長(zhǎng)的話也會(huì)加大系統(tǒng)調(diào)用的開(kāi)銷,用戶需要在低耦合和高效率之間作出權(quán)衡。用戶可以通過(guò)控制職責(zé)鏈中 visit 事件的過(guò)程,對(duì)類文件進(jìn)行如下操作:

          1. 刪除類的字段、方法、指令:只需在職責(zé)鏈傳遞過(guò)程中中斷委派,不訪問(wèn)相應(yīng)的 visit 方法即可,比如刪除方法時(shí)只需直接返回 null,而不是返回由 visitMethod 方法返回的 MethodVisitor 對(duì)象。

            class DelLoginClassAdapter extends ClassAdapter {   public DelLoginClassAdapter(ClassVisitor cv) {    super(cv);   }     public MethodVisitor visitMethod(final int access, final String name,    final String desc, final String signature, final String[] exceptions) {    if (name.equals("login")) {     return null;    }    return cv.visitMethod(access, name, desc, signature, exceptions);   }  }           

          2. 修改類、字段、方法的名字或修飾符:在職責(zé)鏈傳遞過(guò)程中替換調(diào)用參數(shù)。

            class AccessClassAdapter extends ClassAdapter {   public AccessClassAdapter(ClassVisitor cv) {    super(cv);   }     public FieldVisitor visitField(final int access, final String name,          final String desc, final String signature, final Object value) {          int privateAccess = Opcodes.ACC_PRIVATE;          return cv.visitField(privateAccess, name, desc, signature, value);      }  }           

          3. 增加新的類、方法、字段

          ASM 的最終的目的是生成可以被正常裝載的 class 文件,因此其框架結(jié)構(gòu)為客戶提供了一個(gè)生成字節(jié)碼的工具類 —— ClassWriter。它實(shí)現(xiàn)了 ClassVisitor 接口,而且含有一個(gè) toByteArray() 函數(shù),返回生成的字節(jié)碼的字節(jié)流,將字節(jié)流寫回文件即可生產(chǎn)調(diào)整后的 class 文件。一般它都作為職責(zé)鏈的終點(diǎn),把所有 visit 事件的先后調(diào)用(時(shí)間上的先后),最終轉(zhuǎn)換成字節(jié)碼的位置的調(diào)整(空間上的前后),如下例:

          ClassWriter  classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);  ClassAdaptor delLoginClassAdaptor = new DelLoginClassAdapter(classWriter);  ClassAdaptor accessClassAdaptor = new AccessClassAdaptor(delLoginClassAdaptor);     ClassReader classReader = new ClassReader(strFileName);  classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);           

          綜上所述,ASM 的時(shí)序圖如下:


          圖 4. ASM – 時(shí)序圖
          圖 4. ASM – 時(shí)序圖




          回頁(yè)首


          使用 ASM3.0 進(jìn)行 AOP 編程

          我們還是用上面的例子,給 Account 類加上 security check 的功能。與 proxy 編程不同,ASM 不需要將 Account 聲明成接口,Account 可以仍舊是一個(gè)實(shí)現(xiàn)類。ASM 將直接在 Account 類上動(dòng)手術(shù),給 Account 類的 operation 方法首部加上對(duì) SecurityChecker.checkSecurity 的調(diào)用。

          首先,我們將從 ClassAdapter 繼承一個(gè)類。ClassAdapter 是 ASM 框架提供的一個(gè)默認(rèn)類,負(fù)責(zé)溝通 ClassReaderClassWriter。如果想要改變 ClassReader 處讀入的類,然后從 ClassWriter 處輸出,可以重寫相應(yīng)的 ClassAdapter 函數(shù)。這里,為了改變 Account 類的 operation 方法,我們將重寫 visitMethdod 方法。

          class AddSecurityCheckClassAdapter extends ClassAdapter{     public AddSecurityCheckClassAdapter(ClassVisitor cv) {    //Responsechain 的下一個(gè) ClassVisitor,這里我們將傳入 ClassWriter,    //負(fù)責(zé)改寫后代碼的輸出    super(cv);   }      //重寫 visitMethod,訪問(wèn)到 "operation" 方法時(shí),   //給出自定義 MethodVisitor,實(shí)際改寫方法內(nèi)容   public MethodVisitor visitMethod(final int access, final String name,    final String desc, final String signature, final String[] exceptions) {    MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);    MethodVisitor wrappedMv = mv;    if (mv != null) {     //對(duì)于 "operation" 方法     if (name.equals("operation")) {       //使用自定義 MethodVisitor,實(shí)際改寫方法內(nèi)容      wrappedMv = new AddSecurityCheckMethodAdapter(mv);      }     }    return wrappedMv;   }  }     

          下一步就是定義一個(gè)繼承自 MethodAdapterAddSecurityCheckMethodAdapter,在“operation”方法首部插入對(duì) SecurityChecker.checkSecurity() 的調(diào)用。

          class AddSecurityCheckMethodAdapter extends MethodAdapter {   public AddSecurityCheckMethodAdapter(MethodVisitor mv) {    super(mv);   }     public void visitCode() {    visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker",     "checkSecurity", "()V");   }  }      

          其中,ClassReader 讀到每個(gè)方法的首部時(shí)調(diào)用 visitCode(),在這個(gè)重寫方法里,我們用visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker","checkSecurity", "()V"); 插入了安全檢查功能。

          最后,我們將集成上面定義的 ClassAdapterClassReaderClassWriter 產(chǎn)生修改后的 Account 類文件:

          import java.io.File;  import java.io.FileOutputStream;  import org.objectweb.asm.*;        public class Generator{   public static void main() throws Exception {    ClassReader cr = new ClassReader("Account");    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);    ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);    cr.accept(classAdapter, ClassReader.SKIP_DEBUG);    byte[] data = cw.toByteArray();    File file = new File("Account.class");    FileOutputStream fout = new FileOutputStream(file);    fout.write(data);    fout.close();   }  }   

          執(zhí)行完這段程序后,我們會(huì)得到一個(gè)新的 Account.class 文件,如果我們使用下面代碼:

          public class Main {   public static void main(String[] args) {    Account account = new Account();    account.operation();   }  }   

          使用這個(gè) Account,我們會(huì)得到下面的輸出:

          SecurityChecker.checkSecurity ...  operation...   

          也就是說(shuō),在 Account 原來(lái)的 operation 內(nèi)容執(zhí)行之前,進(jìn)行了 SecurityChecker.checkSecurity() 檢查。

          將動(dòng)態(tài)生成類改造成原始類 Account 的子類

          上面給出的例子是直接改造 Account 類本身的,從此 Account 類的 operation 方法必須進(jìn)行 checkSecurity 檢查。但事實(shí)上,我們有時(shí)仍希望保留原來(lái)的 Account 類,因此把生成類定義為原始類的子類是更符合 AOP 原則的做法。下面介紹如何將改造后的類定義為 Account 的子類 Account$EnhancedByASM。其中主要有兩項(xiàng)工作:

          • 改變 Class Description, 將其命名為 Account$EnhancedByASM,將其父類指定為 Account
          • 改變構(gòu)造函數(shù),將其中對(duì)父類構(gòu)造函數(shù)的調(diào)用轉(zhuǎn)換為對(duì) Account 構(gòu)造函數(shù)的調(diào)用。

          AddSecurityCheckClassAdapter 類中,將重寫 visit 方法:

          public void visit(final int version, final int access, final String name,    final String signature, final String superName,    final String[] interfaces) {   String enhancedName = name + "$EnhancedByASM";  //改變類命名   enhancedSuperName = name; //改變父類,這里是”Account”   super.visit(version, access, enhancedName, signature,   enhancedSuperName, interfaces);  }   

          改進(jìn) visitMethod 方法,增加對(duì)構(gòu)造函數(shù)的處理:

          public MethodVisitor visitMethod(final int access, final String name,   final String desc, final String signature, final String[] exceptions) {   MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);   MethodVisitor wrappedMv = mv;   if (mv != null) {    if (name.equals("operation")) {     wrappedMv = new AddSecurityCheckMethodAdapter(mv);    } else if (name.equals("<init>")) {     wrappedMv = new ChangeToChildConstructorMethodAdapter(mv,      enhancedSuperName);    }   }   return wrappedMv;  }   

          這里 ChangeToChildConstructorMethodAdapter 將負(fù)責(zé)把 Account 的構(gòu)造函數(shù)改造成其子類 Account$EnhancedByASM 的構(gòu)造函數(shù):

          class ChangeToChildConstructorMethodAdapter extends MethodAdapter {   private String superClassName;     public ChangeToChildConstructorMethodAdapter(MethodVisitor mv,    String superClassName) {    super(mv);    this.superClassName = superClassName;   }     public void visitMethodInsn(int opcode, String owner, String name,    String desc) {    //調(diào)用父類的構(gòu)造函數(shù)時(shí)    if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) {      owner = superClassName;    }    super.visitMethodInsn(opcode, owner, name, desc);//改寫父類為superClassName   }  }  

          最后演示一下如何在運(yùn)行時(shí)產(chǎn)生并裝入產(chǎn)生的 Account$EnhancedByASM。 我們定義一個(gè) Util 類,作為一個(gè)類工廠負(fù)責(zé)產(chǎn)生有安全檢查的 Account 類:

          public class SecureAccountGenerator {     private static AccountGeneratorClassLoader classLoader =     new AccountGeneratorClassLoade();   private static Class secureAccountClass;     public Account generateSecureAccount() throws ClassFormatError,     InstantiationException, IllegalAccessException {    if (null == secureAccountClass) {                 ClassReader cr = new ClassReader("Account");     ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);     ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);     cr.accept(classAdapter, ClassReader.SKIP_DEBUG);     byte[] data = cw.toByteArray();     secureAccountClass = classLoader.defineClassFromClassFile(      "Account$EnhancedByASM",data);    }    return (Account) secureAccountClass.newInstance();   }     private static class AccountGeneratorClassLoader extends ClassLoader {    public Class defineClassFromClassFile(String className,     byte[] classFile) throws ClassFormatError {     return defineClass("Account$EnhancedByASM", classFile, 0, classFile.length());    }   }  }  

          靜態(tài)方法 SecureAccountGenerator.generateSecureAccount() 在運(yùn)行時(shí)動(dòng)態(tài)生成一個(gè)加上了安全檢查的 Account 子類。著名的 Hibernate 和 Spring 框架,就是使用這種技術(shù)實(shí)現(xiàn)了 AOP 的“無(wú)損注入”。





          回頁(yè)首


          小結(jié)

          最后,我們比較一下 ASM 和其他實(shí)現(xiàn) AOP 的底層技術(shù):

          posted @ 2009-05-12 12:43 lanxin1020 閱讀(261) | 評(píng)論 (0)編輯 收藏

          以下是一份完整的struts-config.xml文件,配置元素的說(shuō)明詳見(jiàn)注釋.

          <?xml version="1.0" encoding="UTF-8"?>
          <!DOCTYPE struts-config PUBLIC
          "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN"
          "http://jakarta.apache.org/struts/dtds/struts-config.dtd">
          <!-- struts-config.xml中的元素必須按照上述doc指令中的dtd文檔定義順序書寫,本例即遵從了dtd定義順序 -->
          <!-- struts-config是整個(gè)xml的根元素,其他元素必須被包含其內(nèi) -->
          <struts-config>
          <!--
             名稱:data-sources
             描述:data-sources元素定義了web App所需要使用的數(shù)據(jù)源
             數(shù)量:最多一個(gè)
             子元素:data-source
          -->
          <data-sources>
             <!--
              名稱:data-source
              描述:data-source元素定義了具體的數(shù)據(jù)源
              數(shù)量:任意多個(gè)
              屬性:
               @key:當(dāng)需要配置多個(gè)數(shù)據(jù)源時(shí),相當(dāng)于數(shù)據(jù)源的名稱,用來(lái)數(shù)據(jù)源彼此間進(jìn)行區(qū)別
               @type:可以使用的數(shù)據(jù)源實(shí)現(xiàn)的類,一般來(lái)自如下四個(gè)庫(kù)
                Poolman,開(kāi)放源代碼軟件
                Expresso,Jcorporate
                JDBC Pool,開(kāi)放源代碼軟件
                DBCP,Jakarta
             -->
             <data-source key="firstOne" type="org.apache.commons.dbcp.BasicDataSource">
              <!--
               名稱:set-property
               描述:用來(lái)設(shè)定數(shù)據(jù)源的屬性
               屬性:
                @autoCommit:是否自動(dòng)提交 可選值:true/false
                @description:數(shù)據(jù)源描述
                @driverClass:數(shù)據(jù)源使用的類
                @maxCount:最大數(shù)據(jù)源連接數(shù)
                @minCount:最小數(shù)據(jù)源連接數(shù)
                @user:數(shù)據(jù)庫(kù)用戶
                @password:數(shù)據(jù)庫(kù)密碼
                @url:數(shù)據(jù)庫(kù)url
              -->
              <set-property property="autoCommit" value="true"/>
              <set-property property="description" value="Hello!"/>
              <set-property property="driverClass" value="com.mysql.jdbc.Driver"/>
              <set-property property="maxCount" value="10"/>
              <set-property property="minCount" value="2"/>
              <set-property property="user" value="root"/>
              <set-property property="password" value=""/>
              <set-property property="url" value="jdbc:mysql://localhost:3306/helloAdmin"/>
             </data-source>
          </data-sources>

          <!--
             名稱:form-beans
             描述:用來(lái)配置多個(gè)ActionForm Bean
             數(shù)量:最多一個(gè)
             子元素:form-bean
          -->
          <form-beans>
             <!--
              名稱:form-bean
              描述:用來(lái)配置ActionForm Bean
              數(shù)量:任意多個(gè)
              子元素:form-property
              屬性:
               @className:指定與form-bean元素相對(duì)應(yīng)的配置類,一般默認(rèn)使用org.apaceh.struts.config.FormBeanConfig,如果自定義,則必須繼承 FormBeanConfig
               @name:必備屬性!為當(dāng)前form-bean制定一個(gè)全局唯一的標(biāo)識(shí)符,使得在整個(gè)Struts框架內(nèi),可以通過(guò)該標(biāo)識(shí)符來(lái)引用這個(gè)ActionForm Bean。
               @type:必備屬性!指明實(shí)現(xiàn)當(dāng)前ActionForm Bean的完整類名。
             -->
             <form-bean name="Hello" type="myPack.Hello">
              <!--
               名稱:form-property
               描述:用來(lái)設(shè)定ActionForm Bean的屬性
               數(shù)量:根據(jù)實(shí)際需求而定,例如,ActionForm Bean對(duì)應(yīng)的一個(gè)登陸Form中有兩個(gè)文本框,name和password,ActionForm Bean中也有這兩個(gè)字段,則此處編寫兩個(gè)form-property來(lái)設(shè)定屬性
               屬性:
                @className:指定與form-property相對(duì)應(yīng)的配置類,默認(rèn)是org.apache.struts.config.FormPropertyConfig,如果自定義,則必須繼承FormPropertyConfig類
                @name:所要設(shè)定的ActionForm Bean的屬性名稱
                @type:所要設(shè)定的ActionForm Bean的屬性值的類
                @initial:當(dāng)前屬性的初值
              -->
              <form-property name="name" type="java.lang.String"/>
              <form-property name="number" type="java.lang.Iteger" initial="18"/>
             </form-bean>
          </form-beans>

          <!--
             名稱:global-exceptions
             描述:處理異常
             數(shù)量:最多一個(gè)
             子元素:exception
          -->
          <global-exceptions>
             <!--
              名稱:exception
              描述:具體定義一個(gè)異常及其處理
              數(shù)量:任意多個(gè)
              屬性:
               @className:指定對(duì)應(yīng)exception的配置類,默認(rèn)為org.apache.struts.config.ExceptionConfig
               @handler:指定異常處理類,默認(rèn)為org.apache.struts.action.ExceptionHandler
               @key:指定在Resource Bundle種描述該異常的消息key
               @path:指定當(dāng)發(fā)生異常時(shí),進(jìn)行轉(zhuǎn)發(fā)的路徑
               @scope:指定ActionMessage實(shí)例存放的范圍,默認(rèn)為request,另外一個(gè)可選值是session
               @type:必須要有!指定所需要處理異常類的名字。
               @bundle:指定資源綁定
             -->
             <exception
              key=""hello.error
              path="/error.jsp"
              scope="session"
              type="hello.HandleError"/>
          </global-exceptions>

          <!--
             名稱:global-forwards
             描述:定義全局轉(zhuǎn)發(fā)
             數(shù)量:最多一個(gè)
             子元素:forward
          -->
          <global-forwards>
             <!--
              名稱:forward
              描述:定義一個(gè)具體的轉(zhuǎn)發(fā)
              數(shù)量:任意多個(gè)
              屬性:
               @className:指定和forward元素對(duì)應(yīng)的配置類,默認(rèn)為org.apache.struts.action.ActionForward
               @contextRelative:如果為true,則指明使用當(dāng)前上下文,路徑以“/”開(kāi)頭,默認(rèn)為false
               @name:必須配有!指明轉(zhuǎn)發(fā)路徑的唯一標(biāo)識(shí)符
               @path:必須配有!指明轉(zhuǎn)發(fā)或者重定向的URI。必須以"/"開(kāi)頭。具體配置要與contextRelative相應(yīng)。
               @redirect:為true時(shí),執(zhí)行重定向操作,否則執(zhí)行請(qǐng)求轉(zhuǎn)發(fā)。默認(rèn)為false
             -->
             <forward name="A" path="/a.jsp"/>
             <forward name="B" path="/hello/b.do"/>
          </global-forwards>

          <!--
             名稱:action-mappings
             描述:定義action集合
             數(shù)量:最多一個(gè)
             子元素:action
          -->
          <action-mappings>
             <!--
              名稱:action
              描述:定義了從特定的請(qǐng)求路徑到相應(yīng)的Action類的映射
              數(shù)量:任意多個(gè)
              子元素:exception,forward(二者均為局部量)
              屬性:
               @attribute:制定與當(dāng)前Action相關(guān)聯(lián)的ActionForm Bean在request和session范圍內(nèi)的名稱(key)
               @className:與Action元素對(duì)應(yīng)的配置類。默認(rèn)為org.apache.struts.action.ActionMapping
               @forward:指名轉(zhuǎn)發(fā)的URL路徑
               @include:指名包含的URL路徑
               @input:指名包含輸入表單的URL路徑,表單驗(yàn)證失敗時(shí),請(qǐng)求會(huì)被轉(zhuǎn)發(fā)到該URL中
               @name:指定和當(dāng)前Acion關(guān)聯(lián)的ActionForm Bean的名字。該名稱必須在form-bean元素中定義過(guò)。
               @path:指定訪問(wèn)Action的路徑,以"/"開(kāi)頭,沒(méi)有擴(kuò)展名
               @parameter:為當(dāng)前的Action配置參數(shù),可以在Action的execute()方法中,通過(guò)調(diào)用ActionMapping的getParameter()方法來(lái)獲取參數(shù)
               @roles:指定允許調(diào)用該Aciton的安全角色。多個(gè)角色之間用逗號(hào)分割。處理請(qǐng)求時(shí),RequestProcessor會(huì)根據(jù)該配置項(xiàng)來(lái)決定用戶是否有調(diào)用該Action的權(quán)限
               @scope:指定ActionForm Bean的存在范圍,可選值為request和session。默認(rèn)為session
               @type:指定Action類的完整類名
               @unknown:值為true時(shí),表示可以處理用戶發(fā)出的所有無(wú)效的Action URL。默認(rèn)為false
               @validate:指定是否要先調(diào)用ActionForm Bean的validate()方法。默認(rèn)為true
              注意:如上屬性中,forward/include/type三者相斥,即三者在同一Action配置中只能存在一個(gè)。
             -->
             <action path="/search"
              type="addressbook.actions.SearchAction"
              name="searchForm"
              scope="request"
              validate="true"
              input="/search.jsp">
              <forward name="success" path="/display.jsp"/>
             </action>  
          </action-mappings>

          <!--
             名稱:controller
             描述:用于配置ActionServlet
             數(shù)量:最多一個(gè)
             屬性:
              @bufferSize:指定上傳文件的輸入緩沖的大小.默認(rèn)為4096
              @className:指定當(dāng)前控制器的配置類.默認(rèn)為org.apache.struts.config.ControllerConfig
              @contentType:指定相應(yīng)結(jié)果的內(nèi)容類型和字符編碼
              @locale:指定是否把Locale對(duì)象保存到當(dāng)前用戶的session中,默認(rèn)為false
              @processorClass:指定負(fù)責(zé)處理請(qǐng)求的Java類的完整類名.默認(rèn)org.apache.struts.action.RequestProcessor
              @tempDir:指定文件上傳時(shí)的臨時(shí)工作目錄.如果沒(méi)有設(shè)置,將才用Servlet容器為web應(yīng)用分配的臨時(shí)工作目錄.
              @nochache:true時(shí),在相應(yīng)結(jié)果中加入特定的頭參數(shù):Pragma ,Cache-Control,Expires防止頁(yè)面被存儲(chǔ)在可數(shù)瀏覽器的緩存中,默認(rèn)為false
          -->
          <controller
             contentType="text/html;charset=UTF-8"
             locale="true"
             processorClass="CustomRequestProcessor">
          </controller>
          <!--
             名稱:message-resources
             描述:配置Resource Bundle.
             數(shù)量:任意多個(gè)
             屬性:
              @className:指定和message-resources對(duì)應(yīng)的配置類.默認(rèn)為org.apache.struts.config.MessageResourcesConfig
              @factory:指定資源的工廠類,默認(rèn)為org.apache.struts.util.PropertyMessageResourcesFactory
              @key:
              @null:
              @parameter:
          -->
          <message-resources
             null="false"
             parameter="defaultResource"/>
          <message-resources
             key="images"
             null="false"
             parameter="ImageResources"/>
            
          <!--
             名稱:plug-in
             描述:用于配置Struts的插件
             數(shù)量:任意多個(gè)
             子元素:set-property
             屬性:
              @className:指定Struts插件類.此類必須實(shí)現(xiàn)org.apache.struts.action.PlugIn接口
          -->
          <plug-in
             className="org.apache.struts.validator.ValidatorPlugIn">
             <!--
              名稱:set-property
              描述:配置插件的屬性
              數(shù)量:任意多個(gè)
              屬性:
               @property:插件的屬性名稱
               @value:該名稱所配置的值
             -->
             <set-property
              property="pathnames"
              value="/WEB-INF/validator-rules.xml,/WEB-INF/vlaidation.xml"/>
          </plug-in>

          </struts-config>

          posted @ 2009-05-11 16:26 lanxin1020 閱讀(285) | 評(píng)論 (0)編輯 收藏
          主站蜘蛛池模板: 崇左市| 陆川县| 通化县| 当涂县| 衡南县| 老河口市| 镇赉县| 环江| 枣庄市| 开阳县| 贵阳市| 乌拉特前旗| 云霄县| 纳雍县| 金昌市| 基隆市| 友谊县| 兴安县| 镇雄县| 徐闻县| 武冈市| 阳山县| 灵山县| 商水县| 邯郸县| 达日县| 华池县| 登封市| 饶阳县| 若尔盖县| 太和县| 内丘县| 雷山县| 阳泉市| 青川县| 高台县| 安吉县| 丹寨县| 临夏县| 合水县| 霸州市|