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测试效果如下:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。