问题背景
在项目开发过程中,发现一个问题,不同springboot版本(2.2.2和2.3以上版本)在做统一错误处理的时候,高版本的无法获取到设置的错误信息,postman测试结果如下:
//2.2.2 版本
{
"code": 401,
"message": "请登录后操作",
"messsageCode": null,
"data": null
}
//2.4.4 版本
{
"code": 401,
"message": "",
"messsageCode": null,
"data": null
}
通过测试结果可见,2.4.4版本message值为空;接下来我就结合源码来分析下产生这个的原因。
一、实现逻辑分析
springboot通过设置统一异常处理的路口,对异常内容进行分析,将处理后的结果返回给客户端进行呈现。要实现自定义异常处理类,有以下方式:
1、自定义一个bean,实现ErrorController接口,那么默认的错误处理机制将不再生效。
2、自定义一个bean,继承BasicErrorController类,使用一部分现成的功能,自己也可以添加新的public方法,使用@RequestMapping及其produces属性指定新的地址映射。
3、自定义一个ErrorAttribute类型的bean,那么还是默认的两种响应方式,只不过改变了内容项而已。
4、继承AbstractErrorController
总的来说就是实现ErrorController接口, BasicErrorController类继承AbstractErrorController,AbstractErrorController实现了ErrorController接口,我们只需要继承BasicErrorController,然后重写我们自己定义的结果返回方法即可。
二、BasicErrorController源码分析
2.1 先看类头部注解
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
//内容部分后面分析
}
这个RequestMapping地址,你可以理解为从左到右的选择关系,如果你在配置文件配置了server.error.path的话,就会使用你配置的异常处理地址,如果没有就会使用你配置的error.path路径地址,如果还是没有,默认使用/error来作为发生异常的处理地址。
2.2 异常处理路口
springboot版本2.2.2
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));//注意这行代码
return new ResponseEntity(body, status);
}
springboot版本2.4.4
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));//与2.2.2版本不一致地方
return new ResponseEntity(body, status);
}
}
它提供了两个接口处理方法,上面一个标注了produces为text/html,当你是网页请求的时候返回的网页数据,下面的接口是当你的请求为其他的时候,返回的是ResponseEntity对象(json数据或者其他,取决与你的返回数据类型配置)。
我们看到第一个接口返回了一个error页面,如果你的项目静态页面下刚好存在一个error所对应的页面,那么Spring Boot会得到你本地的页面。
我在这主要分析的事第二个处理方式,实现自定义返回结果。通过两个版本代码对比,发现其中的区别之处,上面代码有标识。
2.2.1首先来看springboot 2.2.2版本
设置相应结果方法:
其中includeStackTrace因为没有设置erroProperties参数includeStackTrace为ALWAYS所以其值为false
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap();
errorAttributes.put("timestamp", new Date());
this.addStatus(errorAttributes, webRequest);
this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);//此步骤添加相应信息
this.addPath(errorAttributes, webRequest);
return errorAttributes;
}
再看方法addErrorDetails
private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest webRequest, boolean includeStackTrace) {
Throwable error = this.getError(webRequest);
if (error != null) {
while(true) {
if (!(error instanceof ServletException) || error.getCause() == null) {
if (this.includeException) {
errorAttributes.put("exception", error.getClass().getName());
}
this.addErrorMessage(errorAttributes, error);//将error信息放到相应信息中
if (includeStackTrace) {
this.addStackTrace(errorAttributes, error);
}
break;
}
error = error.getCause();
}
}
通过断点得到error值如下图:
通过断点可以看到将error的detailMessage值放入到响应中,如下图所示:
2.2.2 springboot 2.4.4版本
注意:以下代码是沿着出现问题如何去分析解决的思路顺序,带着问题一步步去探索,大家可以打开源码跟着我的思路一步步去看,去理解很快可以看明白。
1. 沿着源码我们先找到方法getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options)
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = this.getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
if (Boolean.TRUE.equals(this.includeException)) {
options = options.including(new Include[]{Include.EXCEPTION});
}
if (!options.isIncluded(Include.EXCEPTION)) {
errorAttributes.remove("exception");
}
if (!options.isIncluded(Include.STACK_TRACE)) {
errorAttributes.remove("trace");
}
if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
errorAttributes.put("message", "");
}
if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}
return errorAttributes;
}
通过上面代码,我们可以知道errorAttributes的取值取决于option的值,所以接下来我们看option取值来源。
2. 方法getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType)
protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
if (this.errorProperties.isIncludeException()) {
options = options.including(new Include[]{Include.EXCEPTION});
}
if (this.isIncludeStackTrace(request, mediaType)) {
options = options.including(new Include[]{Include.STACK_TRACE});
}
if (this.isIncludeMessage(request, mediaType)) {
options = options.including(new Include[]{Include.MESSAGE});
}
if (this.isIncludeBindingErrors(request, mediaType)) {
options = options.including(new Include[]{Include.BINDING_ERRORS});
}
return options;
}
代码中可以看出,ErrorAttributeOptions取值又取决于errorProperties的值,所以我们来先看下ErrorProperties这个类。
3. ErrorProperties这个类
public class ErrorProperties {
@Value("${error.path:/error}")
private String path = "/error";
private boolean includeException;
private ErrorProperties.IncludeStacktrace includeStacktrace;
private ErrorProperties.IncludeAttribute includeMessage;
private ErrorProperties.IncludeAttribute includeBindingErrors;
private final ErrorProperties.Whitelabel whitelabel;
public ErrorProperties() {
this.includeStacktrace = ErrorProperties.IncludeStacktrace.NEVER;
this.includeMessage = ErrorProperties.IncludeAttribute.NEVER;
this.includeBindingErrors = ErrorProperties.IncludeAttribute.NEVER;
this.whitelabel = new ErrorProperties.Whitelabel();
}
//后面代码我们不看,只看这个构造器
}
可以看到,这个构造器给ErrorProperties的参数设置了默认值,通过代码我们知道默认设置的值堆栈跟踪,消息体,其他绑定的错误信息都是never,也就是永远不包含。
在我们做统一错误处理的时候,我们希望我们设置的错误提示能传回给前端,所以我们需要改变includeMessage的值,通过源码可以看到ErrorProperties.IncludeStacktrace的值有:
public static enum IncludeStacktrace {
NEVER,
ALWAYS,
ON_PARAM,
/** @deprecated */
@Deprecated
ON_TRACE_PARAM;
private IncludeStacktrace() {
}
}
通过上面2.0的方法如下:
if (this.isIncludeMessage(request, mediaType)) {
options = options.including(new Include[]{Include.MESSAGE});
}
protected boolean isIncludeMessage(HttpServletRequest request, MediaType produces) {
switch(this.getErrorProperties().getIncludeMessage()) {
case ALWAYS:
return true;
case ON_PARAM:
return this.getMessageParameter(request);
default:
return false;
}
}
我们可以知道需要设置其值为ALWAYS,这样就能获取到设置的错误message的值了。
如何设置ErrorProperties参数的值
如果直接用springboot默认的统一错误处理即用BasicErrorController的话,我们只要在配置文件中加入如下配置:
#springboot 2.3之后,设置统一错误异常类参数配置
#server.error.include-exception=true
#server.error.include-stacktrace = ALWAYS
server.error.include-message= ALWAYS
#server.error.include-binding-errors= ALWAYS
如果是想自定义类去设置错误相应结果,可以参考如下代码:
@RestController
public class ErrorController extends BasicErrorController {
private Logger logger = LoggerFactory.getLogger(ErrorController.class);
/**
* 设置ErrorProperties参数值
*/
public static ErrorProperties initProperties(){
ErrorProperties properties = new ErrorProperties();
properties.setIncludeMessage(ErrorProperties.IncludeAttribute.ALWAYS);
return properties;
}
/**
* 引用父类构造方法,将设置的ErrorProperties传入父类
**/
public ErrorController() {
super(new DefaultErrorAttributes(), initProperties());
}
/**
*
* @param request 请求
* @return 自定义的返回实体类
*/
@Override
public ResponseEntity error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
logger.error("controler before exception: {}; ", body);
return new ResponseEntity<>(BaseResponse.authError(body.get("message").toString()), status);
}
}
总结
以上是我在编程过程中,遇到的问题,并记录了我分析解决过程,如有错误或者不足欢迎大家指正,觉得有帮助的话,麻烦大家点个赞,谢谢大家。
发现问题,探索原因,解决问题这一过程是一件挺爽的事。