ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態改變類行為。Java class 被存儲在嚴格格式定義的 .class 文件里,這些類文件擁有足夠的元數據來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節碼(指令)。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據用戶要求生成新類。
與 BCEL 和 SERL 不同,ASM 提供了更為現代的編程模型。對于 ASM 來說,Java class 被描述為一棵樹;使用 “Visitor” 模式遍歷整個二進制結構;事件驅動的處理方式使得用戶只需要關注于對其編程有意義的部分,而不必了解 Java 類文件格式的所有細節:ASM 框架提供了默認的 “response taker”處理這一切。
動態生成 Java 類與 AOP 密切相關的。AOP 的初衷在于軟件設計世界中存在這么一類代碼,零散而又耦合:零散是由于一些公有的功能(諸如著名的 log 例子)分散在所有模塊之中;同時改變 log 功能又會影響到所有的模塊。出現這樣的缺陷,很大程度上是由于傳統的 面向對象編程注重以繼承關系為代表的“縱向”關系,而對于擁有相同功能或者說方面 (Aspect)的模塊之間的“橫向”關系不能很好地表達。例如,目前有一個既有的銀行管理系統,包括 Bank、Customer、Account、Invoice 等對象,現在要加入一個安全檢查模塊, 對已有類的所有操作之前都必須進行一次安全檢查。
然而 Bank、Customer、Account、Invoice 是代表不同的事務,派生自不同的父類,很難在高層上加入關于 Security Checker 的共有功能。對于沒有多繼承的 Java 來說,更是如此。傳統的解決方案是使用 Decorator 模式,它可以在一定程度上改善耦合,而功能仍舊是分散的 —— 每個需要 Security Checker 的類都必須要派生一個 Decorator,每個需要 Security Checker 的方法都要被包裝(wrap)。下面我們以 Account
類為例看一下 Decorator:
首先,我們有一個 SecurityChecker
類,其靜態方法 checkSecurity
執行安全檢查功能:
public class SecurityChecker { public static void checkSecurity() { System.out.println("SecurityChecker.checkSecurity ..."); //TODO real security check } } |
另一個是 Account
類:
public class Account { public void operation() { System.out.println("operation..."); //TODO real operation } } |
若想對 operation
加入對 SecurityCheck.checkSecurity()
調用,標準的 Decorator 需要先定義一個 Account
類的接口:
public interface Account { void operation(); } |
然后把原來的 Account
類定義為一個實現類:
public class AccountImpl extends Account{ public void operation() { System.out.println("operation..."); //TODO real operation } } |
定義一個 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(); } } |
在這個簡單的例子里,改造一個類的一個方法還好,如果是變動整個模塊,Decorator 很快就會演化成另一個噩夢。動態改變 Java 類就是要解決 AOP 的問題,提供一種得到系統支持的可編程的方法,自動化地生成或者增強 Java 代碼。這種技術已經廣泛應用于最新的 Java 框架內,如 Hibernate,Spring 等。
最直接的改造 Java 類的方法莫過于直接改寫 class 文件。Java 規范詳細說明了class 文件的格式,直接編輯字節碼確實可以改變 Java 類的行為。直到今天,還有一些 Java 高手們使用最原始的工具,如 UltraEdit 這樣的編輯器對 class 文件動手術。是的,這是最直接的方法,但是要求使用者對 Java class 文件的格式了熟于心:小心地推算出想改造的函數相對文件首部的偏移量,同時重新計算 class 文件的校驗碼以通過 Java 虛擬機的安全機制。
Java 5 中提供的 Instrument 包也可以提供類似的功能:啟動時往 Java 虛擬機中掛上一個用戶定義的 hook 程序,可以在裝入特定類的時候改變特定類的字節碼,從而改變該類的行為。但是其缺點也是明顯的:
ClassFileTransformer. transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
,還是 Instrument.redefineClasses(ClassDefinition[] definitions)
,都必須提供新 Java 類的字節碼。也就是說,同直接改寫 class 文件一樣,使用 Instrument 也必須了解想改造的方法相對類首部的偏移量,才能在適當的位置上插入新的代碼。 盡管 Instrument 可以改造類,但事實上,Instrument 更適用于監控和控制虛擬機的行為。
一種比較理想且流行的方法是使用 java.lang.ref.proxy
。我們仍舊使用上面的例子,給 Account
類加上 checkSecurity 功能:
首先,Proxy 編程是面向接口的。下面我們會看到,Proxy 并不負責實例化對象,和 Decorator 模式一樣,要把 Account
定義成一個接口,然后在 AccountImpl
里實現 Account
接口,接著實現一個 InvocationHandler
Account
方法被調用的時候,虛擬機都會實際調用這個 InvocationHandler
的 invoke
方法:
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); } } |
最后,在應用程序中指定 InvocationHandler
生成代理對象:
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.newProxyInstance
生成的是實現 Account
接口的對象而不是 AccountImpl
的子類。這對于軟件架構設計,尤其對于既有軟件系統是有一定掣肘的。
ASM 能夠通過改造既有類,直接生成需要的代碼。增強的代碼是硬編碼在新生成的類文件內部的,沒有反射帶來性能上的付出。同時,ASM 與 Proxy 編程不同,不需要為增強代碼而新定義一個接口,生成的代碼可以覆蓋原來的類,或者是原始類的子類。它是一個普通的 Java 類而不是 proxy 類,甚至可以在應用程序的類框架中擁有自己的位置,派生自己的子類。
相比于其他流行的 Java 字節碼操縱工具,ASM 更小更快。ASM 具有類似于 BCEL 或者 SERP 的功能,而只有 33k 大小,而后者分別有 350k 和 150k。同時,同樣類轉換的負載,如果 ASM 是 60% 的話,BCEL 需要 700%,而 SERP 需要 1100% 或者更多。
ASM 已經被廣泛應用于一系列 Java 項目: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 也通過 cglib,另一個更高層一些的自動代碼生成工具使用了 ASM。
![]() ![]() |
![]()
|
所謂 Java 類文件,就是通常用 javac 編譯器產生的 .class 文件。這些文件具有嚴格定義的格式。為了更好的理解 ASM,首先對 Java 類文件格式作一點簡單的介紹。Java 源文件經過 javac 編譯器編譯之后,將會生成對應的二進制文件(如下圖所示)。每個合法的 Java 類文件都具備精確的定義,而正是這種精確的定義,才使得 Java 虛擬機得以正確讀取和解釋所有的 Java 類文件。
Java 類文件是 8 位字節的二進制流。數據項按順序存儲在 class 文件中,相鄰的項之間沒有間隔,這使得 class 文件變得緊湊,減少存儲空間。在 Java 類文件中包含了許多大小不同的項,由于每一項的結構都有嚴格規定,這使得 class 文件能夠從頭到尾被順利地解析。下面讓我們來看一下 Java 類文件的內部結構,以便對此有個大致的認識。
例如,一個最簡單的 Hello World 程序:
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello world"); } } |
經過 javac 編譯后,得到的類文件大致是:
從上圖中可以看到,一個 Java 類文件大致可以歸為 10 個項:
事實上,使用 ASM 動態生成類,不需要像早年的 class hacker 一樣,熟知 class 文件的每一段,以及它們的功能、長度、偏移量以及編碼方式。ASM 會給我們照顧好這一切的,我們只要告訴 ASM 要改動什么就可以了 —— 當然,我們首先得知道要改什么:對類文件格式了解的越多,我們就能更好地使用 ASM 這個利器。
![]() ![]() |
![]()
|
ASM 通過樹這種數據結構來表示復雜的字節碼結構,并利用 Push 模型來對樹進行遍歷,在遍歷過程中對字節碼進行修改。所謂的 Push 模型類似于簡單的 Visitor 設計模式,因為需要處理字節碼結構是固定的,所以不需要專門抽象出一種 Vistable 接口,而只需要提供 Visitor 接口。所謂 Visitor 模式和 Iterator 模式有點類似,它們都被用來遍歷一些復雜的數據結構。Visitor 相當于用戶派出的代表,深入到算法內部,由算法安排訪問行程。Visitor 代表可以更換,但對算法流程無法干涉,因此是被動的,這也是它和 Iterator 模式由用戶主動調遣算法方式的最大的區別。
在 ASM 中,提供了一個 ClassReader
類,這個類可以直接由字節數組或由 class 文件間接的獲得字節碼數據,它能正確的分析字節碼,構建出抽象的樹在內存中表示字節碼。它會調用 accept
方法,這個方法接受一個實現了 ClassVisitor
接口的對象實例作為參數,然后依次調用 ClassVisitor
接口的各個方法。字節碼空間上的偏移被轉換成 visit 事件時間上調用的先后,所謂 visit 事件是指對各種不同 visit 函數的調用,ClassReader
知道如何調用各種 visit 函數。在這個過程中用戶無法對操作進行干涉,所以遍歷的算法是確定的,用戶可以做的是提供不同的 Visitor 來對字節碼樹進行不同的修改。ClassVisitor
會產生一些子過程,比如 visitMethod
會返回一個實現 MethordVisitor
接口的實例,visitField
會返回一個實現 FieldVisitor
接口的實例,完成子過程后控制返回到父過程,繼續訪問下一節點。因此對于 ClassReader
來說,其內部順序訪問是有一定要求的。實際上用戶還可以不通過 ClassReader
類,自行手工控制這個流程,只要按照一定的順序,各個 visit 事件被先后正確的調用,最后就能生成可以被正確加載的字節碼。當然獲得更大靈活性的同時也加大了調整字節碼的復雜度。
各個 ClassVisitor
通過職責鏈 (Chain-of-responsibility) 模式,可以非常簡單的封裝對字節碼的各種修改,而無須關注字節碼的字節偏移,因為這些實現細節對于用戶都被隱藏了,用戶要做的只是覆寫相應的 visit 函數。
ClassAdaptor
類實現了 ClassVisitor
接口所定義的所有函數,當新建一個 ClassAdaptor
對象的時候,需要傳入一個實現了 ClassVisitor
接口的對象,作為職責鏈中的下一個訪問者 (Visitor),這些函數的默認實現就是簡單的把調用委派給這個對象,然后依次傳遞下去形成職責鏈。當用戶需要對字節碼進行調整時,只需從 ClassAdaptor
類派生出一個子類,覆寫需要修改的方法,完成相應功能后再把調用傳遞下去。這樣,用戶無需考慮字節偏移,就可以很方便的控制字節碼。
每個 ClassAdaptor
類的派生類可以僅封裝單一功能,比如刪除某函數、修改字段可見性等等,然后再加入到職責鏈中,這樣耦合更小,重用的概率也更大,但代價是產生很多小對象,而且職責鏈的層次太長的話也會加大系統調用的開銷,用戶需要在低耦合和高效率之間作出權衡。用戶可以通過控制職責鏈中 visit 事件的過程,對類文件進行如下操作:
刪除類的字段、方法、指令:只需在職責鏈傳遞過程中中斷委派,不訪問相應的 visit 方法即可,比如刪除方法時只需直接返回 null
,而不是返回由 visitMethod
方法返回的 MethodVisitor
對象。
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); } } |
修改類、字段、方法的名字或修飾符:在職責鏈傳遞過程中替換調用參數。
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); } } |
增加新的類、方法、字段
ASM 的最終的目的是生成可以被正常裝載的 class 文件,因此其框架結構為客戶提供了一個生成字節碼的工具類 —— ClassWriter
。它實現了 ClassVisitor
接口,而且含有一個 toByteArray()
函數,返回生成的字節碼的字節流,將字節流寫回文件即可生產調整后的 class 文件。一般它都作為職責鏈的終點,把所有 visit 事件的先后調用(時間上的先后),最終轉換成字節碼的位置的調整(空間上的前后),如下例:
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 的時序圖如下:
![]() ![]() |
![]()
|
我們還是用上面的例子,給 Account
類加上 security check 的功能。與 proxy 編程不同,ASM 不需要將 Account
聲明成接口,Account
可以仍舊是一個實現類。ASM 將直接在 Account
類上動手術,給 Account
類的 operation
方法首部加上對 SecurityChecker.checkSecurity
的調用。
首先,我們將從 ClassAdapter
繼承一個類。ClassAdapter
是 ASM 框架提供的一個默認類,負責溝通 ClassReader
和 ClassWriter
。如果想要改變 ClassReader
處讀入的類,然后從 ClassWriter
處輸出,可以重寫相應的 ClassAdapter
函數。這里,為了改變 Account
類的 operation
方法,我們將重寫 visitMethdod
方法。
class AddSecurityCheckClassAdapter extends ClassAdapter{ public AddSecurityCheckClassAdapter(ClassVisitor cv) { //Responsechain 的下一個 ClassVisitor,這里我們將傳入 ClassWriter, //負責改寫后代碼的輸出 super(cv); } //重寫 visitMethod,訪問到 "operation" 方法時, //給出自定義 MethodVisitor,實際改寫方法內容 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) { //對于 "operation" 方法 if (name.equals("operation")) { //使用自定義 MethodVisitor,實際改寫方法內容 wrappedMv = new AddSecurityCheckMethodAdapter(mv); } } return wrappedMv; } } |
下一步就是定義一個繼承自 MethodAdapter
的 AddSecurityCheckMethodAdapter
,在“operation
”方法首部插入對 SecurityChecker.checkSecurity()
的調用。
class AddSecurityCheckMethodAdapter extends MethodAdapter { public AddSecurityCheckMethodAdapter(MethodVisitor mv) { super(mv); } public void visitCode() { visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker", "checkSecurity", "()V"); } } |
其中,ClassReader
讀到每個方法的首部時調用 visitCode()
,在這個重寫方法里,我們用visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker","checkSecurity", "()V");
插入了安全檢查功能。
最后,我們將集成上面定義的 ClassAdapter
,ClassReader
和ClassWriter
產生修改后的 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(); } } |
執行完這段程序后,我們會得到一個新的 Account.class 文件,如果我們使用下面代碼:
public class Main { public static void main(String[] args) { Account account = new Account(); account.operation(); } } |
使用這個 Account,我們會得到下面的輸出:
SecurityChecker.checkSecurity ... operation... |
也就是說,在 Account
原來的 operation
內容執行之前,進行了 SecurityChecker.checkSecurity()
檢查。
上面給出的例子是直接改造 Account
類本身的,從此 Account
類的 operation
方法必須進行 checkSecurity 檢查。但事實上,我們有時仍希望保留原來的 Account
類,因此把生成類定義為原始類的子類是更符合 AOP 原則的做法。下面介紹如何將改造后的類定義為 Account
的子類 Account$EnhancedByASM
。其中主要有兩項工作:
Account$EnhancedByASM
,將其父類指定為 Account
。
Account
構造函數的調用。 在 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); } |
改進 visitMethod
方法,增加對構造函數的處理:
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
將負責把 Account
的構造函數改造成其子類 Account$EnhancedByASM
的構造函數:
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) { //調用父類的構造函數時 if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) { owner = superClassName; } super.visitMethodInsn(opcode, owner, name, desc);//改寫父類為superClassName } } |
最后演示一下如何在運行時產生并裝入產生的 Account$EnhancedByASM
。 我們定義一個 Util
類,作為一個類工廠負責產生有安全檢查的 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()); } } } |
靜態方法 SecureAccountGenerator.generateSecureAccount()
在運行時動態生成一個加上了安全檢查的 Account
子類。著名的 Hibernate 和 Spring 框架,就是使用這種技術實現了 AOP 的“無損注入”。
![]() ![]() |
![]()
|
最后,我們比較一下 ASM 和其他實現 AOP 的底層技術:
以下是一份完整的struts-config.xml文件,配置元素的說明詳見注釋.
<?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是整個xml的根元素,其他元素必須被包含其內 -->
<struts-config>
<!--
名稱:data-sources
描述:data-sources元素定義了web App所需要使用的數據源
數量:最多一個
子元素:data-source
-->
<data-sources>
<!--
名稱:data-source
描述:data-source元素定義了具體的數據源
數量:任意多個
屬性:
@key:當需要配置多個數據源時,相當于數據源的名稱,用來數據源彼此間進行區別
@type:可以使用的數據源實現的類,一般來自如下四個庫
Poolman,開放源代碼軟件
Expresso,Jcorporate
JDBC Pool,開放源代碼軟件
DBCP,Jakarta
-->
<data-source key="firstOne" type="org.apache.commons.dbcp.BasicDataSource">
<!--
名稱:set-property
描述:用來設定數據源的屬性
屬性:
@autoCommit:是否自動提交 可選值:true/false
@description:數據源描述
@driverClass:數據源使用的類
@maxCount:最大數據源連接數
@minCount:最小數據源連接數
@user:數據庫用戶
@password:數據庫密碼
@url:數據庫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
描述:用來配置多個ActionForm Bean
數量:最多一個
子元素:form-bean
-->
<form-beans>
<!--
名稱:form-bean
描述:用來配置ActionForm Bean
數量:任意多個
子元素:form-property
屬性:
@className:指定與form-bean元素相對應的配置類,一般默認使用org.apaceh.struts.config.FormBeanConfig,如果自定義,則必須繼承 FormBeanConfig
@name:必備屬性!為當前form-bean制定一個全局唯一的標識符,使得在整個Struts框架內,可以通過該標識符來引用這個ActionForm Bean。
@type:必備屬性!指明實現當前ActionForm Bean的完整類名。
-->
<form-bean name="Hello" type="myPack.Hello">
<!--
名稱:form-property
描述:用來設定ActionForm Bean的屬性
數量:根據實際需求而定,例如,ActionForm Bean對應的一個登陸Form中有兩個文本框,name和password,ActionForm Bean中也有這兩個字段,則此處編寫兩個form-property來設定屬性
屬性:
@className:指定與form-property相對應的配置類,默認是org.apache.struts.config.FormPropertyConfig,如果自定義,則必須繼承FormPropertyConfig類
@name:所要設定的ActionForm Bean的屬性名稱
@type:所要設定的ActionForm Bean的屬性值的類
@initial:當前屬性的初值
-->
<form-property name="name" type="java.lang.String"/>
<form-property name="number" type="java.lang.Iteger" initial="18"/>
</form-bean>
</form-beans>
<!--
名稱:global-exceptions
描述:處理異常
數量:最多一個
子元素:exception
-->
<global-exceptions>
<!--
名稱:exception
描述:具體定義一個異常及其處理
數量:任意多個
屬性:
@className:指定對應exception的配置類,默認為org.apache.struts.config.ExceptionConfig
@handler:指定異常處理類,默認為org.apache.struts.action.ExceptionHandler
@key:指定在Resource Bundle種描述該異常的消息key
@path:指定當發生異常時,進行轉發的路徑
@scope:指定ActionMessage實例存放的范圍,默認為request,另外一個可選值是session
@type:必須要有!指定所需要處理異常類的名字。
@bundle:指定資源綁定
-->
<exception
key=""hello.error
path="/error.jsp"
scope="session"
type="hello.HandleError"/>
</global-exceptions>
<!--
名稱:global-forwards
描述:定義全局轉發
數量:最多一個
子元素:forward
-->
<global-forwards>
<!--
名稱:forward
描述:定義一個具體的轉發
數量:任意多個
屬性:
@className:指定和forward元素對應的配置類,默認為org.apache.struts.action.ActionForward
@contextRelative:如果為true,則指明使用當前上下文,路徑以“/”開頭,默認為false
@name:必須配有!指明轉發路徑的唯一標識符
@path:必須配有!指明轉發或者重定向的URI。必須以"/"開頭。具體配置要與contextRelative相應。
@redirect:為true時,執行重定向操作,否則執行請求轉發。默認為false
-->
<forward name="A" path="/a.jsp"/>
<forward name="B" path="/hello/b.do"/>
</global-forwards>
<!--
名稱:action-mappings
描述:定義action集合
數量:最多一個
子元素:action
-->
<action-mappings>
<!--
名稱:action
描述:定義了從特定的請求路徑到相應的Action類的映射
數量:任意多個
子元素:exception,forward(二者均為局部量)
屬性:
@attribute:制定與當前Action相關聯的ActionForm Bean在request和session范圍內的名稱(key)
@className:與Action元素對應的配置類。默認為org.apache.struts.action.ActionMapping
@forward:指名轉發的URL路徑
@include:指名包含的URL路徑
@input:指名包含輸入表單的URL路徑,表單驗證失敗時,請求會被轉發到該URL中
@name:指定和當前Acion關聯的ActionForm Bean的名字。該名稱必須在form-bean元素中定義過。
@path:指定訪問Action的路徑,以"/"開頭,沒有擴展名
@parameter:為當前的Action配置參數,可以在Action的execute()方法中,通過調用ActionMapping的getParameter()方法來獲取參數
@roles:指定允許調用該Aciton的安全角色。多個角色之間用逗號分割。處理請求時,RequestProcessor會根據該配置項來決定用戶是否有調用該Action的權限
@scope:指定ActionForm Bean的存在范圍,可選值為request和session。默認為session
@type:指定Action類的完整類名
@unknown:值為true時,表示可以處理用戶發出的所有無效的Action URL。默認為false
@validate:指定是否要先調用ActionForm Bean的validate()方法。默認為true
注意:如上屬性中,forward/include/type三者相斥,即三者在同一Action配置中只能存在一個。
-->
<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
數量:最多一個
屬性:
@bufferSize:指定上傳文件的輸入緩沖的大小.默認為4096
@className:指定當前控制器的配置類.默認為org.apache.struts.config.ControllerConfig
@contentType:指定相應結果的內容類型和字符編碼
@locale:指定是否把Locale對象保存到當前用戶的session中,默認為false
@processorClass:指定負責處理請求的Java類的完整類名.默認org.apache.struts.action.RequestProcessor
@tempDir:指定文件上傳時的臨時工作目錄.如果沒有設置,將才用Servlet容器為web應用分配的臨時工作目錄.
@nochache:true時,在相應結果中加入特定的頭參數:Pragma ,Cache-Control,Expires防止頁面被存儲在可數瀏覽器的緩存中,默認為false
-->
<controller
contentType="text/html;charset=UTF-8"
locale="true"
processorClass="CustomRequestProcessor">
</controller>
<!--
名稱:message-resources
描述:配置Resource Bundle.
數量:任意多個
屬性:
@className:指定和message-resources對應的配置類.默認為org.apache.struts.config.MessageResourcesConfig
@factory:指定資源的工廠類,默認為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的插件
數量:任意多個
子元素:set-property
屬性:
@className:指定Struts插件類.此類必須實現org.apache.struts.action.PlugIn接口
-->
<plug-in
className="org.apache.struts.validator.ValidatorPlugIn">
<!--
名稱:set-property
描述:配置插件的屬性
數量:任意多個
屬性:
@property:插件的屬性名稱
@value:該名稱所配置的值
-->
<set-property
property="pathnames"
value="/WEB-INF/validator-rules.xml,/WEB-INF/vlaidation.xml"/>
</plug-in>
</struts-config>
1. 以常規的方式調用資源(即,調用servlet或JSP頁面)。個人理解為請求通過過濾執行其他的操作
2.利用修改過的請求信息調用資源。對請求的信息加以修改,然后繼續執行
3. 調用資源,但在發送響應到客戶機前對其進行修改
4. 阻止該資源調用,代之以轉到其他的資源,返回一個特定的狀態代碼或生成替換輸出。個人理解為請求被攔截時強制執行(跳轉)的操作
過濾器提供了幾個重要好處 :
首先,它以一種模塊化的或可重用的方式封裝公共的行為。你有30個不同的serlvet或JSP頁面,需要壓縮它們的內容以減少下載時間嗎?沒問題:構造一個壓縮過濾器,然后將它應用到30個資源上即可。
其次,利用它能夠將高級訪問決策與表現代碼相分離。這對于JSP特別有價值,其中一般希望將幾乎整個頁面集中在表現上,而不是集中在業務邏輯上。例如,希 望阻塞來自某些站點的訪問而不用修改各頁面(這些頁面受到訪問限制)嗎?沒問題:建立一個訪問限制過濾器并把它應用到想要限制訪問的頁面上即可。
最后,過濾器使你能夠對許多不同的資源進行批量性的更改。你有許多現存資源,這些資源除了公司名要更改外其他的保持不變,能辦到么?沒問題:構造一個串替換過濾器,只要合適就使用它。
但要注意,過濾器只在與servlet規范2.3版兼容的服務器上有作用。如果你的Web應用需要支持舊版服務器,就不能使用過濾器。
1. 建立基本過濾器
建立一個過濾器涉及下列五個步驟:
1)建立一個實現Filter接口的類。這個類需要三個方法,分別是:doFilter、init和destroy。
doFilter方法包含主要的過濾代碼(見第2步),init方法建立設置操作,而destroy方法進行清楚。
2)在doFilter方法中放入過濾行為。doFilter方法的第一個參數為ServletRequest對象。此對象給過濾器提供了對進入的信息 (包括表單數據、cookie和HTTP請求頭)的完全訪問。第二個參數為ServletResponse,通常在簡單的過濾器中忽略此參數。最后一個參 數為FilterChain,如下一步所述,此參數用來調用servlet或JSP頁。
3)調用FilterChain對象的doFilter方法。Filter接口的doFilter方法取一個FilterChain對象作為它的一個參 數。在調用此對象的doFilter方法時,激活下一個相關的過濾器。如果沒有另一個過濾器與servlet或JSP頁面關聯,則servlet或JSP 頁面被激活。
4)對相應的servlet和JSP頁面注冊過濾器。在部署描述符文件(web.xml)中使用filter和filter-mapping元素。
5)禁用激活器servlet。防止用戶利用缺省servlet URL繞過過濾器設置。
1.1 建立一個實現Filter接口的類
所有過濾器都必須實現javax.servlet.Filter。這個接口包含三個方法,分別為doFilter、init和destroy。
public void doFilter(ServletRequset request,
ServletResponse response,
FilterChain chain)
thows ServletException, IOException
每當調用一個過濾器(即,每次請求與此過濾器相關的servlet或JSP頁面)時,就執行其doFilter方法。正是這個方法包含了大部分過濾邏輯。 第一個參數為與傳入請求有關的ServletRequest。對于簡單的過濾器,大多數過濾邏輯是基于這個對象的。如果處理HTTP請求,并且需要訪問諸 如getHeader或getCookies等在ServletRequest中無法得到的方法,就要把此對象構造成 HttpServletRequest。
第二個參數為ServletResponse。除了在兩個情形下要使用它以外,通常忽略這個參數。首先,如果希望完全阻塞對相關servlet或JSP頁 面的訪問??烧{用response.getWriter并直接發送一個響應到客戶機。其次,如果希望修改相關的servlet或JSP頁面的輸出,可把響 應包含在一個收集所有發送到它的輸出的對象中。然后,在調用serlvet或JSP頁面后,過濾器可檢查輸出,如果合適就修改它,之后發送到客戶機。
DoFilter的最后一個參數為FilterChain對象。對此對象調用doFilter以激活與servlet或JSP頁面相關的下一個過濾器。如果沒有另一個相關的過濾器,則對doFilter的調用激活servlet或JSP本身。
public void init(FilterConfig config) thows ServletException
init方法只在此過濾器第一次初始化時執行,不是每次調用過濾器都執行它。對于簡單的過濾器,可提供此方法的一個空體,但有兩個原因需要使用init。 首先,FilterConfig對象提供對servlet環境及web.xml文件中指派的過濾器名的訪問。因此,普遍的辦法是利用init將 FilterConfig對象存放在一個字段中,以便doFilter方法能夠訪問servlet環境或過濾器名.其次,FilterConfig對象具 有一個getInitParameter方法,它能夠訪問部署描述符文件(web.xml)中分配的過濾器初始化參數。
public void destroy( )
大多數過濾器簡單地為此方法提供一個空體,不過,可利用它來完成諸如關閉過濾器使用的文件或數據庫連接池等清除任務。
1.2 將過濾行為放入doFilter方法
doFilter方法為大多數過濾器地關鍵部分。每當調用一個過濾器時,都要執行doFilter。對于大多數過濾器來說,doFilter執行的步驟是 基于傳入的信息的。因此,可能要利用作為doFilter的第一個參數提供的ServletRequest。這個對象常常構造為 HttpServletRequest類型,以提供對該類的更特殊方法的訪問。
1.3 調用FilterChain對象的doFilter方法
Filter接口的doFilter方法以一個FilterChain對象作為它的第三個參數。在調用該對象的doFilter方法時,激活下一個相關的 過濾器。這個過程一般持續到鏈中最后一個過濾器為止。在最后一個過濾器調用其FilterChain對象的doFilter方法時,激活servlet或 頁面自身。
但是,鏈中的任意過濾器都可以通過不調用其FilterChain的doFilter方法中斷這個過程。在這樣的情況下,不再調用JSP頁面的serlvet,并且中斷此調用過程的過濾器負責將輸出提供給客戶機。
1.4 對適當的servlet和JSP頁面注冊過濾器
部署描述符文件的2.3版本引入了兩個用于過濾器的元素,分別是:filter和filter-mapping。filter元素向系統注冊一個過濾對象,filter-mapping元素指定該過濾對象所應用的URL。
1.filter元素
filter元素位于部署描述符文件(web.xml)的前部,所有filter-mapping、servlet或servlet-mapping元素之前。filter元素具有如下六個可能的子元素:
1、 icon 這是一個可選的元素,它聲明IDE能夠使用的一個圖象文件。
2、filter-name 這是一個必需的元素,它給過濾器分配一個選定的名字。
3、display-name 這是一個可選的元素,它給出IDE使用的短名稱。
4、 description 這也是一個可選的元素,它給出IDE的信息,提供文本文檔。
5、 filter-class 這是一個必需的元素,它指定過濾器實現類的完全限定名。
6、 init-param 這是一個可選的元素,它定義可利用FilterConfig的getInitParameter方法讀取的初始化參數。單個過濾器元素可包含多個init-param元素。
請注意,過濾是在serlvet規范2.3版中初次引入的。因此,web.xml文件必須使用DTD的2.3版本。下面介紹一個簡單的例子:
<xml version="1.0" encoding="ISO-8859-1"?>
DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "
<web-app>
<filter>
<filter-name>MyFilterfilter-name>
<filter-class>myPackage.FilterClassfilter-class>
filter>
<filter-mapping>...filter-mapping>
<web-app>
2.filter-mapping元素
filter-mapping元素位于web.xml文件中filter元素之后serlvet元素之前。它包含如下三個可能的子元素:
1、 filter-name 這個必需的元素必須與用filter元素聲明時給予過濾器的名稱相匹配。
2、 url-pattern 此元素聲明一個以斜杠(/)開始的模式,它指定過濾器應用的URL。所有filter-mapping元素中必須提供url-pattern或 servlet-name。但不能對單個filter-mapping元素提供多個url-pattern元素項。如果希望過濾器適用于多個模式,可重復 整個filter-mapping元素。
3、 servlet-name 此元素給出一個名稱,此名稱必須與利用servlet元素給予servlet或JSP頁面的名稱相匹配。不能給單個filter-mapping元素提供 多個servlet-name元素項。如果希望過濾器適合于多個servlet名,可重復這個filter-mapping元素。
下面舉一個例子:
xml version="1.0" encoding="ISO-8859-1"?>
DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"
<web-app>
<filter>
<filter-name>MyFilterfilter-name>
<filter-class>myPackage.FilterClassfilter-class>
filter>
<filter-mapping>
<filter-name>MyFilterfilter-name>
<url-pattern>/someDirectory/SomePage.jspurl-pattern>
filter-mapping>
web-app>
1.5 禁用激活器servlet
在對資源應用過濾器時,可通過指定要應用過濾器的URL模式或servlet名來完成。如果提供servlet名,則此名稱必須與web.xml的 servlet元素中給出的名稱相匹配。如果使用應用到一個serlvet的URL模式,則此模式必須與利用web.xml的元素servlet- mapping指定的模式相匹配。但是,多數服務器使用“激活器servlet”為servlet體統一個缺省的URL:http: //host/WebAppPrefix/servlet/ServletName。需要保證用戶不利用這個URL訪問servlet(這樣會繞過過濾器 設置)。
例如,假如利用filter和filter-mapping指示名為SomeFilter的過濾器應用到名為SomeServlet的servlet,則如下:
<filter>
<filter-name>SomeFilterfilter-name>
<filter-class>somePackage.SomeFilterClassfilter-class>
<filter>
<filter-mapping>
<filter-name>SomeFilterfilter-name>
<servlet-name>SomeServletservlet-name>
<filter-mapping>
接著,用servlet和servlet-mapping規定URL http://host/webAppPrefix/Blah 應該調用SomeSerlvet,如下所示:
<filter>
<filter-name>SomeFilterfilter-name>
<filter-class>somePackage.SomeFilterClassfilter-class>
filter>
<filter-mapping>
<filter-name>SomeFilterfilter-name>
<servlet-name>/Blahservlet-name>
<filter-mapping>
現在,在客戶機使用URL http://host/webAppPrefix/Blah 時就會調用過濾器。過濾器不應用到
http://host/webAppPrefix/servlet/SomePackage.SomeServletClass。
盡管有關閉激活器的服務器專用方法。但是,可移植最強的方法時重新映射Web應用鐘的/servlet模式,這樣使所有包含此模式的請求被送到相同的 servlet中。為了重新映射此模式,首先應該建立一個簡單的servlet,它打印一條錯誤消息,或重定向用戶到頂層頁。然后,使用servlet和 servlet-mapping元素發送包含/servlet模式的請求到該servlet。程序清單9-1給出了一個簡短的例子。
程序清單9-1 web.xml(重定向缺省servlet URL的摘錄)
xml version="1.0" encoding="ISO-8859-1"?>
DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"
<web-app>
<servlet>
<servlet-name>Errorservlet-name>
<servlet-class>somePackage.ErrorServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>Errorservlet-name>
<url-pattern>/servlet/*url-pattern>
servlet-mapping>
<web-app>
本文參考:http://www.javaeye.com/topic/140553