springmvc 通过异常增强返回给客户端统一格式
在springmvc开发中,我们经常遇到这样的问题;逻辑正常执行时返回客户端指定格式的数据,比如json,但是遇NullPointerException空指针异常,NoSuchMethodException调用的方法不存在异常,返回给客户端的是服务端异常堆栈信息,导致客户端不能正常解析数据;这明显不是我们想要的。
幸好从spring3.2提供的新注解@ControllerAdvice,从名字上可以看出大体意思是控制器增强。原理是使用AOP对Controller控制器进行增强(前置增强、后置增强、环绕增强,AOP原理请自行查阅);那么我没可以自行对控制器的方法进行调用前(前置增强)和调用后(后置增强)的处理。
spring提供了@ExceptionHandler异常增强注解。程序如果在执行控制器方法前或执行时抛出异常,会被@ExceptionHandler注解了的方法处理。
配置applicationContext-mvc.xml:
<!-- 使用Annotation自动注册Bean,扫描@Controller和@ControllerAdvice--> <context:component-scan base-package="com.drskj.apiservice" use-default-filters="false"> <!-- base-package 如果多个,用“,”分隔 --> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" /> <!--控制器增强,使一个Contoller成为全局的异常处理类,类中用@ExceptionHandler方法注解的方法可以处理所有Controller发生的异常--> <context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice" /> </context:component-scan>
全局异常处理类:
package com.drskj.apiservice.handler;
import java.io.IOException;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;import com.drskj.apiservice.common.utils.ReturnFormat;
/**
- 异常增强,以JSON的形式返回给客服端
* 异常增强类型:NullPointerException,RunTimeException,ClassCastException,
NoSuchMethodException,IOException,IndexOutOfBoundsException
以及springmvc自定义异常等,如下:
SpringMVC自定义异常对应的status code
Exception HTTP Status Code ConversionNotSupportedException 500 (Internal Server Error) HttpMessageNotWritableException 500 (Internal Server Error) HttpMediaTypeNotSupportedException 415 (Unsupported Media Type) HttpMediaTypeNotAcceptableException 406 (Not Acceptable) HttpRequestMethodNotSupportedException 405 (Method Not Allowed) NoSuchRequestHandlingMethodException 404 (Not Found) TypeMismatchException 400 (Bad Request) HttpMessageNotReadableException 400 (Bad Request) MissingServletRequestParameterException 400 (Bad Request) * */ @ControllerAdvice public class RestExceptionHandler{
//运行时异常
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public String runtimeExceptionHandler(RuntimeException runtimeException) {
return ReturnFormat.retParam(1000, null);
}
//空指针异常
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public String nullPointerExceptionHandler(NullPointerException ex) {
ex.printStackTrace();
return ReturnFormat.retParam(1001, null);
}
//类型转换异常
@ExceptionHandler(ClassCastException.class)
@ResponseBody
public String classCastExceptionHandler(ClassCastException ex) {
ex.printStackTrace();
return ReturnFormat.retParam(1002, null);
}
//IO异常 @ExceptionHandler(IOException.class) @ResponseBody public String iOExceptionHandler(IOException ex) { ex.printStackTrace(); return ReturnFormat.retParam(1003, null); } //未知方法异常
@ExceptionHandler(NoSuchMethodException.class)
@ResponseBody
public String noSuchMethodExceptionHandler(NoSuchMethodException ex) {
ex.printStackTrace();
return ReturnFormat.retParam(1004, null);
}
//数组越界异常
@ExceptionHandler(IndexOutOfBoundsException.class)
@ResponseBody
public String indexOutOfBoundsExceptionHandler(IndexOutOfBoundsException ex) {
ex.printStackTrace();
return ReturnFormat.retParam(1005, null);
}
//400错误
@ExceptionHandler({HttpMessageNotReadableException.class})
@ResponseBody
public String requestNotReadable(HttpMessageNotReadableException ex){
System.out.println("400..requestNotReadable");
ex.printStackTrace();
return ReturnFormat.retParam(400, null);
}
//400错误
@ExceptionHandler({TypeMismatchException.class})
@ResponseBody
public String requestTypeMismatch(TypeMismatchException ex){
System.out.println("400..TypeMismatchException");
ex.printStackTrace();
return ReturnFormat.retParam(400, null);
}
//400错误
@ExceptionHandler({MissingServletRequestParameterException.class})
@ResponseBody
public String requestMissingServletRequest(MissingServletRequestParameterException ex){
System.out.println("400..MissingServletRequest");
ex.printStackTrace();
return ReturnFormat.retParam(400, null);
}
//405错误
@ExceptionHandler({HttpRequestMethodNotSupportedException.class})
@ResponseBody
public String request405(){
System.out.println("405...");
return ReturnFormat.retParam(405, null);
}
//406错误 @ExceptionHandler({HttpMediaTypeNotAcceptableException.class}) @ResponseBody public String request406(){ System.out.println("404..."); return ReturnFormat.retParam(406, null); } //500错误
@ExceptionHandler({ConversionNotSupportedException.class,HttpMessageNotWritableException.class})
@ResponseBody
public String server500(RuntimeException runtimeException){
System.out.println("500...");
return ReturnFormat.retParam(406, null);
}
}
以上包括了常见的服务端异常类型,@ResponseBody表示以json格式返回客户端数据。我们也可以自定义异常类(这里我把它叫做MyException)并且继承RunTimeException,并且在全局异常处理类新增一个方法来处理异常,使用@ExceptionHandler(MyException.class)注解在方法上实现自定义异常增强。
格式化response数据类ReturnFormat:
package com.drskj.apiservice.common.utils;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;import com.alibaba.fastjson.JSON;import com.google.common.collect.Maps;
//格式化返回客户端数据格式(json)
public class ReturnFormat {
private static Map<String,String>messageMap = Maps.newHashMap();
//初始化状态码与文字说明
static {
messageMap.put("0", "");messageMap.put(</span>"400", "Bad Request!"<span style="color: #000000;">);</br> messageMap.put(</span>"401", "NotAuthorization"<span style="color: #000000;">);</br> messageMap.put(</span>"405", "Method Not Allowed"<span style="color: #000000;">);</br> messageMap.put(</span>"406", "Not Acceptable"<span style="color: #000000;">);</br> messageMap.put(</span>"500", "Internal Server Error"<span style="color: #000000;">);</br></br> messageMap.put(</span>"1000", "[服务器]运行时异常"<span style="color: #000000;">);</br> messageMap.put(</span>"1001", "[服务器]空值异常"<span style="color: #000000;">);</br> messageMap.put(</span>"1002", "[服务器]数据类型转换异常"<span style="color: #000000;">);</br> messageMap.put(</span>"1003", "[服务器]IO异常"<span style="color: #000000;">);</br> messageMap.put(</span>"1004", "[服务器]未知方法异常"<span style="color: #000000;">);</br> messageMap.put(</span>"1005", "[服务器]数组越界异常"<span style="color: #000000;">);</br> messageMap.put(</span>"1006", "[服务器]网络异常"<span style="color: #000000;">);</br></br> messageMap.put(</span>"1010", "用户未注册"<span style="color: #000000;">);</br> messageMap.put(</span>"1011", "用户已注册"<span style="color: #000000;">);</br> messageMap.put(</span>"1012", "用户名或密码错误"<span style="color: #000000;">);</br> messageMap.put(</span>"1013", "用户帐号冻结"<span style="color: #000000;">);</br> messageMap.put(</span>"1014", "用户信息编辑失败"<span style="color: #000000;">);</br> messageMap.put(</span>"1015", "用户信息失效,请重新获取"<span style="color: #000000;">);</br></br> messageMap.put(</span>"1020", "验证码发送失败"<span style="color: #000000;">);</br> messageMap.put(</span>"1021", "验证码失效"<span style="color: #000000;">);</br> messageMap.put(</span>"1022", "验证码错误"<span style="color: #000000;">);</br> messageMap.put(</span>"1023", "验证码不可用"<span style="color: #000000;">);</br> messageMap.put(</span>"1029", "短信平台异常"<span style="color: #000000;">);</br></br> messageMap.put(</span>"1030", "周边无店铺"<span style="color: #000000;">);</br> messageMap.put(</span>"1031", "店铺添加失败"<span style="color: #000000;">);</br> messageMap.put(</span>"1032", "编辑店铺信息失败"<span style="color: #000000;">);</br> messageMap.put(</span>"1033", "每个用户只能添加一个商铺"<span style="color: #000000;">);</br> messageMap.put(</span>"1034", "店铺不存在"<span style="color: #000000;">);</br></br> messageMap.put(</span>"1040", "无浏览商品"<span style="color: #000000;">);</br> messageMap.put(</span>"1041", "添加失败,商品种类超出上限"<span style="color: #000000;">);</br> messageMap.put(</span>"1042", "商品不存在"<span style="color: #000000;">);</br> messageMap.put(</span>"1043", "商品删除失败"<span style="color: #000000;">);</br></br> messageMap.put(</span>"2010", "缺少参数或值为空"<span style="color: #000000;">);</br></br> messageMap.put(</span>"2029", "参数不合法"<span style="color: #000000;">);</br> messageMap.put(</span>"2020", "无效的Token"<span style="color: #000000;">);</br> messageMap.put(</span>"2021", "无操作权限"<span style="color: #000000;">);</br> messageMap.put(</span>"2022", "RSA解密失败,密文数据已损坏"<span style="color: #000000;">);</br> messageMap.put(</span>"2023", "请重新登录"<span style="color: #000000;">);</br> }</br> </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">static</span> String retParam(<span style="color: #0000ff;">int</span><span style="color: #000000;"> status,Object data) {</br> OutputJson json </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> OutputJson(status, messageMap.get(String.valueOf(status)), data);</br> </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> json.toString(); }</br>
}
返回格式实体类OutPutJson;这里用到了知名的fastjson将对象转json:
package com.drskj.apiservice.common.utils;
import java.io.Serializable;
import com.alibaba.fastjson.JSON;
public class OutputJson implements Serializable{
</span><span style="color: #008000;">/**</span><span style="color: #008000;"></br> * 返回客户端统一格式,包括状态码,提示信息,以及业务数据</br> </span><span style="color: #008000;">*/</span></br> <span style="color: #0000ff;">private</span> <span style="color: #0000ff;">static</span> <span style="color: #0000ff;">final</span> <span style="color: #0000ff;">long</span> serialVersionUID = 1L<span style="color: #000000;">;</br> </span><span style="color: #008000;">//</span><span style="color: #008000;">状态码</span></br> <span style="color: #0000ff;">private</span> <span style="color: #0000ff;">int</span><span style="color: #000000;"> status;</br> </span><span style="color: #008000;">//</span><span style="color: #008000;">必要的提示信息</span></br> <span style="color: #0000ff;">private</span><span style="color: #000000;"> String message;</br> </span><span style="color: #008000;">//</span><span style="color: #008000;">业务数据</span></br> <span style="color: #0000ff;">private</span><span style="color: #000000;"> Object data;</br></br> </span><span style="color: #0000ff;">public</span> OutputJson(<span style="color: #0000ff;">int</span><span style="color: #000000;"> status,String message,Object data){</br> </span><span style="color: #0000ff;">this</span>.status =<span style="color: #000000;"> status;</br> </span><span style="color: #0000ff;">this</span>.message =<span style="color: #000000;"> message;</br> </span><span style="color: #0000ff;">this</span>.data =<span style="color: #000000;"> data;</br> }</br> </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">int</span><span style="color: #000000;"> getStatus() {</br> </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> status;</br> }</br> </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span> setStatus(<span style="color: #0000ff;">int</span><span style="color: #000000;"> status) {</br> </span><span style="color: #0000ff;">this</span>.status =<span style="color: #000000;"> status;</br> }</br> </span><span style="color: #0000ff;">public</span><span style="color: #000000;"> String getMessage() {</br> </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> message;</br> }</br> </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> setMessage(String message) {</br> </span><span style="color: #0000ff;">this</span>.message =<span style="color: #000000;"> message;</br> }</br> </span><span style="color: #0000ff;">public</span><span style="color: #000000;"> Object getData() {</br> </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> data;</br> }</br> </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> setData(Object data) { </span><span style="color: #0000ff;">this</span>.data =<span style="color: #000000;"> data; } </span><span style="color: #0000ff;">public</span><span style="color: #000000;"> String toString(){</br> </span><span style="color: #0000ff;">if</span>(<span style="color: #0000ff;">null</span> == <span style="color: #0000ff;">this</span><span style="color: #000000;">.data){</br> </span><span style="color: #0000ff;">this</span>.setData(<span style="color: #0000ff;">new</span><span style="color: #000000;"> Object());</br> }</br> </span><span style="color: #0000ff;">return</span> JSON.toJSONString(<span style="color: #0000ff;">this</span><span style="color: #000000;">);</br> }
}
实例:CodeController继承自BaseController,有一个sendMessage方法调用Service层发送短信验证码;
1.如果客户端请求方式为非POST,否则抛出HttpMediaTypeNotSupportedException异常;
2.如果username、forType或userType没传,则抛出MissingServletRequestParameterException异常;
3.如果springmvc接收无法进行类型转换的字段,会报TypeMismatchException异常;
.....
大部分的请求异常,springmvc已经为我们定义好了,为我们开发restful应用提高了测试效率,方便排查问题出在哪一环节。
@RestController
@RequestMapping("/api/v1/code")
public class CodeController extends BaseController { @Autowired private CodeService codeService;</span><span style="color: #008000;">/**</span><span style="color: #008000;"></br> * 发送短信</br> * </span><span style="color: #808080;">@param</span><span style="color: #008000;"> username 用户名</br> * </span><span style="color: #808080;">@param</span><span style="color: #008000;"> type register/backpwd</br> * </span><span style="color: #808080;">@return</span><span style="color: #008000;"></br> * status: 0 2010 2029 1011 1010 1006 1020 </br> </span><span style="color: #008000;">*/</span><span style="color: #000000;"></br> @RequestMapping(value</span>="/sendMessage",method=RequestMethod.POST,produces="application/json"<span style="color: #000000;">)</br> </span><span style="color: #0000ff;">public</span> String sendMessage(@RequestParam(value="username",required=<span style="color: #0000ff;">true</span><span style="color: #000000;">)String username,</br> @RequestParam(value</span>="forType",required=<span style="color: #0000ff;">true</span><span style="color: #000000;">)String forType,</br> @RequestParam(value</span>="userType",required=<span style="color: #0000ff;">true</span><span style="color: #000000;">)String userType){</br> </span><span style="color: #0000ff;">if</span>(<span style="color: #0000ff;">null</span> == username || ""<span style="color: #000000;">.equals(username)){</br> </span><span style="color: #0000ff;">return</span> retContent(2010, <span style="color: #0000ff;">null</span><span style="color: #000000;">);</br> }</br> </span><span style="color: #0000ff;">if</span>(!"user".equals(userType) && !"merchant"<span style="color: #000000;">.equals(userType)){</br> </span><span style="color: #0000ff;">return</span> retContent(2029, <span style="color: #0000ff;">null</span><span style="color: #000000;">);</br> }</br> </span><span style="color: #0000ff;">if</span>(!"register".equals(forType) && !"backpwd"<span style="color: #000000;">.equals(forType)){</br> </span><span style="color: #0000ff;">return</span> retContent(2029, <span style="color: #0000ff;">null</span><span style="color: #000000;">);</br> }</br> </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> codeService.sendMessage(username, forType, userType);</br> }</br>
}
public abstract class BaseController { protected String retContent(int status,Object data) { return ReturnFormat.retParam(status, data); } }
最终,不管是正常的业务逻辑还是服务端异常,都会调用ReturnFormat.retParam(int status,Object data)方法返回格式统一的数据。