SpringMVC全局異常統一處理以及處理順序
最近在使用SpringMVC做全局異常統一處理的時候遇到的問題,就是想把ajax請求和普通的網頁請求分開返回json錯誤信息或者跳轉到錯誤頁。
在實際做的時候先按照標準的方式自定義一個HandlerExceptionResolver,命名爲SpringHandlerExceptionResolver,實現HandlerExceptionResolver接口,重寫resolveException方法,具體實現如下:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonJsonView;
import com.butioy.common.bean.JsonResult;
import com.butioy.common.exception.BaseSystemException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
/**
* <p>
* spring MVC 統一異常處理
* </p>
*
* @author butioy
*/
public class SpringHandlerExceptionResolver implements HandlerExceptionResolver {
private static Logger logger = LoggerFactory.getLogger(SpringHandlerExceptionResolver.class);
private FastJsonConfig fastJsonConfig;
@Autowired
public SpringHandlerExceptionResolver(FastJsonConfig fastJsonConfig) {
this.fastJsonConfig = fastJsonConfig;
}
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView mv = specialExceptionResolve(ex, request);
if (null == mv) {
String message = "系統異常,請聯繫管理員";
//BaseSystemException是我自定義的異常基類,繼承自RuntimeException
if (ex instanceof BaseSystemException) {
message = ex.getMessage();
}
mv = errorResult(message, "/error", request);
}
return mv;
}
/**
* 這個方法是拷貝 {@link org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver#doResolveException},
* 加入自定義處理,實現對400, 404, 405, 406, 415, 500(參數問題導致), 503的處理
*
* @param ex 異常信息
* @param request 當前請求對象(用於判斷當前請求是否爲ajax請求)
* @return 視圖模型對象
*/
private ModelAndView specialExceptionResolve(Exception ex, HttpServletRequest request) {
try {
if (ex instanceof NoSuchRequestHandlingMethodException
|| ex instanceof NoHandlerFoundException) {
return result(HttpExceptionEnum.NOT_FOUND_EXCEPTION, request);
}
else if (ex instanceof HttpRequestMethodNotSupportedException) {
return result(HttpExceptionEnum.NOT_SUPPORTED_METHOD_EXCEPTION, request);
}
else if (ex instanceof HttpMediaTypeNotSupportedException) {
return result(HttpExceptionEnum.NOT_SUPPORTED_MEDIA_TYPE_EXCEPTION, request);
}
else if (ex instanceof HttpMediaTypeNotAcceptableException) {
return result(HttpExceptionEnum.NOT_ACCEPTABLE_MEDIA_TYPE_EXCEPTION, request);
}
else if (ex instanceof MissingPathVariableException) {
return result(HttpExceptionEnum.NOT_SUPPORTED_METHOD_EXCEPTION, request);
}
else if (ex instanceof MissingServletRequestParameterException) {
return result(HttpExceptionEnum.MISSING_REQUEST_PARAMETER_EXCEPTION, request);
}
else if (ex instanceof ServletRequestBindingException) {
return result(HttpExceptionEnum.REQUEST_BINDING_EXCEPTION, request);
}
else if (ex instanceof ConversionNotSupportedException) {
return result(HttpExceptionEnum.NOT_SUPPORTED_CONVERSION_EXCEPTION, request);
}
else if (ex instanceof TypeMismatchException) {
return result(HttpExceptionEnum.TYPE_MISMATCH_EXCEPTION, request);
}
else if (ex instanceof HttpMessageNotReadableException) {
return result(HttpExceptionEnum.MESSAGE_NOT_READABLE_EXCEPTION, request);
}
else if (ex instanceof HttpMessageNotWritableException) {
return result(HttpExceptionEnum.MESSAGE_NOT_WRITABLE_EXCEPTION, request);
}
else if (ex instanceof MethodArgumentNotValidException) {
return result(HttpExceptionEnum.NOT_VALID_METHOD_ARGUMENT_EXCEPTION, request);
}
else if (ex instanceof MissingServletRequestPartException) {
return result(HttpExceptionEnum.MISSING_REQUEST_PART_EXCEPTION, request);
}
else if (ex instanceof BindException) {
return result(HttpExceptionEnum.BIND_EXCEPTION, request);
}
else if (ex instanceof AsyncRequestTimeoutException) {
return result(HttpExceptionEnum.ASYNC_REQUEST_TIMEOUT_EXCEPTION, request);
}
} catch (Exception handlerException) {
logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException);
}
return null;
}
/**
* 判斷是否ajax請求
*
* @param request 請求對象
* @return true:ajax請求 false:非ajax請求
*/
private boolean isAjax(HttpServletRequest request) {
return "XMLHttpRequest".equalsIgnoreCase(request.getHeader("X-Requested-With"));
}
/**
* 返回錯誤信息
*
* @param message 錯誤信息
* @param url 錯誤頁url
* @param request 請求對象
* @return 模型視圖對象
*/
private ModelAndView errorResult(String message, String url, HttpServletRequest request) {
logger.warn("請求處理失敗,請求url=[{}], 失敗原因 : {}", request.getRequestURI(), message);
if (isAjax(request)) {
return jsonResult(500, message);
} else {
return normalResult(message, url);
}
}
/**
* 返回異常信息
*
* @param httpException 異常信息
* @param request 請求對象
* @return 模型視圖對象
*/
private ModelAndView result(HttpExceptionEnum httpException, HttpServletRequest request) {
logger.warn("請求處理失敗,請求url=[{}], 失敗原因 : {}", request.getRequestURI(), httpException.getMessage());
if (isAjax(request)) {
return jsonResult(httpException.getCode(), httpException.getMessage());
} else {
return normalResult(httpException.getMessage(), "/error");
}
}
/**
* 返回錯誤頁
*
* @param message 錯誤信息
* @param url 錯誤頁url
* @return 模型視圖對象
*/
private ModelAndView normalResult(String message, String url) {
Map<String, String> model = new HashMap<String, String>();
model.put("errorMessage", message);
return new ModelAndView(url, model);
}
/**
* 返回錯誤數據
*
* @param message 錯誤信息
* @return 模型視圖對象
*/
private ModelAndView jsonResult(int code, String message) {
ModelAndView mv = new ModelAndView();
FastJsonJsonView view = new FastJsonJsonView();
view.setFastJsonConfig(fastJsonConfig);
view.setAttributesMap((JSONObject) JSON.toJSON(JsonResult.fail(code, message)));
mv.setView(view);
return mv;
}
}
寫好之後,在springContext.xml配置文件中配置一下
<bean class="com.butioy.common.handler.SpringHandlerExceptionResolver"/>
然後啓動tomcat,一切正常沒有錯誤信息,但是在請求的時候並沒有按照我的猜想返回自定義的錯誤頁,而是tomcat默認的錯誤頁
於是我就debugger跟蹤一下代碼執行情況。終於讓我給發現了在Spring的DispatcherServlet類的processHandlerException方法中有一個handlerExceptionResolvers集合,這裏面存放着聲明的異常處理bean。
這時會發現這裏面有3個異常處理bean是我們沒有聲明的,查了資料才知道原來這3個bean是SpringMVC默認初始化的,在spring-webmvc的jar包中,跟DispatcherServlet.java同一包下的DispatcherServlet.properties配置文件,配置文件內容如下:
雖然有3個默認的bean, 但是爲什麼優先級就高於我自定義的呢?於是我點開這些類看了一下。發現這些了都間接實現了Spring的Ordered接口。這個接口只有一個方法getOrder(),這個方法返回一個int類型值,該值就是表明這個bean的執行的優先級。上面的統一異常處理實現類的註釋中也有提過,是拷貝了DefaultHandlerExceptionResolver#doResolveException方法,這裏就知道上面的錯誤信息已經被這個類處理類。在上面debugger跟蹤時, 我們知道DefaultHandlerExceptionResolver實例的bean的執行順序是優先於我們自定義的SpringHandlerExceptionResolver的bean,所以那些404,415等錯誤信息一致會被SpringMVC默認的異常處理bean處理。這跟我們的初衷不符。於是我把SpringHandlerExceptionResolver改造了一下,也實現Ordered接口:
這裏我默認給它一個最高優先級,這樣就可以讓自定義的類優先執行了。果然,結果跟我預期一樣。
這裏是返回錯誤頁面:
這裏是json返回數據(我這裏使用的是jsonp請求方式):
至此,整個SpringMVC全局異常處理就完成了。這裏我只是把ajax請求都當成請求json數據,所以就這樣做了。實際依情況而定。
注意: 使用這個方法的時候切記不要在要在spring-mvc.xml中配置 <mvc:default-servlet-handler/>,應爲這會使得配置的SpringHandlerExceptionResolver對404錯誤不起作用。在DispatcherServlet類中的doDispatch方法中我們會發現一行代碼
// Determine handler for the current request. mappedHandler = getHandler(processedRequest); if (mappedHandler == null || mappedHandler.getHandler() == null) { noHandlerFound(processedRequest, response); return; }
這裏的getHandler()會獲取一個handler,如果沒有與url對應handler的就會獲取到
<mvc:default-servlet-handler/>聲明的一個
org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler。
這樣就不會執行noHandlerFound方法,也就不會拋出404異常了。
還有一點需要注意,就是在web.xml配置文件中,在配置DispatcherServlet的時候要加上
<init-param> <!-- 如果未發現映射路徑,拋出異常,而不是跳轉到在web.xml配置的404錯誤頁 --> <param-name>throwExceptionIfNoHandlerFound</param-name> <param-value>true</param-value> </init-param>
因爲如果不加上這個配置,在發生404的時候,就不會拋出異常,而是返回一個404狀態的響應了
下面是DispatcherServlet.java的源碼一部分:
protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception { if (pageNotFoundLogger.isWarnEnabled()) { pageNotFoundLogger.warn("No mapping found for HTTP request with URI [" + getRequestUri(request) + "] in DispatcherServlet with name '" + getServletName() + "'"); } if (this.throwExceptionIfNoHandlerFound) { throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request), new ServletServerHttpRequest(request).getHeaders()); } else { response.sendError(HttpServletResponse.SC_NOT_FOUND); } }
在這裏可以看到,如果throwExceptionIfNoHandlerFound爲false,不會拋出異常,而是會給瀏覽器一個404狀態的響應。而DispatcherServlet.java中這個屬性的默認值就是false。