16.springboot的错误处理机制
1.如果是浏览器,返回一个默认的错误页面:包含了错误信息
2.如果是客户端访问(如用supui来调用,或其他工具来调用时)
返回:是一个json字符串
{
"timestamp": "2020-09-24T03:02:39.488+00:00",
"status": 404,
"error": "Not Found",
"message": "",
"path": "/aa"
}
原理:
ErrorMvcAutoConfiguration类:错误处理的自动配置
源码给容器中添加了以下组件:
1.DefaultErrorAttributes
帮我们在页面共享信息
1.时间戳:timesamp
2.状态码:status
3.错误提示:error
4.异常对象:exception
5.异常消息:message
6.JSR303做校验:errors
例如在页面上直接获取:
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<h1>status:[[${status}]]</h1>
<h2>timestamp:[[${#dates.format(timestamp,'yyyy-MM-dd')}]]</h2>
</main>
2.BasicErrorController:处理默认的/error请求
部分源码如下:如果没有配置server.error.path,就使用${error.path:/error}}
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
...
//返回html的格式,浏览器发送的请求来到这个方法处理
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//决定错误去哪个页面:modelAndView 包含了页面地址和页面数据
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
//返回json的格式:其他客户端发送的请求来到这个方法处理
@RequestMapping
public ResponseEntity<Map<String, Object>> 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));
return new ResponseEntity<>(body, status);
}
...
}
结论:发现都是处理/error的请求,但是有两种处理方式,分别返回html格式和json格式,理由如上述源码!
这里分为了两种
1.响应页面
源码如下:
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
//通过所有的异常视图解析器(ErrorViewResolver-->也就是4.DefaultErrorViewResolver )得到ModelAndView
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
2.响应json数据
3.ErrorPageCustomizer:部分原码如下
@Value("${error.path:/error}")
private String path = "/error";
系统出现错误以后,来到error请求进行处理;(类似于web.xml注册的祖屋页面规则)
4.DefaultErrorViewResolver:决定错误去哪个页面
源码如下:
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status.value()), 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/其他错误码
String errorViewName = "error/" + viewName;
//使用模板引擎进行解析,如果可以解析就用模板引擎
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
//模板引擎可用的情况下返回errorViewName指定的市入地址
return new ModelAndView(errorViewName, model);
}
//模板引擎不可用的话,就在静态资源文件夹("/META-INF/resources/","/resources/", "/static/", "/public/")下找errorViewName对应的页面 error/404
return resolveResource(errorViewName, model);
}
步骤:
1.一但系统出现4xx或者5xx之类的错误,ErrorPageCustomizer就会生效,(定制错误的响应规则);就会来到/error请求
2.接着会被BasicErrorController处理,决定返回的时页面还是json数据
3.若2决定返回的是页面:通过DefaultErrorViewResolver决定返回的是哪个错误页面
那springboot如何区分是到底是浏览器访问还是其他客户端访问呢:
1.浏览器访问时:请求头里会有accept:text/html
2.如果是其它客户端访问时,例如postman:发现其请求头中的accept是:accept:"*/*"
这就是浏览器和客户端发送请求时的不同,也是springboot用于区分的标准
问题:如何定制错误响应
1.如何定制错误页面
1.1有模板引擎的情况下,其底层会去templates文件夹下/error/状态码.html
【将错误页面命名为 错误状态码.html 放在模板引擎文件夹下(templates)里面的error文件夹下的错误码.html,即路径是templates/error/错误码.html】,发生此状态码的错误就会来到对应的页面
1.2我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先
源码:
static {
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
1.2在没有模板引擎的情况下如何定制呢?(templates文件夹下没有找到对应的错误码.html)
默认也是在静态资源文件夹下查找!"/META-INF/resources/","/resources/", "/static/", "/public/"
1.3模板引擎和静态文件夹下均没有,就是来到springboot的默认的错误提示页面
2.如何定制错误响应json
1.定制自己的异常类:
public class MyException extends RuntimeException{
public MyException() {
super("自定义异常!");
}
}
2.创建自己的异常处理器
@ControllerAdvice
public class MyExceptionHandler {
@ResponseBody
@ExceptionHandler(MyException.class)--->处理的是自定义的异常类,也可以是所有异常:Exception.class
public Map<String,Object> handleException(Exception e){
Map<String, Object> map=new HashMap<>();
map.put("code","521");
map.put("message",e.getMessage());
return map;
}
}
3.抛出异常的地方:当我们输入路径:localhost:8080/hello就会抛出异常
@RequestMapping("hello")
public void hellp(){
throw new MyException();
}
1.浏览器访问:localhost:8080/hello页面出现:
2.postman客户端访问时输出:
结论:但以上的处理没有达到自适应(浏览器访问时返回的时html页面,客户端访问时返回的时json数据)
那如何达到自适应效果呢
1.如何达到自适应效果呢(浏览器返回的是页面/客户端返回的是json数据)
只需要将自定义异常处理器的代码改为如下:
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(MyException.class)
public String handleException(Exception e, HttpServletRequest request){
//源码中获取异常状态码:
//Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
//RequestDispatcher.ERROR_STATUS_CODE=javax.servlet.error.status_code
//设置自定义的状态码:必须有这行(要不无法异常无法定位到自定义的错误页面)
request.setAttribute("javax.servlet.error.status_code","521");
Map<String, Object> map=new HashMap<>();
map.put("code",400);
map.put("message",e.getMessage());
return "forward:/error";---->转发到/error路径:ErrorPageCustomizer会进行处理,和上面的结合上了
}
}
我们必须要设置自己的错误状态码,要不虽然可以实现自适应(浏览器返回的是页面/客户端返回的是json数据),但页面上返回的是下面图片,并不是咱们
自定义的页面,并且发现其状态码是200,
客户端返回的数据是:发现其数据是规定的数据,那如何携带自定义数据呢
{
"timestamp": "2020-09-24T09:27:09.559+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "自定义异常!",
"path": "/hello"
}
如何给页面或客户端json传入自定义数据
加入以下自定义代码
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
//返回值的map就是页面和json能获取的所有字段
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> map = super.getErrorAttributes(webRequest, options);
map.put("wmd","吴孟达");
return map;
}
}
结果:不管输入什么错误路径,都会有自定义属性:wmd
{
"timestamp": "2020-09-24T09:40:11.163+00:00",
"status": 404,
"error": "Not Found",
"message": "",
"path": "/aa",
"wmd": "吴孟达"
}
最终的解决办法:
1.在原先的基础上:
自定义异常处理器的代码:
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(MyException.class)
public String handleException(Exception e, HttpServletRequest request){
//设置自定义的状态码
request.setAttribute("javax.servlet.error.status_code",500);
Map<String, Object> map=new HashMap<>();
map.put("code","500");
map.put("message",e.getMessage());
map.put("name","吴孟达");------->放入自定义对象
request.setAttribute("ext",map);----->并将这个map放到request域中
return "forward:/error";
}
}
2.自定义错误属性代码:
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> map = super.getErrorAttributes(webRequest, options);
map.put("ld","刘丹");---->放入自定义属性
Object ext=webRequest.getAttribute("ext",0);-->并从request域中获取上面放入的自定义对象值:0代表是从request域中获取
map.put("ext",ext);--->将request域中的自定义属性值放入map中,这样页面和json都可取到
return map;
}
}
3.输入错误路径输出:
{
"timestamp": "2020-09-24T10:05:48.082+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/hello",
"ld": "刘丹",
"ext": {
"code": "500",
"name": "吴孟达",
"message": "自定义异常!"
}
}