1. 程式人生 > >java安全編碼指南之:異常處理

java安全編碼指南之:異常處理

[toc] # 簡介 異常是java程式設計師無法避免的一個話題,我們會有JVM自己的異常也有應用程式的異常,對於不同的異常,我們的處理原則是不是一樣的呢? 一起來看看吧。 # 異常簡介 先上個圖,看一下常見的幾個異常型別。 ![](https://img-blog.csdnimg.cn/20200728105438266.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_25,color_8F8F8F,t_70) 所有的異常都來自於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點: 1. 如果當前執行緒例項在呼叫Object類的wait(),wait(long)或wait(long,int)方法或join(),join(long),join(long,int)方法,或者在該例項中呼叫了Thread.sleep(long)或Thread.sleep(long,int)方法,並且正在阻塞狀態中時,則其中斷狀態將被清除,並將收到InterruptedException。 2. 如果此執行緒在InterruptibleChannel上的I / O操作中處於被阻塞狀態,則該channel將被關閉,該執行緒的中斷狀態將被設定為true,並且該執行緒將收到java.nio.channels.ClosedByInterruptException異常。 3. 如果此執行緒在java.nio.channels.Selector中處於被被阻塞狀態,則將設定該執行緒的中斷狀態為true,並且它將立即從select操作中返回。 4. 如果上面的情況都不成立,則設定中斷狀態為true。 看下面的例子: ~~~java public void wrongInterrupted(){ try{ Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } ~~~ 上面程式碼中我們捕獲了一個InterruptedException,但是我們僅僅是打印出了異常資訊,並沒有做任何操作。這樣程式的表現和沒有傳送一異常一樣,很明顯是有問題的。 根據上面的介紹,我們知道,interrupted()方法會清除中斷狀態,所以,如果我們自身處理不了異常的情況下,需要重新呼叫Thread.currentThread().interrupt()重新丟擲中斷,由上層程式碼負責處理,如下所示。 ~~~java public void correctInterrupted(){ try{ Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } ~~~ # 不要在異常中暴露敏感資訊 遇到異常的時候,通常我們需要進行一定程度的日誌輸出,從而來定位異常。但是我們在做日誌輸出的時候,一定要注意不要暴露敏感資訊。 下表可以看到異常資訊可能會暴露的敏感資訊: ![](https://img-blog.csdnimg.cn/20200728150808660.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_25,color_8F8F8F,t_70) 除了敏感資訊之外,我們還要做好日誌資訊的安全保護。 # 在處理捕獲的異常時,需要恢復物件的初始狀態 如果我們在處理異常的時候,修改了物件中某些欄位的狀態,在捕獲異常的時候需要怎麼處理呢? ~~~java 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的邏輯沒有處理完畢,但是我們部分修改了物件的資料,這是很危險的。 實際上,我們需要一個重置: ~~~java 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。 我們舉個例子: ~~~java 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的原因有三個。 1. 使用null check的開銷要遠遠小於異常捕獲的開銷。 2. 如果在try block中有多個可能丟擲NullPointerException的語句,我們很難定位到具體的錯誤語句。 3. 最後,如果發生了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的時候不需要遵守上述規則。 當然這樣做是需要避免的。我們看一個例子: ~~~java 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,很明顯,我們的程式碼並沒有對其進行捕獲: ~~~java public static void main(String[] args) { ThrowException.undeclaredThrow( new Exception("Any checked exception")); } ~~~ 怎麼解決這個問題呢?換個思路,我們可以使用Constructor.newInstance()來替代class.newInstance()。 ~~~java 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](https://github.com/ddean2009/learn-java-base-9-to-20/tree/master/security) > 本文已收錄於 [http://www.flydean.com/java-security-code-line-exception/](http://www.flydean.com/java-security-code-line-exception/) > > 最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現! > > 歡迎關注我的公眾號:「程式那些事」,懂技術,更