ASM 是一個 Java 字節(jié)碼操控框架。它能被用來動態(tài)生成類或者增強既有類的功能。ASM 可以直接產(chǎn)生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態(tài)改變類行為。Java class 被存儲在嚴格格式定義的 .class 文件里,這些類文件擁有足夠的元數(shù)據(jù)來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節(jié)碼(指令)。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。
與 BCEL 和 SERL 不同,ASM 提供了更為現(xiàn)代的編程模型。對于 ASM 來說,Java class 被描述為一棵樹;使用 “Visitor” 模式遍歷整個二進制結(jié)構(gòu);事件驅(qū)動的處理方式使得用戶只需要關注于對其編程有意義的部分,而不必了解 Java 類文件格式的所有細節(jié):ASM 框架提供了默認的 “response taker”處理這一切。
動態(tài)生成 Java 類與 AOP 密切相關的。AOP 的初衷在于軟件設計世界中存在這么一類代碼,零散而又耦合:零散是由于一些公有的功能(諸如著名的 log 例子)分散在所有模塊之中;同時改變 log 功能又會影響到所有的模塊。出現(xiàn)這樣的缺陷,很大程度上是由于傳統(tǒng)的 面向?qū)ο缶幊套⒅匾岳^承關系為代表的“縱向”關系,而對于擁有相同功能或者說方面 (Aspect)的模塊之間的“橫向”關系不能很好地表達。例如,目前有一個既有的銀行管理系統(tǒng),包括 Bank、Customer、Account、Invoice 等對象,現(xiàn)在要加入一個安全檢查模塊, 對已有類的所有操作之前都必須進行一次安全檢查。
然而 Bank、Customer、Account、Invoice 是代表不同的事務,派生自不同的父類,很難在高層上加入關于 Security Checker 的共有功能。對于沒有多繼承的 Java 來說,更是如此。傳統(tǒng)的解決方案是使用 Decorator 模式,它可以在一定程度上改善耦合,而功能仍舊是分散的 —— 每個需要 Security Checker 的類都必須要派生一個 Decorator,每個需要 Security Checker 的方法都要被包裝(wrap)。下面我們以 Account
類為例看一下 Decorator:
首先,我們有一個 SecurityChecker
類,其靜態(tài)方法 checkSecurity
執(zhí)行安全檢查功能:
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()
調(diào)用,標準的 Decorator 需要先定義一個 Account
類的接口:
public interface Account { void operation(); } |
然后把原來的 Account
類定義為一個實現(xiàn)類:
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 很快就會演化成另一個噩夢。動態(tài)改變 Java 類就是要解決 AOP 的問題,提供一種得到系統(tǒng)支持的可編程的方法,自動化地生成或者增強 Java 代碼。這種技術已經(jīng)廣泛應用于最新的 Java 框架內(nèi),如 Hibernate,Spring 等。
最直接的改造 Java 類的方法莫過于直接改寫 class 文件。Java 規(guī)范詳細說明了class 文件的格式,直接編輯字節(jié)碼確實可以改變 Java 類的行為。直到今天,還有一些 Java 高手們使用最原始的工具,如 UltraEdit 這樣的編輯器對 class 文件動手術。是的,這是最直接的方法,但是要求使用者對 Java class 文件的格式了熟于心:小心地推算出想改造的函數(shù)相對文件首部的偏移量,同時重新計算 class 文件的校驗碼以通過 Java 虛擬機的安全機制。
Java 5 中提供的 Instrument 包也可以提供類似的功能:啟動時往 Java 虛擬機中掛上一個用戶定義的 hook 程序,可以在裝入特定類的時候改變特定類的字節(jié)碼,從而改變該類的行為。但是其缺點也是明顯的:
ClassFileTransformer. transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
,還是 Instrument.redefineClasses(ClassDefinition[] definitions)
,都必須提供新 Java 類的字節(jié)碼。也就是說,同直接改寫 class 文件一樣,使用 Instrument 也必須了解想改造的方法相對類首部的偏移量,才能在適當?shù)奈恢蒙喜迦胄碌拇a。 盡管 Instrument 可以改造類,但事實上,Instrument 更適用于監(jiān)控和控制虛擬機的行為。
一種比較理想且流行的方法是使用 java.lang.ref.proxy
。我們?nèi)耘f使用上面的例子,給 Account
類加上 checkSecurity 功能:
首先,Proxy 編程是面向接口的。下面我們會看到,Proxy 并不負責實例化對象,和 Decorator 模式一樣,要把 Account
定義成一個接口,然后在 AccountImpl
里實現(xiàn) Account
接口,接著實現(xiàn)一個 InvocationHandler
Account
方法被調(diào)用的時候,虛擬機都會實際調(diào)用這個 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
生成的是實現(xiàn) Account
接口的對象而不是 AccountImpl
的子類。這對于軟件架構(gòu)設計,尤其對于既有軟件系統(tǒng)是有一定掣肘的。
ASM 能夠通過改造既有類,直接生成需要的代碼。增強的代碼是硬編碼在新生成的類文件內(nèi)部的,沒有反射帶來性能上的付出。同時,ASM 與 Proxy 編程不同,不需要為增強代碼而新定義一個接口,生成的代碼可以覆蓋原來的類,或者是原始類的子類。它是一個普通的 Java 類而不是 proxy 類,甚至可以在應用程序的類框架中擁有自己的位置,派生自己的子類。
相比于其他流行的 Java 字節(jié)碼操縱工具,ASM 更小更快。ASM 具有類似于 BCEL 或者 SERP 的功能,而只有 33k 大小,而后者分別有 350k 和 150k。同時,同樣類轉(zhuǎn)換的負載,如果 ASM 是 60% 的話,BCEL 需要 700%,而 SERP 需要 1100% 或者更多。
ASM 已經(jīng)被廣泛應用于一系列 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 編譯器產(chǎn)生的 .class 文件。這些文件具有嚴格定義的格式。為了更好的理解 ASM,首先對 Java 類文件格式作一點簡單的介紹。Java 源文件經(jīng)過 javac 編譯器編譯之后,將會生成對應的二進制文件(如下圖所示)。每個合法的 Java 類文件都具備精確的定義,而正是這種精確的定義,才使得 Java 虛擬機得以正確讀取和解釋所有的 Java 類文件。
Java 類文件是 8 位字節(jié)的二進制流。數(shù)據(jù)項按順序存儲在 class 文件中,相鄰的項之間沒有間隔,這使得 class 文件變得緊湊,減少存儲空間。在 Java 類文件中包含了許多大小不同的項,由于每一項的結(jié)構(gòu)都有嚴格規(guī)定,這使得 class 文件能夠從頭到尾被順利地解析。下面讓我們來看一下 Java 類文件的內(nèi)部結(jié)構(gòu),以便對此有個大致的認識。
例如,一個最簡單的 Hello World 程序:
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello world"); } } |
經(jīng)過 javac 編譯后,得到的類文件大致是:
從上圖中可以看到,一個 Java 類文件大致可以歸為 10 個項:
事實上,使用 ASM 動態(tài)生成類,不需要像早年的 class hacker 一樣,熟知 class 文件的每一段,以及它們的功能、長度、偏移量以及編碼方式。ASM 會給我們照顧好這一切的,我們只要告訴 ASM 要改動什么就可以了 —— 當然,我們首先得知道要改什么:對類文件格式了解的越多,我們就能更好地使用 ASM 這個利器。
![]() ![]() |
![]()
|
ASM 通過樹這種數(shù)據(jù)結(jié)構(gòu)來表示復雜的字節(jié)碼結(jié)構(gòu),并利用 Push 模型來對樹進行遍歷,在遍歷過程中對字節(jié)碼進行修改。所謂的 Push 模型類似于簡單的 Visitor 設計模式,因為需要處理字節(jié)碼結(jié)構(gòu)是固定的,所以不需要專門抽象出一種 Vistable 接口,而只需要提供 Visitor 接口。所謂 Visitor 模式和 Iterator 模式有點類似,它們都被用來遍歷一些復雜的數(shù)據(jù)結(jié)構(gòu)。Visitor 相當于用戶派出的代表,深入到算法內(nèi)部,由算法安排訪問行程。Visitor 代表可以更換,但對算法流程無法干涉,因此是被動的,這也是它和 Iterator 模式由用戶主動調(diào)遣算法方式的最大的區(qū)別。
在 ASM 中,提供了一個 ClassReader
類,這個類可以直接由字節(jié)數(shù)組或由 class 文件間接的獲得字節(jié)碼數(shù)據(jù),它能正確的分析字節(jié)碼,構(gòu)建出抽象的樹在內(nèi)存中表示字節(jié)碼。它會調(diào)用 accept
方法,這個方法接受一個實現(xiàn)了 ClassVisitor
接口的對象實例作為參數(shù),然后依次調(diào)用 ClassVisitor
接口的各個方法。字節(jié)碼空間上的偏移被轉(zhuǎn)換成 visit 事件時間上調(diào)用的先后,所謂 visit 事件是指對各種不同 visit 函數(shù)的調(diào)用,ClassReader
知道如何調(diào)用各種 visit 函數(shù)。在這個過程中用戶無法對操作進行干涉,所以遍歷的算法是確定的,用戶可以做的是提供不同的 Visitor 來對字節(jié)碼樹進行不同的修改。ClassVisitor
會產(chǎn)生一些子過程,比如 visitMethod
會返回一個實現(xiàn) MethordVisitor
接口的實例,visitField
會返回一個實現(xiàn) FieldVisitor
接口的實例,完成子過程后控制返回到父過程,繼續(xù)訪問下一節(jié)點。因此對于 ClassReader
來說,其內(nèi)部順序訪問是有一定要求的。實際上用戶還可以不通過 ClassReader
類,自行手工控制這個流程,只要按照一定的順序,各個 visit 事件被先后正確的調(diào)用,最后就能生成可以被正確加載的字節(jié)碼。當然獲得更大靈活性的同時也加大了調(diào)整字節(jié)碼的復雜度。
各個 ClassVisitor
通過職責鏈 (Chain-of-responsibility) 模式,可以非常簡單的封裝對字節(jié)碼的各種修改,而無須關注字節(jié)碼的字節(jié)偏移,因為這些實現(xiàn)細節(jié)對于用戶都被隱藏了,用戶要做的只是覆寫相應的 visit 函數(shù)。
ClassAdaptor
類實現(xiàn)了 ClassVisitor
接口所定義的所有函數(shù),當新建一個 ClassAdaptor
對象的時候,需要傳入一個實現(xiàn)了 ClassVisitor
接口的對象,作為職責鏈中的下一個訪問者 (Visitor),這些函數(shù)的默認實現(xiàn)就是簡單的把調(diào)用委派給這個對象,然后依次傳遞下去形成職責鏈。當用戶需要對字節(jié)碼進行調(diào)整時,只需從 ClassAdaptor
類派生出一個子類,覆寫需要修改的方法,完成相應功能后再把調(diào)用傳遞下去。這樣,用戶無需考慮字節(jié)偏移,就可以很方便的控制字節(jié)碼。
每個 ClassAdaptor
類的派生類可以僅封裝單一功能,比如刪除某函數(shù)、修改字段可見性等等,然后再加入到職責鏈中,這樣耦合更小,重用的概率也更大,但代價是產(chǎn)生很多小對象,而且職責鏈的層次太長的話也會加大系統(tǒng)調(diào)用的開銷,用戶需要在低耦合和高效率之間作出權(quán)衡。用戶可以通過控制職責鏈中 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); } } |
修改類、字段、方法的名字或修飾符:在職責鏈傳遞過程中替換調(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); } } |
增加新的類、方法、字段
ASM 的最終的目的是生成可以被正常裝載的 class 文件,因此其框架結(jié)構(gòu)為客戶提供了一個生成字節(jié)碼的工具類 —— ClassWriter
。它實現(xiàn)了 ClassVisitor
接口,而且含有一個 toByteArray()
函數(shù),返回生成的字節(jié)碼的字節(jié)流,將字節(jié)流寫回文件即可生產(chǎn)調(diào)整后的 class 文件。一般它都作為職責鏈的終點,把所有 visit 事件的先后調(diào)用(時間上的先后),最終轉(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 的時序圖如下:
![]() ![]() |
![]()
|
我們還是用上面的例子,給 Account
類加上 security check 的功能。與 proxy 編程不同,ASM 不需要將 Account
聲明成接口,Account
可以仍舊是一個實現(xiàn)類。ASM 將直接在 Account
類上動手術,給 Account
類的 operation
方法首部加上對 SecurityChecker.checkSecurity
的調(diào)用。
首先,我們將從 ClassAdapter
繼承一個類。ClassAdapter
是 ASM 框架提供的一個默認類,負責溝通 ClassReader
和 ClassWriter
。如果想要改變 ClassReader
處讀入的類,然后從 ClassWriter
處輸出,可以重寫相應的 ClassAdapter
函數(shù)。這里,為了改變 Account
類的 operation
方法,我們將重寫 visitMethdod
方法。
class AddSecurityCheckClassAdapter extends ClassAdapter{ public AddSecurityCheckClassAdapter(ClassVisitor cv) { //Responsechain 的下一個 ClassVisitor,這里我們將傳入 ClassWriter, //負責改寫后代碼的輸出 super(cv); } //重寫 visitMethod,訪問到 "operation" 方法時, //給出自定義 MethodVisitor,實際改寫方法內(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) { //對于 "operation" 方法 if (name.equals("operation")) { //使用自定義 MethodVisitor,實際改寫方法內(nèi)容 wrappedMv = new AddSecurityCheckMethodAdapter(mv); } } return wrappedMv; } } |
下一步就是定義一個繼承自 MethodAdapter
的 AddSecurityCheckMethodAdapter
,在“operation
”方法首部插入對 SecurityChecker.checkSecurity()
的調(diào)用。
class AddSecurityCheckMethodAdapter extends MethodAdapter { public AddSecurityCheckMethodAdapter(MethodVisitor mv) { super(mv); } public void visitCode() { visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker", "checkSecurity", "()V"); } } |
其中,ClassReader
讀到每個方法的首部時調(diào)用 visitCode()
,在這個重寫方法里,我們用visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker","checkSecurity", "()V");
插入了安全檢查功能。
最后,我們將集成上面定義的 ClassAdapter
,ClassReader
和ClassWriter
產(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í)行完這段程序后,我們會得到一個新的 Account.class 文件,如果我們使用下面代碼:
public class Main { public static void main(String[] args) { Account account = new Account(); account.operation(); } } |
使用這個 Account,我們會得到下面的輸出:
SecurityChecker.checkSecurity ... operation... |
也就是說,在 Account
原來的 operation
內(nèi)容執(zhí)行之前,進行了 SecurityChecker.checkSecurity()
檢查。
上面給出的例子是直接改造 Account
類本身的,從此 Account
類的 operation
方法必須進行 checkSecurity 檢查。但事實上,我們有時仍希望保留原來的 Account
類,因此把生成類定義為原始類的子類是更符合 AOP 原則的做法。下面介紹如何將改造后的類定義為 Account
的子類 Account$EnhancedByASM
。其中主要有兩項工作:
Account$EnhancedByASM
,將其父類指定為 Account
。
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); } |
改進 visitMethod
方法,增加對構(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
將負責把 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ù)時 if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) { owner = superClassName; } super.visitMethodInsn(opcode, owner, name, desc);//改寫父類為superClassName } } |
最后演示一下如何在運行時產(chǎn)生并裝入產(chǎn)生的 Account$EnhancedByASM
。 我們定義一個 Util
類,作為一個類工廠負責產(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()
在運行時動態(tài)生成一個加上了安全檢查的 Account
子類。著名的 Hibernate 和 Spring 框架,就是使用這種技術實現(xiàn)了 AOP 的“無損注入”。
![]() ![]() |
![]()
|
最后,我們比較一下 ASM 和其他實現(xiàn) AOP 的底層技術:
過濾器提供了幾個重要好處 :
首先,它以一種模塊化的或可重用的方式封裝公共的行為。你有30個不同的serlvet或JSP頁面,需要壓縮它們的內(nèi)容以減少下載時間嗎?沒問題:構(gòu)造一個壓縮過濾器,然后將它應用到30個資源上即可。
其次,利用它能夠?qū)⒏呒壴L問決策與表現(xiàn)代碼相分離。這對于JSP特別有價值,其中一般希望將幾乎整個頁面集中在表現(xiàn)上,而不是集中在業(yè)務邏輯上。例如,希 望阻塞來自某些站點的訪問而不用修改各頁面(這些頁面受到訪問限制)嗎?沒問題:建立一個訪問限制過濾器并把它應用到想要限制訪問的頁面上即可。
最后,過濾器使你能夠?qū)υS多不同的資源進行批量性的更改。你有許多現(xiàn)存資源,這些資源除了公司名要更改外其他的保持不變,能辦到么?沒問題:構(gòu)造一個串替換過濾器,只要合適就使用它。
但要注意,過濾器只在與servlet規(guī)范2.3版兼容的服務器上有作用。如果你的Web應用需要支持舊版服務器,就不能使用過濾器。
1. 建立基本過濾器
建立一個過濾器涉及下列五個步驟:
1)建立一個實現(xiàn)Filter接口的類。這個類需要三個方法,分別是:doFilter、init和destroy。
doFilter方法包含主要的過濾代碼(見第2步),init方法建立設置操作,而destroy方法進行清楚。
2)在doFilter方法中放入過濾行為。doFilter方法的第一個參數(shù)為ServletRequest對象。此對象給過濾器提供了對進入的信息 (包括表單數(shù)據(jù)、cookie和HTTP請求頭)的完全訪問。第二個參數(shù)為ServletResponse,通常在簡單的過濾器中忽略此參數(shù)。最后一個參 數(shù)為FilterChain,如下一步所述,此參數(shù)用來調(diào)用servlet或JSP頁。
3)調(diào)用FilterChain對象的doFilter方法。Filter接口的doFilter方法取一個FilterChain對象作為它的一個參 數(shù)。在調(diào)用此對象的doFilter方法時,激活下一個相關的過濾器。如果沒有另一個過濾器與servlet或JSP頁面關聯(lián),則servlet或JSP 頁面被激活。
4)對相應的servlet和JSP頁面注冊過濾器。在部署描述符文件(web.xml)中使用filter和filter-mapping元素。
5)禁用激活器servlet。防止用戶利用缺省servlet URL繞過過濾器設置。
1.1 建立一個實現(xiàn)Filter接口的類
所有過濾器都必須實現(xiàn)javax.servlet.Filter。這個接口包含三個方法,分別為doFilter、init和destroy。
public void doFilter(ServletRequset request,
ServletResponse response,
FilterChain chain)
thows ServletException, IOException
每當調(diào)用一個過濾器(即,每次請求與此過濾器相關的servlet或JSP頁面)時,就執(zhí)行其doFilter方法。正是這個方法包含了大部分過濾邏輯。 第一個參數(shù)為與傳入請求有關的ServletRequest。對于簡單的過濾器,大多數(shù)過濾邏輯是基于這個對象的。如果處理HTTP請求,并且需要訪問諸 如getHeader或getCookies等在ServletRequest中無法得到的方法,就要把此對象構(gòu)造成 HttpServletRequest。
第二個參數(shù)為ServletResponse。除了在兩個情形下要使用它以外,通常忽略這個參數(shù)。首先,如果希望完全阻塞對相關servlet或JSP頁 面的訪問??烧{(diào)用response.getWriter并直接發(fā)送一個響應到客戶機。其次,如果希望修改相關的servlet或JSP頁面的輸出,可把響 應包含在一個收集所有發(fā)送到它的輸出的對象中。然后,在調(diào)用serlvet或JSP頁面后,過濾器可檢查輸出,如果合適就修改它,之后發(fā)送到客戶機。
DoFilter的最后一個參數(shù)為FilterChain對象。對此對象調(diào)用doFilter以激活與servlet或JSP頁面相關的下一個過濾器。如果沒有另一個相關的過濾器,則對doFilter的調(diào)用激活servlet或JSP本身。
public void init(FilterConfig config) thows ServletException
init方法只在此過濾器第一次初始化時執(zhí)行,不是每次調(diào)用過濾器都執(zhí)行它。對于簡單的過濾器,可提供此方法的一個空體,但有兩個原因需要使用init。 首先,F(xiàn)ilterConfig對象提供對servlet環(huán)境及web.xml文件中指派的過濾器名的訪問。因此,普遍的辦法是利用init將 FilterConfig對象存放在一個字段中,以便doFilter方法能夠訪問servlet環(huán)境或過濾器名.其次,F(xiàn)ilterConfig對象具 有一個getInitParameter方法,它能夠訪問部署描述符文件(web.xml)中分配的過濾器初始化參數(shù)。
public void destroy( )
大多數(shù)過濾器簡單地為此方法提供一個空體,不過,可利用它來完成諸如關閉過濾器使用的文件或數(shù)據(jù)庫連接池等清除任務。
1.2 將過濾行為放入doFilter方法
doFilter方法為大多數(shù)過濾器地關鍵部分。每當調(diào)用一個過濾器時,都要執(zhí)行doFilter。對于大多數(shù)過濾器來說,doFilter執(zhí)行的步驟是 基于傳入的信息的。因此,可能要利用作為doFilter的第一個參數(shù)提供的ServletRequest。這個對象常常構(gòu)造為 HttpServletRequest類型,以提供對該類的更特殊方法的訪問。
1.3 調(diào)用FilterChain對象的doFilter方法
Filter接口的doFilter方法以一個FilterChain對象作為它的第三個參數(shù)。在調(diào)用該對象的doFilter方法時,激活下一個相關的 過濾器。這個過程一般持續(xù)到鏈中最后一個過濾器為止。在最后一個過濾器調(diào)用其FilterChain對象的doFilter方法時,激活servlet或 頁面自身。
但是,鏈中的任意過濾器都可以通過不調(diào)用其FilterChain的doFilter方法中斷這個過程。在這樣的情況下,不再調(diào)用JSP頁面的serlvet,并且中斷此調(diào)用過程的過濾器負責將輸出提供給客戶機。
1.4 對適當?shù)膕ervlet和JSP頁面注冊過濾器
部署描述符文件的2.3版本引入了兩個用于過濾器的元素,分別是:filter和filter-mapping。filter元素向系統(tǒng)注冊一個過濾對象,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 這是一個必需的元素,它指定過濾器實現(xiàn)類的完全限定名。
6、 init-param 這是一個可選的元素,它定義可利用FilterConfig的getInitParameter方法讀取的初始化參數(shù)。單個過濾器元素可包含多個init-param元素。
請注意,過濾是在serlvet規(guī)范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指定的模式相匹配。但是,多數(shù)服務器使用“激活器servlet”為servlet體統(tǒng)一個缺省的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規(guī)定URL http://host/webAppPrefix/Blah 應該調(diào)用SomeSerlvet,如下所示:
<filter>
<filter-name>SomeFilterfilter-name>
<filter-class>somePackage.SomeFilterClassfilter-class>
filter>
<filter-mapping>
<filter-name>SomeFilterfilter-name>
<servlet-name>/Blahservlet-name>
<filter-mapping>
現(xiàn)在,在客戶機使用URL http://host/webAppPrefix/Blah 時就會調(diào)用過濾器。過濾器不應用到
http://host/webAppPrefix/servlet/SomePackage.SomeServletClass。
盡管有關閉激活器的服務器專用方法。但是,可移植最強的方法時重新映射Web應用鐘的/servlet模式,這樣使所有包含此模式的請求被送到相同的 servlet中。為了重新映射此模式,首先應該建立一個簡單的servlet,它打印一條錯誤消息,或重定向用戶到頂層頁。然后,使用servlet和 servlet-mapping元素發(fā)送包含/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
<html:javascript formName="searchSgbySjForm" dynamicJavascript="true" staticJavascript="false"/>
生成 :
var bCancel = false;
function validateSearchSgbySjForm(form)
{
if (bCancel) return true;
else return validateRequired(form) && validateDate(form);
}
function required ()
{
this.aa = new Array("sgfssjq", "事故發(fā)生時間起 不可為空.", new Function ("varName", "this.datePatternStrict='yyy-MM-dd'; return thisvarName];"));
this.ab = new Array("sgfssjz", "事故發(fā)生時間止 不可為空.", new Function ("varName", "this.datePatternStrict='yyy-MM-dd'; return this[varName];"));
}
function DateValidations ()
{
this.aa = new Array("sgfssjq", "事故發(fā)生時間起 不是有效的日期類型.", new Function ("varName", "this.datePatternStrict='yyy-MM-dd'; return this [varName];"));
this.ab = new Array("sgfssjz", "事故發(fā)生時間止 不是有效的日期類型.", new Function ("varName", "this.datePatternStrict='yyy-MM-dd'; return this[varName];"));
}
如果有多個的話required和DateValidations 都會重復的,而javascript是只認最后一個函數(shù)的。所以,會導致驗證出錯。
再寫一個標簽 ,主要根據(jù)原來的代碼修改,代碼如下:
package com.tmri.acd.tag;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.BodyTagSupport;
import org.apache.commons.validator.Field;
import org.apache.commons.validator.Form;
import org.apache.commons.validator.ValidatorAction;
import org.apache.commons.validator.ValidatorResources;
import org.apache.commons.validator.util.ValidatorUtils;
import org.apache.commons.validator.Var;
import org.apache.struts.Globals;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.config.ModuleConfig;
import com.tmri.acd.tag.TagUtils;
import org.apache.struts.util.MessageResources;
import org.apache.struts.validator.Resources;
import org.apache.struts.validator.ValidatorPlugIn;
import java.util.StringTokenizer;
public class JavascriptValidatorTag extends BodyTagSupport
{
private static final Comparator actionComparator = new Comparator()
{
public int compare(Object o1, Object o2)
{
ValidatorAction va1 = (ValidatorAction) o1;
ValidatorAction va2 = (ValidatorAction) o2;
if ((va1.getDepends() == null || va1.getDepends().length() == 0) && (va2.getDepends() == null || va2.getDepends().length() == 0))
{
return 0;
}
else if ( (va1.getDepends() != null && va1.getDepends().length() > 0) && (va2.getDepends() == null || va2.getDepends().length() == 0))
{
return 1;
}
}
1 引子
try…catch…finally恐怕是大家再熟悉不過的語句了,而且感覺用起來也是很簡單,邏輯上似乎也是很容易理解。不過,我親自體驗的“教訓”告訴我,這個東西可不是想象中的那么簡單、聽話。不信?那你看看下面的代碼,“猜猜”它執(zhí)行后的結(jié)果會是什么?不要往后看答案、也不許執(zhí)行代碼看真正答案哦。如果你的答案是正確,那么這篇文章你就不用浪費時間看啦。
public class TestException
{
public TestException()
{
}
boolean testEx() throws Exception
{
boolean ret = true;
try
{
ret = testEx1();
}
catch (Exception e)
{
System.out.println("testEx, catch exception");
ret = false;
throw e;
}
finally
{
System.out.println("testEx, finally; return value=" + ret);
return ret;
}
}
boolean testEx1() throws Exception
{
boolean ret = true;
try
{
ret = testEx2();
if (!ret)
{
return false;
}
System.out.println("testEx1, at the end of try");
return ret;
}
catch (Exception e)
{
System.out.println("testEx1, catch exception");
ret = false;
throw e;
}
finally
{
System.out.println("testEx1, finally; return value=" + ret);
return ret;
}
}
boolean testEx2() throws Exception
{
boolean ret = true;
try
{
int b = 12;
int c;
for (int i = 2; i >= -2; i--)
{
c = b / i;
System.out.println("i=" + i);
}
return true;
}
catch (Exception e)
{
System.out.println("testEx2, catch exception");
ret = false;
throw e;
}
finally
{
System.out.println("testEx2, finally; return value=" + ret);
return ret;
}
}
public static void main(String[] args)
{
TestException testException1 = new TestException();
try
{
testException1.testEx();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
你的答案是什么?是下面的答案嗎?
i=2
i=1
testEx2, catch exception
testEx2, finally; return value=false
testEx1, catch exception
testEx1, finally; return value=false
testEx, catch exception
testEx, finally; return value=false
如果你的答案真的如上面所說,那么你錯啦。^_^,那就建議你仔細看一看這篇文章或者拿上面的代碼按各種不同的情況修改、執(zhí)行、測試,你會發(fā)現(xiàn)有很多事情不是原來想象中的那么簡單的。
現(xiàn)在公布正確答案:
i=2
i=1
testEx2, catch exception
testEx2, finally; return value=false
testEx1, finally; return value=false
testEx, finally; return value=false
2 基礎知識
2.1 相關概念
例外是在程序運行過程中發(fā)生的異常事件,比如除0溢出、數(shù)組越界、文件找不到等,這些事件的發(fā)生將阻止程序的正常運行。為了加強程序的魯棒性,程序設計時,必須考慮到可能發(fā)生的異常事件并做出相應的處理。C語言中,通過使用if語句來判斷是否出現(xiàn)了例外,同時,調(diào)用函數(shù)通過被調(diào)用函數(shù)的返回值感知在被調(diào)用函數(shù)中產(chǎn)生的例外事件并進行處理。全程變量ErroNo常常用來反映一個異常事件的類型。但是,這種錯誤處理機制會導致不少問題。
Java通過面向?qū)ο蟮姆椒▉硖幚砝狻T谝粋€方法的運行過程中,如果發(fā)生了例外,則這個方法生成代表該例外的一個對象,并把它交給運行時系統(tǒng),運行時系統(tǒng)尋找相應的代碼來處理這一例外。我們把生成例外對象并把它提交給運行時系統(tǒng)的過程稱為拋棄(throw)一個例外。運行時系統(tǒng)在方法的調(diào)用棧中查找,從生成例外的方法開始進行回朔,直到找到包含相應例外處理的方法為止,這一個過程稱為捕獲(catch)一個例外。
2.2 Throwable類及其子類
用面向?qū)ο蟮姆椒ㄌ幚砝?,就必須建立類的層次。?Throwable位于這一類層次的最頂層,只有它的后代才可以做為一個例外被拋棄。圖1表示了例外處理的類層次。
從圖中可以看出,類Throwable有兩個直接子類:Error和Exception。Error類對象(如動態(tài)連接錯誤等),由Java虛擬機生成并拋棄(通常,Java程序不對這類例外進行處理);Exception類對象是Java程序處理或拋棄的對象。它有各種不同的子類分別對應于不同類型的例外。其中類RuntimeException代表運行時由Java虛擬機生成的例外,如算術運算例外ArithmeticException(由除0錯等導致)、數(shù)組越界例外ArrayIndexOutOfBoundsException等;其它則為非運行時例外,如輸入輸出例外IOException等。Java編譯器要求Java程序必須捕獲或聲明所有的非運行時例外,但對運行時例外可以不做處理。
2.3 異常處理關鍵字
Java的異常處理是通過5個關鍵字來實現(xiàn)的:try,catch,throw,throws,finally。JB的在線幫助中對這幾個關鍵字是這樣解釋的:
Throws: Lists the exceptions a method could throw.
Throw: Transfers control of the method to the exception handler.
Try: Opening exception-handling statement.
Catch: Captures the exception.
Finally: Runs its code before terminating the program.
2.3.1 try語句
try語句用大括號{}指定了一段代碼,該段代碼可能會拋棄一個或多個例外。
2.3.2 catch語句
catch語句的參數(shù)類似于方法的聲明,包括一個例外類型和一個例外對象。例外類型必須為Throwable類的子類,它指明了catch語句所處理的例外類型,例外對象則由運行時系統(tǒng)在try所指定的代碼塊中生成并被捕獲,大括號中包含對象的處理,其中可以調(diào)用對象的方法。
catch語句可以有多個,分別處理不同類的例外。Java運行時系統(tǒng)從上到下分別對每個catch語句處理的例外類型進行檢測,直到找到類型相匹配的catch語句為止。這里,類型匹配指catch所處理的例外類型與生成的例外對象的類型完全一致或者是它的父類,因此,catch語句的排列順序應該是從特殊到一般。
也可以用一個catch語句處理多個例外類型,這時它的例外類型參數(shù)應該是這多個例外類型的父類,程序設計中要根據(jù)具體的情況來選擇catch語句的例外處理類型。
2.3.3 finally語句
try所限定的代碼中,當拋棄一個例外時,其后的代碼不會被執(zhí)行。通過finally語句可以指定一塊代碼。無論try所指定的程序塊中拋棄或不拋棄例外,也無論catch語句的例外類型是否與所拋棄的例外的類型一致,finally所指定的代碼都要被執(zhí)行,它提供了統(tǒng)一的出口。通常在finally語句中可以進行資源的清除工作。如關閉打開的文件等。
2.3.4 throws語句
throws總是出現(xiàn)在一個函數(shù)頭中,用來標明該成員函數(shù)可能拋出的各種異常。對大多數(shù)Exception子類來說,Java 編譯器會強迫你聲明在一個成員函數(shù)中拋出的異常的類型。如果異常的類型是Error或 RuntimeException, 或它們的子類,這個規(guī)則不起作用, 因為這在程序的正常部分中是不期待出現(xiàn)的。 如果你想明確地拋出一個RuntimeException,你必須用throws語句來聲明它的類型。
2.3.5 throw語句
throw總是出現(xiàn)在函數(shù)體中,用來拋出一個異常。程序會在throw語句后立即終止,它后面的語句執(zhí)行不到,然后在包含它的所有try塊中(可能在上層調(diào)用函數(shù)中)從里向外尋找含有與其匹配的catch子句的try塊。
3 關鍵字及其中語句流程詳解
3.1 try的嵌套
你可以在一個成員函數(shù)調(diào)用的外面寫一個try語句,在這個成員函數(shù)內(nèi)部,寫另一個try語句保護其他代碼。每當遇到一個try語句,異常的框架就放到堆棧上面,直到所有的try語句都完成。如果下一級的try語句沒有對某種異常進行處理,堆棧就會展開,直到遇到有處理這種異常的try語句。下面是一個try語句嵌套的例子。
class MultiNest {
static void procedure() {
try {
int a = 0;
int b = 42/a;
} catch(java.lang.ArithmeticException e) {
System.out.println("in procedure, catch ArithmeticException: " + e);
}
}
public static void main(String args[]) {
try {
procedure();
} catch(java.lang. Exception e) {
System.out.println("in main, catch Exception: " + e);
}
}
}
這個例子執(zhí)行的結(jié)果為:
in procedure, catch ArithmeticException: java.lang.ArithmeticException: / by zero
成員函數(shù)procedure里有自己的try/catch控制,所以main不用去處理 ArrayIndexOutOfBoundsException;當然如果如同最開始我們做測試的例子一樣,在procedure中catch到異常時使用throw e;語句將異常拋出,那么main當然還是能夠捕捉并處理這個procedure拋出來的異常。例如在procedure函數(shù)的catch中的System.out語句后面增加throw e;語句之后,執(zhí)行結(jié)果就變?yōu)椋?br />
in procedure, catch ArithmeticException: java.lang.ArithmeticException: / by zero
in main, catch Exception: java.lang.ArithmeticException: / by zero
3.2 try-catch程序塊的執(zhí)行流程以及執(zhí)行結(jié)果
相對于try-catch-finally程序塊而言,try-catch的執(zhí)行流程以及執(zhí)行結(jié)果還是比較簡單的。
首先執(zhí)行的是try語句塊中的語句,這時可能會有以下三種情況:
1.如果try塊中所有語句正常執(zhí)行完畢,那么就不會有其他的“動做”被執(zhí)行,整個try-catch程序塊正常完成。
2.如果try語句塊在執(zhí)行過程中碰到異常V,這時又分為兩種情況進行處理:
-->如果異常V能夠被與try相應的catch塊catch到,那么第一個catch到這個異常的catch塊(也是離try最近的一個與異常V匹配的catch塊)將被執(zhí)行;如果catch塊執(zhí)行正常,那么try-catch程序塊的結(jié)果就是“正常完成”;如果該catch塊由于原因R突然中止,那么try-catch程序塊的結(jié)果就是“由于原因R突然中止(completes abruptly)”。
-->如果異常V沒有catch塊與之匹配,那么這個try-catch程序塊的結(jié)果就是“由于拋出異常V而突然中止(completes abruptly)”。
3. 如果try由于其他原因R突然中止(completes abruptly),那么這個try-catch程序塊的結(jié)果就是“由于原因R突然中止(completes abruptly)”。
3.3 try-catch-finally程序塊的執(zhí)行流程以及執(zhí)行結(jié)果
try-catch-finally程序塊的執(zhí)行流程以及執(zhí)行結(jié)果比較復雜。
首先執(zhí)行的是try語句塊中的語句,這時可能會有以下三種情況:
1.如果try塊中所有語句正常執(zhí)行完畢,那么finally塊的居于就會被執(zhí)行,這時分為以下兩種情況:
-->如果finally塊執(zhí)行順利,那么整個try-catch-finally程序塊正常完成。
-->如果finally塊由于原因R突然中止,那么try-catch-finally程序塊的結(jié)局是“由于原因R突然中止(completes abruptly)”
2.如果try語句塊在執(zhí)行過程中碰到異常V,這時又分為兩種情況進行處理:
-->如果異常V能夠被與try相應的catch塊catch到,那么第一個catch到這個異常的catch塊(也是離try最近的一個與異常V匹配的catch塊)將被執(zhí)行;這時就會有兩種執(zhí)行結(jié)果:
-->如果catch塊執(zhí)行正常,那么finally塊將會被執(zhí)行,這時分為兩種情況:
-->如果finally塊執(zhí)行順利,那么整個try-catch-finally程序塊正常完成。
-->如果finally塊由于原因R突然中止,那么try-catch-finally程序塊的結(jié)局是“由于原因R突然中止(completes abruptly)”
-->如果catch塊由于原因R突然中止,那么finally模塊將被執(zhí)行,分為兩種情況:
-->如果如果finally塊執(zhí)行順利,那么整個try-catch-finally程序塊的結(jié)局是“由于原因R突然中止(completes abruptly)”。
-->如果finally塊由于原因S突然中止,那么整個try-catch-finally程序塊的結(jié)局是“由于原因S突然中止(completes abruptly)”,原因R將被拋棄。
(注意,這里就正好和我們的例子相符合,雖然我們在testEx2中使用throw e拋出了異常,但是由于testEx2中有finally塊,而finally塊的執(zhí)行結(jié)果是complete abruptly的(別小看這個用得最多的return,它也是一種導致complete abruptly的原因之一啊——后文中有關于導致complete abruptly的原因分析),所以整個try-catch-finally程序塊的結(jié)果是“complete abruptly”,所以在testEx1中調(diào)用testEx2時是捕捉不到testEx1中拋出的那個異常的,而只能將finally中的return結(jié)果獲取到。
如果在你的代碼中期望通過捕捉被調(diào)用的下級函數(shù)的異常來給定返回值,那么一定要注意你所調(diào)用的下級函數(shù)中的finally語句,它有可能會使你throw出來的異常并不能真正被上級調(diào)用函數(shù)可見的。當然這種情況是可以避免的,以testEx2為例:如果你一定要使用finally而且又要將catch中throw的e在testEx1中被捕獲到,那么你去掉testEx2中的finally中的return就可以了。
這個事情已經(jīng)在OMC2.0的MIB中出現(xiàn)過啦:服務器的異常不能完全被反饋到客戶端。)
-->如果異常V沒有catch塊與之匹配,那么finally模塊將被執(zhí)行,分為兩種情況:
-->如果finally塊執(zhí)行順利,那么整個try-catch-finally程序塊的結(jié)局就是“由于拋出異常V而突然中止(completes abruptly)”。
-->如果finally塊由于原因S突然中止,那么整個try-catch-finally程序塊的結(jié)局是“由于原因S突然中止(completes abruptly)”,異常V將被拋棄。
3.如果try由于其他原因R突然中止(completes abruptly),那么finally塊被執(zhí)行,分為兩種情況:
-->如果finally塊執(zhí)行順利,那么整個try-catch-finally程序塊的結(jié)局是“由于原因R突然中止(completes abruptly)”。
-->如果finally塊由于原因S突然中止,那么整個try-catch-finally程序塊的結(jié)局是“由于原因S突然中止(completes abruptly)”,原因R將被拋棄。
3.4 try-catch-finally程序塊中的return
從上面的try-catch-finally程序塊的執(zhí)行流程以及執(zhí)行結(jié)果一節(jié)中可以看出無論try或catch中發(fā)生了什么情況,finally都是會被執(zhí)行的,那么寫在try或者catch中的return語句也就不會真正的從該函數(shù)中跳出了,它的作用在這種情況下就變成了將控制權(quán)(語句流程)轉(zhuǎn)到finally塊中;這種情況下一定要注意返回值的處理。
例如,在try或者catch中return false了,而在finally中又return true,那么這種情況下不要期待你的try或者catch中的return false的返回值false被上級調(diào)用函數(shù)獲取到,上級調(diào)用函數(shù)能夠獲取到的只是finally中的返回值,因為try或者catch中的return語句只是轉(zhuǎn)移控制權(quán)的作用。
3.5 如何拋出異常
如果你知道你寫的某個函數(shù)有可能拋出異常,而你又不想在這個函數(shù)中對異常進行處理,只是想把它拋出去讓調(diào)用這個函數(shù)的上級調(diào)用函數(shù)進行處理,那么有兩種方式可供選擇:
第一種方式:直接在函數(shù)頭中throws SomeException,函數(shù)體中不需要try/catch。比如將最開始的例子中的testEx2改為下面的方式,那么testEx1就能捕捉到testEx2拋出的異常了。
boolean testEx2() throws Exception{
boolean ret = true;
int b=12;
int c;
for (int i=2;i>=-2;i--){
c=b/i;
System.out.println("i="+i);
}
return true;
}
第二種方式:使用try/catch,在catch中進行一定的處理之后(如果有必要的話)拋出某種異常。例如上面的testEx2改為下面的方式,testEx1也能捕獲到它拋出的異常:
boolean testEx2() throws Exception{
boolean ret = true;
try{
int b=12;
int c;
for (int i=2;i>=-2;i--){
c=b/i;
System.out.println("i="+i);
}
return true;
}catch (Exception e){
System.out.println("testEx2, catch exception");
Throw e;
}
}
第三種方法:使用try/catch/finally,在catch中進行一定的處理之后(如果有必要的話)拋出某種異常。例如上面的testEx2改為下面的方式,testEx1也能捕獲到它拋出的異常:
boolean testEx2() throws Exception{
boolean ret = true;
try{
int b=12;
int c;
for (int i=2;i>=-2;i--){
c=b/i;
System.out.println("i="+i);
throw new Exception("aaa");
}
return true;
}catch (java.lang.ArithmeticException e){
System.out.println("testEx2, catch exception");
ret = false;
throw new Exception("aaa");
}finally{
System.out.println("testEx2, finally; return value="+ret);
}
}
4 關于abrupt completion
前面提到了complete abruptly(暫且理解為“突然中止”或者“異常結(jié)束”吧),它主要包含了兩種大的情形:abrupt completion of expressions and statements,下面就分兩種情況進行解釋。
4.1 Normal and Abrupt Completion of Evaluation
每一個表達式(expression)都有一種使得其包含的計算得以一步步進行的正常模式,如果每一步計算都被執(zhí)行且沒有異常拋出,那么就稱這個表達式“正常結(jié)束(complete normally)”;如果這個表達式的計算拋出了異常,就稱為“異常結(jié)束(complete abruptly)”。異常結(jié)束通常有一個相關聯(lián)的原因(associated reason),通常也就是拋出一個異常V。
與表達式、操作符相關的運行期異常有:
-->A class instance creation expression, array creation expression , or string concatenation operatior expression throws an OutOfMemoryError if there is insufficient memory available.
-->An array creation expression throws a NegativeArraySizeException if the value of any dimension expression is less than zero.
-->A field access throws a NullPointerException if the value of the object reference expression is null.
-->A method invocation expression that invokes an instance method throws a NullPointerException if the target reference is null.
-->An array access throws a NullPointerException if the value of the array reference expression is null.
-->An array access throws an ArrayIndexOutOfBoundsException if the value of the array index expression is negative or greater than or equal to the length of the array.
-->A cast throws a ClassCastException if a cast is found to be impermissible at run time.
-->An integer division or integer remainder operator throws an ArithmeticException if the value of the right-hand operand expression is zero.
-->An assignment to an array component of reference type throws an ArrayStoreException when the value to be assigned is not compatible with the component type of the array.
4.2 Normal and Abrupt Completion of Statements
正常情況我們就不多說了,在這里主要是列出了abrupt completion的幾種情況:
-->break, continue, and return 語句將導致控制權(quán)的轉(zhuǎn)換,從而使得statements不能正常地、完整地執(zhí)行。
-->某些表達式的計算也可能從java虛擬機拋出異常,這些表達式在上一小節(jié)中已經(jīng)總結(jié)過了;一個顯式的的throw語句也將導致異常的拋出。拋出異常也是導致控制權(quán)的轉(zhuǎn)換的原因(或者說是阻止statement正常結(jié)束的原因)。
如果上述事件發(fā)生了,那么這些statement就有可能使得其正常情況下應該都執(zhí)行的語句不能完全被執(zhí)行到,那么這些statement也就是被稱為是complete abruptly.
導致abrupt completion的幾種原因:
-->A break with no label
-->A break with a given label
-->A continue with no label
-->A continue with a given label
-->A return with no value
-->A return with a given value A
-->throw with a given value, including exceptions thrown by the Java virtual machine
5 關于我們的編程的一點建議
弄清楚try-catch-finally的執(zhí)行情況后我們才能正確使用它。
如果我們使用的是try-catch-finally語句塊,而我們又需要保證有異常時能夠拋出異常,那么在finally語句中就不要使用return語句了(finally語句塊的最重要的作用應該是釋放申請的資源),因為finally中的return語句會導致我們的throw e被拋棄,在這個try-catch-finally的外面將只能看到finally中的返回值(除非在finally中拋出異常)。(我們需要記?。翰粌Hthrow語句是abrupt completion 的原因,return、break、continue等這些看起來很正常的語句也是導致abrupt completion的原因。)