spring mvc 輸出 json 異常處理

上一篇寫了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和異常處理




發佈了17 篇原創文章 · 獲贊 3 · 訪問量 24萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章