Spring MVC異常統一處理(異常資訊的國際化,日誌記錄)
JAVA EE專案中,不管是對底層的資料操作,還是業務層的處理過程,還是控制層的處理,都不可避免的會遇到各種可預知的(業務異常主動丟擲)、不可預知的異常需要處理。一般dao層、service層的異常都會直接丟擲,最後由controller統一進行處理,每個過程都單獨處理異常,且要考慮到異常資訊和前端的反饋,程式碼的耦合度高,不統一,後期維護的工作也多。
同時還必須考慮異常模組和日誌模組、國際化的支援。
因此需要一種異常處理機制將異常處理解耦出來,這樣保證相關處理過程的功能單一,和系統其它模組解耦,也實現了異常資訊的統一處理和維護。
接下來以實際工作中Spring MVC實現異常的統一處理為例。
分析
首先看看Spring MVC處理異常的3中方式,進行比較,最終選用一個比較合適的方式。
- Spring MVC提供的簡單異常處理器SimpleMappingExceptionResolver;
- Spring MVC異常處理介面HandlerExceptionResolver自定義自己的異常處理器;
- @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請求產生的異常資訊無法反饋給前端。
綜合考慮,使用第二種方式進行異常統一處理方案的設計。
方案設計
首先分析下方案應該實現的需求。
需求
- 出錯頁面跳轉: 例如404頁面。基於Spring MVC,前端訪問某個頁面跳轉controller的時候出現異常的時候,跳轉到錯誤頁面。
- Ajax異常反饋: 前端通過Ajax的方式訪問controller獲取JSON資料出現異常的時候,需要將異常資訊反饋給前端。
- 異常資訊的日誌記錄: 配合日誌模組,實現異常日誌的記錄。
- 異常資訊的國際化: 配合國際化設計實現異常資訊的國際化。
設計
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反饋)結束。