​ 该部分讲解的源码已发布到git,下载地址:https://github.com/sunnymaple/rest-spring-boot-starter

​ 上一片文章我们对响应参数做了统一的封装,看是功能已经完成,但是还是有一点欠缺,因为这些都是在程序正常返回响应值得理想情况下,我们的程序不可能没有bug,亦或是客户端提交了不合法的参数导致程序无法正常运行,如果是后者,那么我们就得给客户端做出相应的提示,让客户端做出判断后修改提交参数等操作,当然在程序出现bug时,我们的程序应该记录相关错误日志。这些在上一节的内容中是没有办法统一处理的,那么可以在controller中try-cath下,然后打印日志或者返回异常相关信息,这样无疑造成开发的不利,那同样可以使用自定义的starter做统一的异常处理。

1、SpringBoot定义的异常处理

再讲如何写starter之前,先看看springboot为我们做的统一异常处理,如果你引入上一章的自定义starter时,请在pom文件中删除,或者在application.properties配置文件中,加入下面配置:

response.handler.enabled=false

编写测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
@RequestMapping("/test")
@Validated
@Slf4j
public class TestController {
@RequestMapping("/exceptionTest")
public Integer exceptionTest(@NotNull(message = "val不能为空!") Integer val){
Integer result = null;
try {
result = serviceMethod(val);
} catch (Exception e) {
log.info("",e);
}
return result;
}
/**
* 模拟service层抛出异常
* @return
* @throws Exception
*/
private Integer serviceMethod(Integer val){
return val/0;
}
}

由代码可以看出exceptionTest接口定义了参数val不能为空(这涉及到@Validated和@NotNull注解,后面会给出说明),以及调用了serviceMethod,该防护会抛出异常。
而springboot给出的异常处理接口在spring-boot-autoconfigure jar包中的BasicErrorController,部分源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<>(body, status);
}
}

BasicErrorController定义了两个接口,一个处理MediaType.TEXT_HTML_VALUE,即text/html,响应的是ModelAndView 对象,即返回一个视图;而另一个则非text/html,响应ResponseEntity对象。
接下来我们启动测试项目,并在浏览器地址栏中输入:

http://localhost:8888/test/exceptionTest

结果如下:
图片
页面提示了val不能为空,但是中文乱码了,但是这个页面对用户而言是不友好的,当然我们可以自定义5xx(500等以5开头的响应吗)或者4xx(404/405等已4开头的状态码),但这不是这里研究的对象。接下来,我们再在postMan工具中,同样请求该接口,接口响应参数如下:

1
2
3
4
5
6
7
{
    "timestamp": "2019-06-14T03:07:49.998+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "exceptionTest.val: val不能为空!",
    "path": "/test/exceptionTest"
}

但是如果我输入了参数,而程序出由于除了0导致了异常,此时我们在application.properties文件中将response.handler.enabled=false指定为true或者注释掉,并在postman中输入如下地址:

1
http://localhost:8888/test/exceptionTest?val=1

响应结果如下:

1
2
3
4
5
6
{
    "status": 200,
    "message": "请求成功!",
    "result": null,
    "timestamp": 1560482400365
}

显然异常被catch了,请求成功了,但是result为null,显然不是我们想要的。

2、自定义异常处理

