评论

收藏

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

编程语言 编程语言 发布于:2021-10-07 13:05 | 阅读数:394 | 评论:0

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

请求头如下:
DSC0001.jpg

② 使用“postman”访问
{
  "timestamp": 1529479254647,
  "status": 404,
  "error": "not found",
  "message": "no message available",
  "path": "/aaa1"
}
请求头如下:
DSC0002.jpg

总结:如果是浏览器访问,则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)。
如下图所示:
DSC0003.jpg

页面能获取的信息;
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>
//...
浏览器测试效果如下:
DSC0004.jpg

postman测试效果如下:
DSC0005.jpg

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

关注下面的标签,发现更多相似文章