小蚂蚁 发表于 2021-10-7 13:05:20

SpringBoot 错误处理机制与自定义错误处理实现详解

这篇文章主要介绍了SpringBoot 错误处理机制与自定义错误处理实现详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
【1】springboot的默认错误处理
① 浏览器访问

请求头如下:

② 使用“postman”访问


{
"timestamp": 1529479254647,
"status": 404,
"error": "not found",
"message": "no message available",
"path": "/aaa1"
}
请求头如下:

总结:如果是浏览器访问,则springboot默认返回错误页面;如果是其他客户端访问,则默认返回json数据。
【2】默认错误处理原理
springboot默认配置了许多xxxautoconfiguration,这里我们找errormvcautoconfiguration。
其注册部分组件如下:
① defaulterrorattributes


@bean
@conditionalonmissingbean(value = errorattributes.class, search = searchstrategy.current)
public defaulterrorattributes errorattributes() {
return new defaulterrorattributes();
}
跟踪其源码如下:


public class defaulterrorattributes
    implements errorattributes, handlerexceptionresolver, ordered {

private static final string error_attribute = defaulterrorattributes.class.getname()
      + ".error";

@override
public int getorder() {
    return ordered.highest_precedence;
}

@override
public modelandview resolveexception(httpservletrequest request,
      httpservletresponse response, object handler, exception ex) {
    storeerrorattributes(request, ex);
    return null;
}

private void storeerrorattributes(httpservletrequest request, exception ex) {
    request.setattribute(error_attribute, ex);
}

@override
public map<string, object> geterrorattributes(requestattributes requestattributes,
      boolean includestacktrace) {
    map<string, object> errorattributes = new linkedhashmap<string, object>();
    errorattributes.put("timestamp", new date());
    addstatus(errorattributes, requestattributes);
    adderrordetails(errorattributes, requestattributes, includestacktrace);
    addpath(errorattributes, requestattributes);
    return errorattributes;
}

private void addstatus(map<string, object> errorattributes,
      requestattributes requestattributes) {
    integer status = getattribute(requestattributes,
      "javax.servlet.error.status_code");
    if (status == null) {
      errorattributes.put("status", 999);
      errorattributes.put("error", "none");
      return;
    }
    errorattributes.put("status", status);
    try {
      errorattributes.put("error", httpstatus.valueof(status).getreasonphrase());
    }
    catch (exception ex) {
      // unable to obtain a reason
      errorattributes.put("error", "http status " + status);
    }
}

private void adderrordetails(map<string, object> errorattributes,
      requestattributes requestattributes, boolean includestacktrace) {
    throwable error = geterror(requestattributes);
    if (error != null) {
      while (error instanceof servletexception && error.getcause() != null) {
      error = ((servletexception) error).getcause();
      }
      errorattributes.put("exception", error.getclass().getname());
      adderrormessage(errorattributes, error);
      if (includestacktrace) {
      addstacktrace(errorattributes, error);
      }
    }
    object message = getattribute(requestattributes, "javax.servlet.error.message");
    if ((!stringutils.isempty(message) || errorattributes.get("message") == null)
      && !(error instanceof bindingresult)) {
      errorattributes.put("message",
          stringutils.isempty(message) ? "no message available" : message);
    }
}

private void adderrormessage(map<string, object> errorattributes, throwable error) {
    bindingresult result = extractbindingresult(error);
    if (result == null) {
      errorattributes.put("message", error.getmessage());
      return;
    }
    if (result.geterrorcount() > 0) {
      errorattributes.put("errors", result.getallerrors());
      errorattributes.put("message",
          "validation failed for object='" + result.getobjectname()
            + "'. error count: " + result.geterrorcount());
    }
    else {
      errorattributes.put("message", "no errors");
    }
}

private bindingresult extractbindingresult(throwable error) {
    if (error instanceof bindingresult) {
      return (bindingresult) error;
    }
    if (error instanceof methodargumentnotvalidexception) {
      return ((methodargumentnotvalidexception) error).getbindingresult();
    }
    return null;
}

private void addstacktrace(map<string, object> errorattributes, throwable error) {
    stringwriter stacktrace = new stringwriter();
    error.printstacktrace(new printwriter(stacktrace));
    stacktrace.flush();
    errorattributes.put("trace", stacktrace.tostring());
}

private void addpath(map<string, object> errorattributes,
      requestattributes requestattributes) {
    string path = getattribute(requestattributes, "javax.servlet.error.request_uri");
    if (path != null) {
      errorattributes.put("path", path);
    }
}

@override
public throwable geterror(requestattributes requestattributes) {
    throwable exception = getattribute(requestattributes, error_attribute);
    if (exception == null) {
      exception = getattribute(requestattributes, "javax.servlet.error.exception");
    }
    return exception;
}

@suppresswarnings("unchecked")
private <t> t getattribute(requestattributes requestattributes, string name) {
    return (t) requestattributes.getattribute(name, requestattributes.scope_request);
}

}
即,填充错误数据!
② basicerrorcontroller


