1. 程式人生 > >Spring MVC異常統一處理(異常資訊的國際化,日誌記錄)

Spring MVC異常統一處理(異常資訊的國際化,日誌記錄)

       JAVA EE專案中,不管是對底層的資料操作,還是業務層的處理過程,還是控制層的處理,都不可避免的會遇到各種可預知的(業務異常主動丟擲)、不可預知的異常需要處理。一般dao層、service層的異常都會直接丟擲,最後由controller統一進行處理,每個過程都單獨處理異常,且要考慮到異常資訊和前端的反饋,程式碼的耦合度高,不統一,後期維護的工作也多。

       同時還必須考慮異常模組和日誌模組、國際化的支援。

       因此需要一種異常處理機制將異常處理解耦出來,這樣保證相關處理過程的功能單一,和系統其它模組解耦,也實現了異常資訊的統一處理和維護。

       接下來以實際工作中Spring MVC實現異常的統一處理為例。

分析

       首先看看Spring MVC處理異常的3中方式,進行比較,最終選用一個比較合適的方式。

  1.   Spring MVC提供的簡單異常處理器SimpleMappingExceptionResolver;
  2.   Spring MVC異常處理介面HandlerExceptionResolver自定義自己的異常處理器;
  3.   @ExceptionHandler註解實現異常處理;

簡單實踐

      對於第一種方式來說,使用SimpleMappingExceptionResolver能夠準確顯示定義的異常處理頁面,進行異常處理,具有整合簡單、有良好的擴充套件性,因為是基於配置的對已有的程式碼沒有侵入性等優點。但是該方法僅僅能夠獲取到異常資訊,對於其他資料的情況不適用。配置方法如下:

 <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">  
        <!-- 定義預設的異常處理頁面,當該異常型別的註冊時使用 -->  
        <property name="defaultErrorView" value="error"></property>  
        <!-- 定義異常處理頁面用來獲取異常資訊的變數名,預設名為exception -->  
        <property name="exceptionAttribute" value="ex"></property>  
        <!-- 定義需要特殊處理的異常,用類名或完全路徑名作為key,異常也頁名作為值 -->  
        <property name="exceptionMappings">  
            <props>  
                <prop key="cn.basttg.core.exception.BusinessException">error-business</prop>  
                <prop key="cn.basttg.core.exception.ParameterException">error-parameter</prop>  
      
                <!-- 這裡還可以繼續擴充套件對不同異常型別的處理 -->  
            </props>  
        </property>  
    </bean>  

      對於第二種方式,使用實現HandlerExceptionResolver介面的異常處理進行異常處理,具有整合簡單、良好的擴充套件性、對已有程式碼沒有侵入性等優點。同時由於自定義實現,我們可以在處理異常時進行額外的處理(日誌的記錄、異常資訊的國際化等)。專案實際的開發中也是使用的這種整合方案,配置如下:
<bean id="exceptionResolver"
	class="com.***.**.common.exception.PlatformMappingExceptionResolver">
        <!--配合自定義的異常解析器-->
        <property name="exceptionMappings">
		<props>
		      <prop key="com.***.**.common.exception.BusinessException">error/error</prop>
		      <prop key="java.lang.Exception">error/error</prop>
		</props>
	</property>
</bean>

      對於第三種方式,通過@ExceptionHandler註解實現異常處理,同樣十分靈活,不過這種方式需要在每個controller上都需註解,解決方案是增加一個BaseController類,使用@ExceptionHandler註解宣告異常處理,其他controller都繼承他。實現方式如下:

  public class BaseController {  
        /** 基於@ExceptionHandler異常處理 */  
        @ExceptionHandler  
        public String exp(HttpServletRequest request, Exception ex) {  
              
            request.setAttribute("ex", ex);  
              
            // 根據不同錯誤轉向不同頁面  
            if(ex instanceof BusinessException) {  
                return "error-business";  
            }else if(ex instanceof ParameterException) {  
                return "error-parameter";  
            } else {  
                return "error";  
            }  
        }  
    }  


      使用這種方法存在侵入性,而且在異常處理時也不能獲取異常以外的資料,且Ajax請求產生的異常資訊無法反饋給前端。

      綜合考慮,使用第二種方式進行異常統一處理方案的設計。

方案設計

      首先分析下方案應該實現的需求。

需求

  1.   出錯頁面跳轉: 例如404頁面。基於Spring MVC,前端訪問某個頁面跳轉controller的時候出現異常的時候,跳轉到錯誤頁面。
  2.   Ajax異常反饋: 前端通過Ajax的方式訪問controller獲取JSON資料出現異常的時候,需要將異常資訊反饋給前端。
  3.   異常資訊的日誌記錄: 配合日誌模組,實現異常日誌的記錄。
  4.   異常資訊的國際化:  配合國際化設計實現異常資訊的國際化。

