簡介
異常是java程式員無法避免的一個話題,我們會有JVM自己的異常也有應用程式的異常,對于不同的異常,我們的處理原則是不是一樣的呢?
一起來看看吧。
異常簡介
先上個圖,看一下常見的幾個異常類型。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38FdsYEOGhjR48lcvx2bjxSNy8VZ6l2cs0TPRJmdO1GT1Z0VatGbIJWb1kHZzQ2MMZ3bENGMShUYfRHelRHLw81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL2YjM4MDN1ATM4IzNwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
所有的異常都來自于Throwable。Throwable有兩個子類,Error和Exception。
Error通常表示的是嚴重錯誤,這些錯誤是不建議被catch的。
注意這裡有一個例外,比如ThreadDeath也是繼承自Error,但是它表示的是線程的死亡,雖然不是嚴重的異常,但是因為應用程式通常不會對這種異常進行catch,是以也歸類到Error中。
Exception表示的是應用程式希望catch住的異常。
在Exception中有一個很特别的異常叫做RuntimeException。RuntimeException叫做運作時異常,是不需要被顯示catch住的,是以也叫做unchecked Exception。而其他非RuntimeException的Exception則需要顯示try catch,是以也叫做checked Exception。
不要忽略checked exceptions
我們知道checked exceptions是一定要被捕獲的異常,我們在捕獲異常之後通常有兩種處理方式。
第一種就是按照業務邏輯處理異常,第二種就是本身并不處理異常,但是将異常再次抛出,由上層代碼來處理。
如果捕獲了,但是不處理,那麼就是忽略checked exceptions。
接下來我們來考慮一下java中線程的中斷異常。
java中有三個非常相似的方法interrupt,interrupted和isInterrupted。
isInterrupted()隻會判斷是否被中斷,而不會清除中斷狀态。
interrupted()是一個類方法,調用isInterrupted(true)判斷的是目前線程是否被中斷。并且會清除中斷狀态。
前面兩個是判斷是否中斷的方法,而interrupt()就是真正觸發中斷的方法。
它的工作要點有下面4點:
- 如果目前線程執行個體在調用Object類的wait(),wait(long)或wait(long,int)方法或join(),join(long),join(long,int)方法,或者在該執行個體中調用了Thread.sleep(long)或Thread.sleep(long,int)方法,并且正在阻塞狀态中時,則其中斷狀态将被清除,并将收到InterruptedException。
- 如果此線程在InterruptibleChannel上的I / O操作中處于被阻塞狀态,則該channel将被關閉,該線程的中斷狀态将被設定為true,并且該線程将收到java.nio.channels.ClosedByInterruptException異常。
- 如果此線程在java.nio.channels.Selector中處于被被阻塞狀态,則将設定該線程的中斷狀态為true,并且它将立即從select操作中傳回。
- 如果上面的情況都不成立,則設定中斷狀态為true。
看下面的例子:
public void wrongInterrupted(){
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
上面代碼中我們捕獲了一個InterruptedException,但是我們僅僅是列印出了異常資訊,并沒有做任何操作。這樣程式的表現和沒有發送一異常一樣,很明顯是有問題的。
根據上面的介紹,我們知道,interrupted()方法會清除中斷狀态,是以,如果我們自身處理不了異常的情況下,需要重新調用Thread.currentThread().interrupt()重新抛出中斷,由上層代碼負責處理,如下所示。
public void correctInterrupted(){
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
不要在異常中暴露敏感資訊
遇到異常的時候,通常我們需要進行一定程度的日志輸出,進而來定位異常。但是我們在做日志輸出的時候,一定要注意不要暴露敏感資訊。
下表可以看到異常資訊可能會暴露的敏感資訊:
除了敏感資訊之外,我們還要做好日志資訊的安全保護。
在處理捕獲的異常時,需要恢複對象的初始狀态
如果我們在處理異常的時候,修改了對象中某些字段的狀态,在捕獲異常的時候需要怎麼處理呢?
private int age=30;
public void wrongRestore(){
try{
age=20;
throw new IllegalStateException("custom exception!");
}catch (IllegalStateException e){
System.out.println("we do nothing");
}
}
上面的例子中,我們将age重置為20,然後抛出了異常。雖然抛出了異常,但是我們并沒有重置age,最後導緻age最終被修改了。
整個restore的邏輯沒有處理完畢,但是我們部分修改了對象的資料,這是很危險的。
實際上,我們需要一個重置:
public void rightRestore(){
try{
age=20;
throw new IllegalStateException("custom exception!");
}catch (IllegalStateException e){
System.out.println("we do nothing");
age=30;
}
}
不要手動完成finally block
我們在使用try-finally和try-catch-finally語句時,一定不要在finally block中使用return, break, continue或者throw語句。
為什麼呢?
根據Java Language Specification(JLS)的說明,finally block一定會被執行,不管try語句中是否抛出異常。
在try-finally和try-catch-finally語句中,如果try語句中抛出了異常R,然後finally block被執行,這時候有兩種情況:
- 如果finally block正常執行,那麼try語句被終止的原因是異常R。
- 如果在finally block中抛出了異常S,那麼try語句被終止的原因将會變成S。
我們舉個例子:
public class FinallyUsage {
public boolean wrongFinally(){
try{
throw new IllegalStateException("my exception!");
}finally {
System.out.println("Code comes to here!");
return true;
}
}
public boolean rightFinally(){
try{
throw new IllegalStateException("my exception!");
}finally {
System.out.println("Code comes to here!");
}
}
public static void main(String[] args) {
FinallyUsage finallyUsage=new FinallyUsage();
finallyUsage.wrongFinally();
finallyUsage.rightFinally();
}
}
上面的例子中,我們定義了兩個方法,一個方法中我們在finally中直接return,另一方法中,我們讓finally正常執行完畢。
最終,我們可以看到wrongFinally将異常隐藏了,而rightFinally保留了try的異常。
同樣的,如果我們在finally block中抛出了異常,我們一定要記得對其進行捕獲,否則将會隐藏try block中的異常資訊。
不要捕獲NullPointerException和它的父類異常
通常來說NullPointerException表示程式代碼有邏輯錯誤,是需要程式員來進行代碼邏輯修改,進而進行修複的。
比如說加上一個null check。
不捕獲NullPointerException的原因有三個。
- 使用null check的開銷要遠遠小于異常捕獲的開銷。
- 如果在try block中有多個可能抛出NullPointerException的語句,我們很難定位到具體的錯誤語句。
- 最後,如果發生了NullPointerException,程式基本上不可能正常運作或者恢複,是以我們需要提前進行null check的判斷。
同樣的,程式也不要對NullPointerException的父類RuntimeException, Exception, or Throwable進行捕捉。
不要throw RuntimeException, Exception, or Throwable
我們抛出異常主要是為了能夠找到準确的處理異常的方法,如果直接抛出RuntimeException, Exception, 或者 Throwable就會導緻程式無法準确處理特定的異常。
通常來說我們需要自定義RuntimeException, Exception, 或者 Throwable的子類,通過具體的子類來區分具體的異常類型。
不要抛出未聲明的checked Exception
一般來說checked Exception是需要顯示catch住,或者在調用方法上使用throws做申明的。
但是我們可以通過某些手段來繞過這種限制,進而在使用checked Exception的時候不需要遵守上述規則。
當然這樣做是需要避免的。我們看一個例子:
private static Throwable throwable;
private ThrowException() throws Throwable {
throw throwable;
}
public static synchronized void undeclaredThrow(Throwable throwable) {
ThrowException.throwable = throwable;
try {
ThrowException.class.newInstance();
} catch (InstantiationException e) {
} catch (IllegalAccessException e) {
} finally {
ThrowException.throwable = null;
}
}
上面的例子中,我們定義了一個ThrowException的private構造函數,這個構造函數會throw一個throwable,這個throwable是從方法傳入的。
在undeclaredThrow方法中,我們調用了ThrowException.class.newInstance()執行個體化一個ThrowException執行個體,因為需要調用構造函數,是以會抛出傳入的throwable。
因為Exception是throwable的子類,如果我們在調用的時候傳入一個checked Exception,很明顯,我們的代碼并沒有對其進行捕獲:
public static void main(String[] args) {
ThrowException.undeclaredThrow(
new Exception("Any checked exception"));
}
怎麼解決這個問題呢?換個思路,我們可以使用Constructor.newInstance()來替代class.newInstance()。
try {
Constructor constructor =
ThrowException.class.getConstructor(new Class<?>[0]);
constructor.newInstance();
} catch (InstantiationException e) {
} catch (InvocationTargetException e) {
System.out.println("catch exception!");
} catch (NoSuchMethodException e) {
} catch (IllegalAccessException e) {
} finally {
ThrowException.throwable = null;
}
上面的例子,我們使用Constructor的newInstance方法來建立對象的執行個體。和class.newInstance不同的是,這個方法會抛出InvocationTargetException異常,并且把所有的異常都封裝進去。
是以,這次我們獲得了一個checked Exception。
本文的代碼:
learn-java-base-9-to-20/tree/master/security本文已收錄于 http://www.flydean.com/java-security-code-line-exception/最通俗的解讀,最深刻的幹貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!
歡迎關注我的公衆号:「程式那些事」,懂技術,更懂你!