Java Web項目整體異常處理機制
在實際的j2ee項目中,系統內部難免會出現一些異常,如果把異常放任不管直接打印到瀏覽器可能會讓用戶感覺莫名其妙,也有可能讓某些用戶找到破解系統的方法。
出來工作一年時間了,我也大概對異常處理有了一些了解,在這呢小弟簡單介紹下個人對異常處理的見解,拋磚引玉,希望各位大神提出寶貴的意見和建議。
就拿spring+struts2+hibernate項目說明:通常一個頁面請求到后臺以后,首先是到action(也就是所謂mvc的 controller),在action層會調用業務邏輯service,servce層會調用持久層dao獲取數據。最后執行結果會匯總到 action,然后通過action控制轉發到指定頁面,執行流程如下圖所示:
而這三層其實都有可能發生異常,比如dao層可能會有SQLException,service可能會有 NullPointException,action可能會有IOException,一但發生異常并且程序員未做處理,那么該層不會再往下執行,而是向 調用自己的方法拋出異常,如果dao、service、action層都未處理異常的話,異常信息會拋到服務器,然后服務器會把異常直接打印到頁面,結果 就會如下圖所示:
其實這種錯誤對于客戶來說毫無意義,因為他們通常是看不懂這是什么意思的。
剛學java的 時候,我們處理異常通常兩種方法:①直接throws,放任不管;②寫try...catch,在catch塊中不作任何操作,或者僅僅 printStackTrace()把異常打印到控制臺。第一種方法最后就造就了上圖的結果;而第二種方法更杯具:頁面不報錯,但是也不執行用戶的請求, 簡單的說,其實這就是bug(委婉點:通常是這樣)!
那么發生異常到底應該怎么辦呢?我想在大家對java異常有一定了解以后,會知道:異常應該在action控制轉發之前盡量處理,同時記錄log日志,然后在頁面以友好的錯誤提示告訴用戶出錯了。大家看下面的代碼:
//創建日志對象 Log log = LogFactory.getLog(this.getClass()); //action層執行數據添加操作 public String save(){ try{ //調用service的save方法 service.save(obj); }catch(Exception e){ log.error(...); //記錄log日志 return "error"; 到指定error頁面 } return "success"; } |
如果按照上面的方式處理異常以后,我們用戶最后看到的頁面可能就會是下面這種形式(我想這種錯誤提示應該稍微友好點了吧):
然后我們回到剛才處理異常的地方,如果大家積累了一些項目經驗以后會發現使用上面那種處理異常的方式可能還不夠靈活:
①因為spring把大多數非運行時異常都轉換成運行時異常(RuntimeException)最后導致程序員根本不知道什么地方應該進行try...catch操作
②每個方法都重復寫try...catch,而且catch塊內的代碼都很相似,這明顯做了很多重復工作而且還很容易出錯,同時也加大了單元測試的用例數(項目經理通常喜歡根據代碼行來估算UT case)
③發生異常有很多種情況:可能有數據庫增刪改查錯誤,可能是文件讀寫錯誤,等等。用戶覺得每次發生異常都是“訪問過程中產生錯誤,請重試”的提示完全不能說明錯誤情況,他們希望讓異常信息更詳盡些,比如:在執行數據刪除時發生錯誤,這樣他們可以更準確地給維護人員提供bug信息。
如何解決上面的問題呢?我是這樣做的:JDK異常或自定義異常+異常攔截器
struts2攔截器的作用在網上有很多資料,在此不再贅述,我的異常攔截器原理如下圖所示:
首先我的action類、service類和dao類如果有必要捕獲異常,我都會try...catch,catch塊內不記錄log,通常是拋出一個新異常,并且注明錯誤信息:
//action層執行數據添加操作 public String save(){ try{ //調用service的save方法 service.save(obj); }catch(Exception e){ //你問我為什么拋出Runtime異常?因為我懶得在方法后寫throws xx throw new RuntimeException("添加數據時發生錯誤!",e); } return "success"; } |
然后在異常攔截器對異常進行處理,看下面的代碼:
public String intercept(ActionInvocation actioninvocation) {
String result = null; // Action的返回值
try {
// 運行被攔截的Action,期間如果發生異常會被catch住
result = actioninvocation.invoke();
return result;
} catch (Exception e) {
/**
* 處理異常
*/
String errorMsg = "未知錯誤!";
//通過instanceof判斷到底是什么異常類型
if (e instanceof BaseException) {
BaseException be = (BaseException) e;
be.printStackTrace(); //開發時打印異常信息,方便調試
if(be.getMessage()!=null||Constants.BLANK.equals(be.getMessage().trim())){
//獲得錯誤信息
errorMsg = be.getMessage().trim();
}
} else if(e instanceof RuntimeException){
//未知的運行時異常
RuntimeException re = (RuntimeException)e;
re.printStackTrace();
} else{
//未知的嚴重異常
e.printStackTrace();
}
//把自定義錯誤信息
HttpServletRequest request = (HttpServletRequest) actioninvocation
.getInvocationContext().get(StrutsStatics.HTTP_REQUEST);
/**
* 發送錯誤消息到頁面
*/
request.setAttribute("errorMsg", errorMsg);
/**
* log4j記錄日志
*/
Log log = LogFactory
.getLog(actioninvocation.getAction().getClass());
if (e.getCause() != null){
log.error(errorMsg, e);
}else{
log.error(errorMsg, e);
}
return "error";
}// ...end of catch
}
需要注意的是:在使用instanceof判斷異常類型的時候一定要從子到父依次找,比如BaseException繼承與RuntimeException,則必須首先判斷是否是BaseException再判斷是否是RuntimeException。
最后在error JSP頁面顯示具體的錯誤消息即可:
<body> <s:if test="%{#request.errorMsg==null}"> <p>對不起,系統發生了未知的錯誤</p> </s:if> <s:else> <p>${requestScope.errorMsg}</p> </s:else> </body> |
以上方式可以攔截后臺代碼所有的異常,但如果出現數據庫連接異常時不能被捕獲的,大家可以使用struts2的全局異常處理機制來處理:
<global-results> <result name="error" >/Web/common/page/error.jsp</result> </global-results> <global-exception-mappings> <exception-mapping result="error" exception="java.lang.Exception"></exception-mapping> </global-exception-mappings> |
上面這是一個很簡單的異常攔截器,大家可以使用自定義異常,那樣會更靈活一些。
以上異常攔截器可以使用其它很多技術替換:比如spring aop,servlet filter等,根據項目實際情況處理。
【補充】ajax也可以進行攔截,但是因為ajax屬于異步操作,action通過response形式直接把數據返回給ajax回調函數,如果發生異常,ajax是不會執行頁面跳轉的,所以必須把錯誤信息返回給回調函數,我針對json數據的ajax是這樣做的:
/** * 讀取文件,獲取對應錯誤消息 */ HttpServletResponse response = (HttpServletResponse)actioninvocation.getInvocationContext().get(StrutsStatics.HTTP_RESPONSE); response.setCharacterEncoding(Constants.ENCODING_UTF8); /** * 發送錯誤消息到頁面 */ PrintWriter out; try { out = response.getWriter(); Message msg = new Message(errorMsg); //把異常信息轉換成json格式返回給前臺 out.print(JSONObject.fromObject(msg).toString()); } catch (IOException e1) { throw e; } |
以上是個人拙見,勿拍磚,謝謝。