说了上面那么多不痛不痒的,接下来,我们将实现我们自己的异常处理功能,在上一篇文章的基础上,我们加个exception包,在该包下定义了实现该功能的类(源码已经上传到git:https://github.com/sunnymaple/rest-spring-boot-starter):
图片

主要看看AppExceptionHandler类以及BasicRestExceptionController类的作用。

2.1、AppExceptionHandler

直接上部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@ControllerAdvice
@Slf4j
public class AppExceptionHandler {


/**
* 打印日志并设置异常信息
* @param e 异常
* @param message 异常message
* @param status 错误状态码
*/
private void printLogAndSetAttribute(Throwable e, String message,Integer status){
//打印日志
log.error(e.getClass().getSimpleName() + "-->" + message,e);
//修改http响应状态码为异常状态码status,这里根据需求定,如果后台发生异常,响应状态码和异常状态码需要一致,则打开下面一行代码
// request.setAttribute("javax.servlet.error.status_code",status);
//设置异常信息
if (Utils.isEmpty(status)){
status = HttpStatusEnum.INTERNAL_SERVER_ERROR.getStatus();
}
request.setAttribute("status",status);
request.setAttribute("message",message);
request.setAttribute("isException","1");
}

/**
* 参数绑定异常
* @param e {@see BindException}
* @return
*/
@ExceptionHandler(BindException.class)
public String exceptionHandle(BindException e,HandlerMethod handlerMethod,HttpServletRequest request){
...
//转发到异常处理接口
return forward(handlerMethod);
}

/**
* 抛出参数验证异常
* @param e {@see ConstraintViolationException}
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
public String exceptionHandle(ConstraintViolationException e, HandlerMethod handlerMethod){
...
//转发到异常处理接口
return forward(handlerMethod);
}

/**
* 不支持的请求方式异常处理
* 如接口指定了只有Get请求,而实际以Post的方式请求,就会抛出该异常
* 注意:该接口不能传入{@link HandlerMethod}对象,否则捕捉不到该异常
* @param e HttpRequestMethodNotSupportedException
* @return
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public String exceptionHandle(HttpRequestMethodNotSupportedException e){
HttpStatusEnum definitionEnum = HttpStatusEnum.METHOD_NOT_ALLOWED;
...
//转发到异常处理接口
return forward(handlerMethod);
}

/**
* 异常RestException
* @param e
* @param handlerMethod
* @return
*/
@ExceptionHandler(RestException.class)
public String exceptionHandle(RestException e,HandlerMethod handlerMethod){
...
//转发到异常处理接口
return forward(handlerMethod);
}

/**
* 其他异常
* @param e
* @param handlerMethod
* @return
*/
@ExceptionHandler(Exception.class)
public String exceptionHandle(Exception e, HandlerMethod handlerMethod){
...
//转发到异常处理接口
return forward(handlerMethod);
}
}

首先看到该类上的controller增强器注解@ControllerAdvice,方法printLogAndSetAttribute()用于打印日志并且设置异常相关信息;而更多的方法是加上了@ExceptionHandler注解,并定义一个异常,表示被定义的异常都会被该接口拦截,不止上面定义的这些异常,大家也可以自己定义处理更多的异常。主要讲下处理的BindException和ConstraintViolationException异常,它们是spring对@Validated注解(参数验证)处理的异常,它的用法是在controller类中加上@Validated注解,并在方法(接口)中的参数或者参数对象属性中使用下面这些注解:

@Null 限制只能为null
@NotNull 限制必须不为null
@AssertFalse 限制必须为false
@AssertTrue 限制必须为true
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Future 限制必须是一个将来的日期
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@Pattern(value) 限制必须符合指定的正则表达式
@Size(max,min) 限制字符长度必须在min到max之间
@Past 验证注解的元素值(日期类型)比当前时间早
@NotEmpty 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式

实现了这两个异常的处理方法,那么我们在请求上面的测试接口时(不带请求参数),返回的响应参数是:

1
2
3
4
5
6
{
    "status": 1000,
    "message": "val不能为空!",
    "result": null,
    "timestamp": 1560490900367
}

而在每一个处理异常的方法中,都返回了:

1
return forward(handlerMethod);

我们看看方法forward的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

/**
* 定义异常处理接口
*/
private static final String FORWARD_EXCEPTION_URI = "/restException";

/**
* SpringBoot定义的异常处理接口 {@link BasicErrorController}
*/
@Value("${server.error.path:${error.path:/error}}")
private String errorPath;

private String forward(HandlerMethod handlerMethod){
return isResponseBody(handlerMethod) ? ("forward:" + FORWARD_EXCEPTION_URI) : ("forward:" + errorPath);
}

/**
* 判断是否是请求接口是否是ResponseBody
* @param handlerMethod
* @return
*/
private boolean isResponseBody(HandlerMethod handlerMethod){
//判断是否使用了responseHandler
AppResponseHandlerProperties properties = AppResponseHandlerProperties.getInstance();
if (Utils.isEmpty(properties) || !properties.isEnabled()){
return false;
}
//接口方法
Method method = handlerMethod.getMethod();
//接口类
Class<?> aClass = handlerMethod.getBean().getClass();
//先判断Controller类对象是否有RestController/ResponseBody注解
RestController restController = aClass.getAnnotation(RestController.class);
ResponseBody responseBody = aClass.getAnnotation(ResponseBody.class);
if (!Utils.isEmpty(restController) || !Utils.isEmpty(responseBody)){
//获取返回值类型
Class<?> returnType = method.getReturnType();
if (returnType == ModelAndView.class){
return false;
}
}
//判断方法(接口)是否有ResponseBody注解
ResponseBody rBody = handlerMethod.getMethodAnnotation(ResponseBody.class);
if (Utils.isEmpty(rBody)){
return false;
}
return true;
}

首先它判断是否是@ResponseBody,如果是它转发到接口/restException,否则则是errorPath,而errorPath使用@Value定义的正式文章开头提到的springboot提供的异常处理接口BasicErrorController。

2.2、BasicRestExceptionController

而BasicRestExceptionController类则是上一节提到的/restException接口,它和BasicErrorController一样,同样定义了两个接口,但是比较简单,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Controller
@RequestMapping(value = "/restException")
public class BasicRestExceptionController {

/**
* SpringBoot定义的异常处理接口 {@link BasicErrorController}
*/
@Value("${server.error.path:${error.path:/error}}")
private String errorPath;

/**
* 对定义了ResponseBody的请求接口,返回异常信息(message)
* 然后交给{@link AppResponseHandler}处理
* @param request
* @return
*/
@RequestMapping(produces = "application/json")
@ResponseBody
public String error(HttpServletRequest request) {
return request.getAttribute("message").toString();
}

@RequestMapping(produces = "text/html")
public String error() {
return "forward:" + errorPath;
}

}

3、对统一响应参数的修改

3.1、AppResponseHandler

在beforeBodyWrite方法中,添加了对异常的处理,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
Object result = null;
try {
//对异常进行处理
HttpServletRequest request = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();
Object isException = request.getAttribute("isException");
if (!Utils.isEmpty(isException) && Objects.equals(isException.toString(),"1")){
//接口异常
String message = request.getAttribute("message").toString();
Integer status = Integer.parseInt(request.getAttribute("status").toString());
// result = JSONObject.toJSONString(new RestResult(status,message), SerializerFeature.WriteMapNullValue);
result = new RestResult(status,message);
}else {
//封装统一格式的响应参数
result = restResult(o,mediaType,serverHttpRequest,serverHttpResponse,aClass);
}
} catch (Exception e) {
HttpStatusEnum statusEnum = HttpStatusEnum.INTERNAL_SERVER_ERROR;
result = new RestResult(statusEnum.getStatus(),statusEnum.getMessage());
log.info("创建统一格式的响应数据异常!",e);
}
return result;
}

3.2、HttpStatusEnum

在响应状态码枚举类中添加了如下异常状态码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 参数提交异常,异常信息由使用者定义
*/
PARAM_EXCEPTION(1000),
/**
* 用户名密码错误提示信息
*/
USER_PASSWORD_ERROR(1001, "用户名或密码错误!"),

LOGIN_EXCEPTION(1002,"未登录,或者没有权限!"),
/**
* 不支持的请求方式,如指定了post请求,却使用get请求之后抛出的异常
*/
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED.value(),"不支持的请求方式!")
× 请我吃糖~
打赏二维码