上一篇寫了JSON支持,請求成功時,AnnotationMethodHandlerAdapter使用messageConverters將方法返回值輸出到客戶端。
如果請求失敗呢?根本就沒有返回值,怎麼輸出?
這種情況,這需要使用spring的錯誤解析器(ExceptionResolver)。當Controller發生異常,ExceptionResolver將被調用,如此便可以對上面的情況進行處理。
我們本來想使用spring的org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,很遺憾,這個錯誤解析器有bug,而且功能有缺陷,因此需要我們自己去實現一個ExceptionResolver。
這個bug難以描述,感興趣的可以找出bug,去它WIKI提出,本人英文太爛,根本不會寫.........。
實現ExceptionResolver的思路:
目前的異常處理:大部分的做法是在Controller中進行try...catch,try中處理正常請求,catch中處理異常請求。缺點:
1、每個方法都要這麼寫,代碼重複。
2、捕獲不到Controller外部的錯誤,例如某個攔截器的錯誤,這時將無法給客戶一個有好的提示。
3、需要在catch語句塊中記錄日誌,代碼重複。
這個錯誤解析器應該有如下功能:
1、既然使用異常解析器,那麼就不必在Controller中對異常進行處理,拋出即可,簡化開發,異常統一控制。
2、ajax請求(有@ResponseBody的Controller)發生錯誤,輸出JSON。
3、頁面請求(無@ResponseBody的Controller)發生錯誤,輸出錯誤頁面。
4、它需要與AnnotationMethodHandlerAdapter使用同一個messageConverters
5、異常處理細節可控制。
請看
AnnotationHandlerMethodExceptionResolver ,這是我寫的實現類。用法:
/**
* 不必在Controller中對異常進行處理,拋出即可,由此異常解析器統一控制。<br>
* ajax請求(有@ResponseBody的Controller)發生錯誤,輸出JSON。<br>
* 頁面請求(無@ResponseBody的Controller)發生錯誤,輸出錯誤頁面。<br>
* 需要與AnnotationMethodHandlerAdapter使用同一個messageConverters<br>
* Controller中需要有專門處理異常的方法。
*
* @author dongjian
*
* */
public class AnnotationHandlerMethodExceptionResolver extends ExceptionHandlerExceptionResolver {
private String defaultErrorView;
public String getDefaultErrorView() {
return defaultErrorView;
}
public void setDefaultErrorView(String defaultErrorView) {
this.defaultErrorView = defaultErrorView;
}
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) {
if (handlerMethod == null) {
return null;
}
Method method = handlerMethod.getMethod();
if (method == null) {
return null;
}
ModelAndView returnValue = super.doResolveHandlerMethodException(request, response, handlerMethod, exception);
ResponseBody responseBodyAnn = AnnotationUtils.findAnnotation(method, ResponseBody.class);
if (responseBodyAnn != null) {
try {
ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(method, ResponseStatus.class);
if (responseStatusAnn != null) {
HttpStatus responseStatus = responseStatusAnn.value();
String reason = responseStatusAnn.reason();
if (!StringUtils.hasText(reason)) {
response.setStatus(responseStatus.value());
} else {
try {
response.sendError(responseStatus.value(), reason);
} catch (IOException e) { }
}
}
return handleResponseBody(returnValue, request, response);
} catch (Exception e) {
return null;
}
}
if(returnValue.getViewName() == null){
returnValue.setViewName(defaultErrorView);
}
return returnValue;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private ModelAndView handleResponseBody(ModelAndView returnValue, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Map value = returnValue.getModelMap();
HttpInputMessage inputMessage = new ServletServerHttpRequest(request);
List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept();
if (acceptedMediaTypes.isEmpty()) {
acceptedMediaTypes = Collections.singletonList(MediaType.ALL);
}
MediaType.sortByQualityValue(acceptedMediaTypes);
HttpOutputMessage outputMessage = new ServletServerHttpResponse(response);
Class<?> returnValueType = value.getClass();
List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters();
if (messageConverters != null) {
for (MediaType acceptedMediaType : acceptedMediaTypes) {
for (HttpMessageConverter messageConverter : messageConverters) {
if (messageConverter.canWrite(returnValueType, acceptedMediaType)) {
messageConverter.write(value, acceptedMediaType, outputMessage);
return new ModelAndView();
}
}
}
}
if (logger.isWarnEnabled()) {
logger.warn("Could not find HttpMessageConverter that supports return type [" + returnValueType + "] and " + acceptedMediaTypes);
}
return null;
}
}
------------------------web.xml-------------------------------------
spring mvc當然會自動註冊一些異常解析器,我們需要禁止自動註冊,讓其使用我們自定義的類。
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:dispatcher-servlet.xml</param-value>
</init-param>
<init-param>
<param-name>detectAllHandlerExceptionResolvers</param-name><!-- 取消其自動註冊的異常解析器 -->
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
---------------------------spring mvc配置 -------------------------------------------
<!-- 由於取消自動註冊,DispatcherServlet會在spring上下文尋找 id 爲 handlerExceptionResolver作爲異常解析器 -->
<bean id="handlerExceptionResolver" class="com.jd.spsales.common.web.AnnotationHandlerMethodExceptionResolver">
<property name="defaultErrorView" value="error.vm"/><!-- 錯誤頁面 -->
<property name="messageConverters" ref="messageConverters"/> <!--見上一篇,有定義過,標有@ResponseBody被此messageConverters輸出-->
</bean>
-------------------------------BaseController 代碼-------------------------------------------
所有Controller繼承BaseController
/**
* 異常控制,這便是異常細節可控,將來可用於支持國際化(異常信息國際化)
* */
@ExceptionHandler(Exception.class)
@ResponseStatus(value=HttpStatus.INTERNAL_SERVER_ERROR)
public ModelAndView handleException(Exception ex, HttpServletRequest request) {
return new ModelAndView().addObject("error", "錯誤信息");
}
---------------------error.vm代碼---------------------
$!{error}
OK,到現在就可以了。下面讓我們測試一下預期功能。
------------------------------測試代碼DemoController extends BaseController --------------------------------------------
@RequestMapping("/demoAjax")
@ResponseBody
public Map demoAjax(String name) {
if(StringUtils.isEmpty(name) ) throw new RuntimeException();
return Collections.singletonMap("name", name);
}
@RequestMapping("/demoPage")
public ModelAndView demoPage(String name) {
if(StringUtils.isEmpty(name) ) throw new RuntimeException();
ModelAndView mav = new ModelAndView();
mav.setViewName("demo.vm");
return mav;
}
---------------------------測試代碼之前端------------------------------
$.ajaxSetup({ //設置ajax請求全局默認設置
async : true,
error : function(jqXHR, textStatus, errorThrown){
var msg = $.parseJSON(jqXHR.responseText).error;
alert(msg);
},
traditional : true,
dataType : "json",
type : "POST"
});
$.ajax({ //請求將成功
url: "demoAjax.action",
data: {name: "you param"},
dataType : "json",
type : "POST",
success: function(data){
alert("請求發送成功,返回數據:" + data.name);
}
});
$.ajax({ //請求將失敗,彈出人性化的錯誤信息
url: "demoAjax.action",
dataType : "json",
type : "POST",
success: function(data){
alert("請求發送成功,返回數據:" + data.name);
}
});
<a href="demoPage.action?name=you name">將返回demo頁面</a>
<a href="demoPage.action">將返回錯誤頁面</a>
到此異常處理完成。
我們不必在程序中對異常進行處理,不管三七二十一,全部拋出即可,代碼大大簡化。
請求處理情況如下:
ajax請求:正常時,使用@ResponseBody輸出。錯誤時,返回錯誤的JSON串。
頁面請求:正常時進入該頁面,當請求發生異常時,返回錯誤頁面。
不用編碼處理JSON和異常處理