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日誌,然後在頁面以友好的錯誤提示告訴使用者出錯了。大家看下面的程式碼:
Java程式碼- //建立日誌物件
- 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";
- }
然後在異常攔截器對異常進行處理,看下面的程式碼:
Java程式碼- 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頁面顯示具體的錯誤訊息即可:
Java程式碼- <body>
- <s:if test="%{#request.errorMsg==null}">
- <p>對不起,系統發生了未知的錯誤</p>
- </s:if>
- <s:else>
- <p>${requestScope.errorMsg}</p>
- </s:else>
- </body>
以上方式可以攔截後臺程式碼所有的異常,但如果出現數據庫連線異常時不能被捕獲的,大家可以使用struts2的全域性異常處理機制來處理:
Java程式碼- <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是這樣做的:
Java程式碼- /**
- * 讀取檔案,獲取對應錯誤訊息
- */
- 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;
- }
Java程式碼
- //~ Methods ========================================================================================================
- @ExceptionHandler()
- public @ResponseBody String handle(Exception exception, HttpServletRequest request, HttpServletResponse response) {
- logger.error(request.getRequestURI() + " 請求失敗", exception);
- ResponseData data = new ResponseData(false, exception.getClass() + ": " + exception.getMessage());
- data.setRequestURI(request.getRequestURI());
- StackTraceElement[] trace = exception.getStackTrace();
- StringBuilder traceContent = new StringBuilder();
- for (int i=0; i < trace.length; i++)
- traceContent.append("\tat " + trace[i]);
- data.setExecptionTrace(traceContent.toString());
- String json = "";
- try {
- json = mapper.writeValueAsString(data);
- } catch (Exception e1) {
- e1.printStackTrace();
- }
- if(!(request.getRequestURI().endsWith(".json") || request.getRequestURI().endsWith("Json")))
- throw new RuntimeException(exception);
- response.setStatus(500);//服務端處理失敗
- response.setContentType("application/json;charset=UTF-8");
- return json;
- }
如果是ajax請求,提示處理:
Java程式碼
- //Ajax請求完成執行。判斷Session超時,如果超時或者無效,返回的內容為login.jsp頁面的內容,頁面包含:AJAX-AccessDeniedException
- //如果頁面包含AJAX-AccessDeniedException,說明session超時或者無效。
- Ext.Ajax.on('requestcomplete', function(conn, response, options){
- if(options.params['REQUEST_MODE'] == "AJAX" &&
- response.responseText.indexOf("AJAX-AccessDeniedException") != -1) {
- Ext.Msg.alert('提示', '會話超時,請重新登入!', function(){
- window.location = './login';
- });
- }
- });
- Ext.Ajax.on('requestexception', function(conn, response, options){
- //ajax請求,出現異常,彈出視窗提示資訊。
- //var json = response.responseText.replace("</generated></generated>", "");
- var json = response.responseText;
- var data = Ext.decode(json);
- Ext.Msg.alert('提示', "請求URI:"+ data.requestURI + "<br>錯誤資訊:"+
- data.message + "<br><a href='#'>錯誤堆疊資訊</a>", function(){
- });
- return false;
- });