@bean
@conditionalonmissingbean(value = errorcontroller.class, search = searchstrategy.current)
public basicerrorcontroller basicerrorcontroller(errorattributes errorattributes) {
return new basicerrorcontroller(errorattributes, this.serverproperties.geterror(),
      this.errorviewresolvers);
}
跟踪其源码:


@controller
@requestmapping("${server.error.path:${error.path:/error}}")
public class basicerrorcontroller extends abstracterrorcontroller {
//产生html类型的数据;浏览器发送的请求来到这个方法处理
@requestmapping(produces = "text/html")
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 ? new modelandview("error", model) : modelandview);
}
//产生json数据,其他客户端来到这个方法处理;
@requestmapping
@responsebody
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<map<string, object>>(body, status);
}
//...
}
其中 resolveerrorview(request, response, status, model);方法跟踪如下:


public abstract class abstracterrorcontroller implements errorcontroller {
protected modelandview resolveerrorview(httpservletrequest request,
      httpservletresponse response, httpstatus status, map<string, object> model) {
      //拿到所有的错误视图解析器
    for (errorviewresolver resolver : this.errorviewresolvers) {
      modelandview modelandview = resolver.resolveerrorview(request, status, model);
      if (modelandview != null) {
      return modelandview;
      }
    }
    return null;
}
//...
}
③ errorpagecustomizer


@bean
public errorpagecustomizer errorpagecustomizer() {
return new errorpagecustomizer(this.serverproperties);
}
跟踪其源码:


@override
public void registererrorpages(errorpageregistry errorpageregistry) {
errorpage errorpage = new errorpage(this.properties.getservletprefix()
      + this.properties.geterror().getpath());
errorpageregistry.adderrorpages(errorpage);
}
//getpath()->go on
/**
   * path of the error controller.
   */
@value("${error.path:/error}")
private string path = "/error";
即,系统出现错误以后来到error请求进行处理(web.xml注册的错误页面规则)。
④ defaulterrorviewresolver


@bean
@conditionalonbean(dispatcherservlet.class)
@conditionalonmissingbean
public defaulterrorviewresolver conventionerrorviewresolver() {
return new defaulterrorviewresolver(this.applicationcontext,
      this.resourceproperties);
}
跟踪其源码:


public class defaulterrorviewresolver implements errorviewresolver, ordered {

private static final map<series, string> series_views;
//错误状态码
static {
    map<series, string> views = new hashmap<series, string>();
    views.put(series.client_error, "4xx");
    views.put(series.server_error, "5xx");
    series_views = collections.unmodifiablemap(views);
}
//...
@override
public modelandview resolveerrorview(httpservletrequest request, httpstatus status,
      map<string, object> model) {
// 这里如果没有拿到精确状态码(如404)的视图,则尝试拿4xx(或5xx)的视图
    modelandview modelandview = resolve(string.valueof(status), model);
    if (modelandview == null && series_views.containskey(status.series())) {
      modelandview = resolve(series_views.get(status.series()), model);
    }
    return modelandview;
}

private modelandview resolve(string viewname, map<string, object> model) {
    //默认springboot可以去找到一个页面? error/404||error/4xx
    string errorviewname = "error/" + viewname;
    //模板引擎可以解析这个页面地址就用模板引擎解析
    templateavailabilityprovider provider = this.templateavailabilityproviders
      .getprovider(errorviewname, this.applicationcontext);
    if (provider != null) {
      //模板引擎可用的情况下返回到errorviewname指定的视图地址
      return new modelandview(errorviewname, model);
    }
    //模板引擎不可用,就在静态资源文件夹下找errorviewname对应的页面 error/404.html
    return resolveresource(errorviewname, model);
}

private modelandview resolveresource(string viewname, map<string, object> model) {
    //从静态资源文件夹下面找错误页面
    for (string location : this.resourceproperties.getstaticlocations()) {
      try {
      resource resource = this.applicationcontext.getresource(location);
      resource = resource.createrelative(viewname + ".html");
      if (resource.exists()) {
          return new modelandview(new htmlresourceview(resource), model);
      }
      }
      catch (exception ex) {
      }
    }
    return null;
}
总结如下:
一但系统出现4xx或者5xx之类的错误,errorpagecustomizer就会生效(定制错误的响应规则),就会来到/error请求,然后被basicerrorcontroller处理返回modelandview或者json。
【3】定制错误响应
① 定制错误响应页面
1)有模板引擎的情况下
error/状态码–将错误页面命名为 错误状态码.html 放在模板引擎文件夹里面的error文件夹下,发生此状态码的错误就会来到 对应的页面。
我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找精确的状态码.html)。
如下图所示:

页面能获取的信息;

timestamp:时间戳
status:状态码
error:错误提示
exception:异常对象
message:异常消息
errors:jsr303数据校验的错误都在这里
2)没有模板引擎(模板引擎找不到这个错误页面),静态资源文件夹下找。
3)以上都没有错误页面,就是默认来到springboot默认的错误提示页面。
webmvcautoconfiguration源码如下:


@configuration
@conditionalonproperty(prefix = "server.error.whitelabel", name = "enabled", matchifmissing = true)
@conditional(errortemplatemissingcondition.class)
protected static class whitelabelerrorviewconfiguration {

private final spelview defaulterrorview = new spelview(
      "<html><body><h1>whitelabel error page</h1>"
          + "<p>this application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
          + "<div id='created'>${timestamp}</div>"
          + "<div>there was an unexpected error (type=${error}, status=${status}).</div>"
          + "<div>${message}</div></body></html>");

@bean(name = "error")
@conditionalonmissingbean(name = "error")
public view defaulterrorview() {
    return this.defaulterrorview;
}

// if the user adds @enablewebmvc then the bean name view resolver from
// webmvcautoconfiguration disappears, so add it back in to avoid disappointment.
@bean
@conditionalonmissingbean(beannameviewresolver.class)
public beannameviewresolver beannameviewresolver() {
    beannameviewresolver resolver = new beannameviewresolver();
    resolver.setorder(ordered.lowest_precedence - 10);
    return resolver;
}

}
② 定制错误响应数据
第一种,使用springmvc的异常处理器


@controlleradvice
public class myexceptionhandler {

//浏览器客户端返回的都是json
@responsebody
@exceptionhandler(usernotexistexception.class)
public map<string,object> handleexception(exception e){
    map<string,object> map = new hashmap<>();
    map.put("code","user.notexist");
    map.put("message",e.getmessage());
    return map;
}
}
这样无论浏览器还是postman返回的都是json!
第二种,转发到/error请求进行自适应效果处理


@exceptionhandler(usernotexistexception.class)
public string handleexception(exception e, httpservletrequest request){
   map<string,object> map = new hashmap<>();
   //传入我们自己的错误状态码 4xx 5xx
   /**
   * integer statuscode = (integer) request
   .getattribute("javax.servlet.error.status_code");
   */
   request.setattribute("javax.servlet.error.status_code",500);
   map.put("code","user.notexist");
   map.put("message","用户出错啦");
   //转发到/error
   return "forward:/error";
}
但是此时没有将自定义 code message传过去!
第三种,注册myerrorattributes继承自defaulterrorattributes(推荐)
从第【2】部分(默认错误处理原理)中知道错误数据都是通过defaulterrorattributes.geterrorattributes()方法获取,如下所示:


@override
public map<string, object> geterrorattributes(requestattributes requestattributes,
    boolean includestacktrace) {
map<string, object> errorattributes = new linkedhashmap<string, object>();
errorattributes.put("timestamp", new date());
addstatus(errorattributes, requestattributes);
adderrordetails(errorattributes, requestattributes, includestacktrace);
addpath(errorattributes, requestattributes);
return errorattributes;
}
我们可以编写一个myerrorattributes继承自defaulterrorattributes重写其geterrorattributes方法将我们的错误数据添加进去。
示例如下:


//给容器中加入我们自己定义的errorattributes
@component
public class myerrorattributes extends defaulterrorattributes {

//返回值的map就是页面和json能获取的所有字段
@override
public map<string, object> geterrorattributes(requestattributes requestattributes, boolean includestacktrace) {
    //defaulterrorattributes的错误数据
    map<string, object> map = super.geterrorattributes(requestattributes, includestacktrace);
    map.put("company","springboot");
    //我们的异常处理器携带的数据
    map<string,object> ext = (map<string, object>) requestattributes.getattribute("ext", 0);
    map.put("ext",ext);
    return map;
}
}
异常处理器修改如下:


@exceptionhandler(usernotexistexception.class)
public string handleexception(exception e, httpservletrequest request){
   map<string,object> map = new hashmap<>();
   //传入我们自己的错误状态码 4xx 5xx
   /**
   * integer statuscode = (integer) request
   .getattribute("javax.servlet.error.status_code");
   */
   request.setattribute("javax.servlet.error.status_code",500);
   map.put("code","user.notexist");
   map.put("message","用户出错啦");
//将自定义错误数据放入request中
   request.setattribute("ext",map);
   //转发到/error
   return "forward:/error";
}
5xx.html页面代码如下:


//...
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<h1>status:[[${status}]]</h1>
<h2>timestamp:[[${timestamp}]]</h2>
<h2>exception:[[${exception}]]</h2>
<h2>message:[[${message}]]</h2>
<h2>ext:[[${ext.code}]]</h2>
<h2>ext:[[${ext.message}]]</h2>
</main>
//...
浏览器测试效果如下:

postman测试效果如下:

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持CodeAE代码之家。
原文链接:https://blog.csdn.net/j080624/article/details/80747669

http://www.zzvips.com/article/170767.html
页: [1]
查看完整版本: SpringBoot 错误处理机制与自定义错误处理实现详解