欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2024-03-31 16:27:40
【1】springboot的默认错误处理 ① 浏览器访问 请求头如下: ② 使用“postman”访问 { "timestamp": 1529...

【1】springboot的默认错误处理

① 浏览器访问

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

请求头如下:

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

② 使用“postman”访问

{
  "timestamp": 1529479254647,
  "status": 404,
  "error": "not found",
  "message": "no message available",
  "path": "/aaa1"
}

请求头如下:

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

总结:如果是浏览器访问,则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)。

如下图所示:

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

页面能获取的信息;

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>
//...

浏览器测试效果如下:

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

postman测试效果如下:

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。