設計

      1、 首先自定義異常解析器,程式碼清單如下:

package com.cisdi.ecis.common.exception;

import java.io.PrintWriter;
import java.io.StringWriter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import com.cisdi.ecis.common.utils.ExceptionI18Message;

/**
 * 
 * @author LCore
 * 
 * 平臺異常資訊跳轉、解析
 * 
 */
public class PlatformMappingExceptionResolver extends
		SimpleMappingExceptionResolver {
	static Logger logger = LoggerFactory.getLogger(PlatformMappingExceptionResolver.class);
	@Override
	protected ModelAndView doResolveException(HttpServletRequest request,
			HttpServletResponse response, Object handler, Exception ex) {

		String viewName = determineViewName(ex, request);
		// vm方式返回
		if (viewName != null) {
			if (!( request.getHeader("accept").indexOf("application/json") > -1 || ( request
					.getHeader("X-Requested-With") != null && request
					.getHeader("X-Requested-With").indexOf("XMLHttpRequest") > -1 ) )) {
				// 非非同步方式返回
				Integer statusCode = determineStatusCode(request, viewName);
				if (statusCode != null) {
					applyStatusCodeIfPossible(request, response, statusCode);
				}
				// 跳轉到提示頁面
				return getModelAndView(viewName, ex, request);
			} else {
				// 非同步方式返回
				try {
					PrintWriter writer = response.getWriter();
					writer.write(ExceptionI18Message.getLocaleMessage(ex.getMessage()));
					response.setStatus(404, ExceptionI18Message.getLocaleMessage(ex.getMessage()));
				        //將異常棧資訊記錄到日誌中
                                        logger.error(getTrace(ex)); 
					writer.flush();
				} catch ( Exception e ) {
					
					e.printStackTrace();
				}
				// 不進行頁面跳轉
				return null;

			}
		} else {
			return null;
		}
	}
	public static String getTrace(Throwable t) {
        StringWriter stringWriter= new StringWriter();
        PrintWriter writer= new PrintWriter(stringWriter);
        t.printStackTrace(writer);
        StringBuffer buffer= stringWriter.getBuffer();
        return buffer.toString();
    }
}


       2、之後在Spring MVC配置檔案中配置異常解析器對映路徑。

<!--配置異常對映路徑,ajax提示 -->
<bean id="exceptionResolver"		
        class="com.cisdi.ecis.common.exception.PlatformMappingExceptionResolver">
	<property name="exceptionMappings">
		<props>
			<prop key="com.cisdi.ecis.common.exception.BusinessException">error/error</prop>
			<prop key="java.lang.Exception">error/error</prop>
		</props>
	</property>
</bean>

        3、 異常資訊的國際化

        通過上述配置其實就已經滿足了方案需求中的大部分需求,還僅剩一個需求:異常資訊的國際化。上述程式碼中有一段程式碼:

ExceptionI18Message.getLocaleMessage(ex.getMessage()

       ExceptionI18Message就是根據當前的語言環境得到異常資訊,實現細則如下:

package com.cisdi.ecis.common.utils;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.support.RequestContext;

public class ExceptionI18Message{
   
    public static String getLocaleMessage(String key){
    	HttpServletRequest request =  ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
    	RequestContext requestContext = new RequestContext(request);
    	return requestContext.getMessage(key);
    }

}

        後端程式設計師在編碼時,可以直接丟擲業務異常,但是壓入的message應該是國際化檔案中的"key",自己在去國際化檔案中編寫多套語言的key的value。例如:

##Exception
pbs.exception.copyNode=The Exceptioninfo I18n

       之後我們壓入的異常資訊為pbs.exception.copyNode:
throw new Exception("pbs.exception.copyNode");

 測試

       到此為止,方案已經設計完畢,簡單的測試下是否滿足我們的需求吧,對於頁面跳轉的異常這裡就不在測試了,主要在於前端Ajax請求controller丟擲業務異常的時候前端是否能夠收到反饋。

       前端程式碼:

$.ajax({
                url: "${basePath}/doc/addDocMaterials",
                type: "post",
                dataType: "json",
                data: obj,
                complete: function(xhr) {
                    console.log(xhr);
                    if (xhr.status == 200 && xhr.responseText != null) {} else {
                        $.messager.alert('#springMessage("message.tip")', xhr.responseText);
                        displayLoad();
                    }
                }
});

       之後後端主動丟擲業務異常的時候,前端獲取到的反饋結果如下:(這裡我們就以上面的丟擲異常的程式碼為例)。

        到此為止,關於Spring MVC異常的統一處理方案(國際化、Ajax反饋)結束。