1、默认的错误处理机制
https://blog.csdn.net/qq_40634846/article/details/107463007
1.1 SpringBoot默认的错误效果
有时候点击添加按钮,跳转页面失败(显示报错找不到路径)。或者输入日期信息,点击提交按钮,发生4xx状态报错。然后会跳转到下图这个错误页面(这是SpringBoot默认的错误处理页面),这个就是错误处理。
1.1 浏览器,返回一个默认的错误页面
浏览器发送请求的请求头
1.2 如果是其他客户端(app),默认响应一个json数据(postman模拟)
客户端的请求头
1.2 错误处理原理
SpringBoot默认配置的错误处理都是由ErrorMvcAutoConfiguration类配置的。
1.2.1 错误处理的核心组件
核心组件都是在ErrorMvcAutoConfiguration配置的。有如下组件:
- DefaultErrorAttributes
- BasicErrorController
- ErrorPageCustomizer
- DefaultErrorViewResolver
SpringBoot自动配置错误处理是由ErrorMvcAutoConfiguration决定的。其中有4个核心组件起着重要作用,分别是:DefaultErrorAttributes、DefaultErrorViewResolver、BasicErrorController、ErrorPageCustomizer
1.2.2 ErrorPageCustomizer
一旦系统出现4xx或者5xx之类的错误,ErrorPageCustomizer就会生效(ErrorPageCustomizer是定制错误的响应规则)
ErrorMvcAutoConfiguration里面是这样注册ErrorPageCustomizer的,如下:
@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
点进去看ErrorPageCustomizer的内容是什么,如下:
/**
* {@link WebServerFactoryCustomizer} that configures the server's error pages.
*/
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
private final ServerProperties properties;
private final DispatcherServletPath dispatcherServletPath;
protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
@Override
public int getOrder() {
return 0;
}
}
并且发现ErrorPageCustomizer类是ErrorMvcAutoConfiguration的静态内部类。
可以看到它有一个重载方法registerErrorPages(),如下:
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
发生错误以后,系统会去哪个路径呢?这个是由registerErrorPages()中的getPath()决定的,getPath()方法如下:
public String getPath() {
return this.path;
}
再点击path,查看它是怎么定义的,如下:
/**
* Path of the error controller.
*/
@Value("${error.path:/error}")
private String path = "/error";
path是从配置文件中的error.path属性获取值,缺省值为/error
从上面path变量的定义可以得出,系统发生4xx/5xx错误后,会来到/error请求(也就是去到@RequestMapping(value=”/error”)的controller中进行处理),请求处理此错误。
1.2.3 BasicErrorController
上面谈到,系统一发生4xx/5xx就会默认去/error的controller中处理请求。
SpringBoot自动配置类ErrorMvcAutoConfiguration中的BasicErrorController组件,如下:
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
点进去看看BasicErrorController是什么内容,如下:
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
可以看到它是一个controller,而且处理的请求是配置文件中server.error.path属性值,缺省值是error.path属性值,error.path的缺省值是/error。从ErrorPageCustomizer类中,我们学习到系统一发生4xx/5xx就会默认去/error的controller中处理请求。也就是默认会去BasicErrorController请求处理。
再看到BasicErrorController有2个处理请求的核心方法,分别是 errorHtml()、error(),如下:
@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, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@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, isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
第一个方法errorHtml()返回值为ModelAndView,而且该方法会产生html类型的数据。这个方法就是在浏览器中展示错误页面的处理方法。比如浏览器访问某个地址,发生4xx错误,那么controller方法响应给它的是一个浏览器能显示的视图。
第二个方法返回值为ResponseEntity
是根据什么来区分到底用errorHtml()来处理/error还是用error()处理呢?
答:是根据请求头Request Headers的Accept属性来区分的。
解释:
在浏览器出现错误后,页面跳转到SpringBoot默认的错误页面,按F12,选择network可以看到如下:
因此浏览器发送的/error请求,BasicErrorController会使用errorHtml()来处理,因为errorHtml()是会产生html类型的数据。
在其他客户端出现错误后,发送的/error请求中,会有如下:
所以除了浏览器以外发送的请求,都会由BasicErrorController的error()方法处理。
总结1:浏览器发送的请求,会来到BasicErrorController的errorHtml()方法处理;其他客户端则来到BasicErrorController的error()方法处理。
总结2:一旦系统发生4xx/5xx之类的错误,ErrorPageCustomizer就会生效,就会发送/error请求,来到BasicErrorController的errorHtml()或error()进行处理。
现在分析是浏览器发送请求,所以详细看errorHtml()的内容,如下:
@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, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
从倒数第二行看到,最终返回的是modelAndView或者new ModelAndView(x, model),而modelAndView是由resolveErrorView(x,x,x, model)。(注意,这里无论是由resolveErrorView()得到的modelAndView,还是new ModelAndView(),他们都使用了model这个数据。后面需要知道这个知识点)
先了解resolveErrorView(),它的作用是去哪个页面作为错误页面:包含页面地址以及页面内容。
resolveErrorView()方法如下:
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;
}
resolveErrorView()方法是拿到所有的errorViewResolvers,其中一个就是DefaultErrorViewResolver(后面会详细讲此组件)。这里的遍历使用了设计模式中的策略模式。拿到xxxErrorViewResolver后,就会执行resolverErrorView(),并将值赋给modelAndView。如果不为空,则把这个modelAndView返回出去,否则继续遍历。遍历完后,modelAndView仍为空,则返回null。所以结合上面errorHtml()方法返回值,当modelAndView为空的时候,它会new一个ModelAndView。
总结:errorHtml()方法里面是会调用到resolveErrorView()方法,而resolveErrorView()方法里面又会拿到所有的xxxErrorViewResolver,其中一个ErrorViewResolver就是DefaultErrorViewResolver。拿到这个DefaultErrorViewResolver的实例后,又会调用它的resolveErrorView()方法。 因此后面讲解DefaultErrorViewResolver
1.2.4 DefaultErrorViewResolver
上面提到errorHtml()中最终会调用到DefaultErrorViewResolver的resolveErrorView()方法。
首先我们从根源找DefaultErrorViewResolver,在ErrorMvcAutoConfiguration中有如下:
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}
点击DefaultErrorViewResolver,看看内容是什么,如下:(代码中的注解就是解释,注意看)
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");//所有错误页面都是用各自的状态码命名,太麻烦了
views.put(Series.SERVER_ERROR, "5xx");//这里使用4xx,5xx命名
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
可以看到它会处理4xx以及5xx的错误。
再来看看DefaultErrorViewResolver的 resolveErrorView() 方法,如下:
@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;
}
从代码可知,最终返回的是modelAndView,它是由 resolve() 方法得来的。
再来看看rsolve()方法,如下:(代码中的注解就是解释,注意看)
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//SpringBoot默认可以找到一个error/404之类的页面。
String errorViewName = "error/" + viewName;
//模板引擎可以解析这个页面就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
//模板引擎可用的情况下,返回 error/viewName指定的视图地址
return new ModelAndView(errorViewName, model);
}
//模板引擎不可用,就在静态资源文件夹下找error/ViewName对应的页面,比如error/404
return resolveResource(errorViewName, model);
}
第二行代码可以看出,SpringBoot默认可以去找到一个页面,路径是error/viewName,而viewName是由入参得来的,而resolveErrorView传递进的入参是一个枚举类型,里面有很多状态码(比如1xx,2xx,3xx,4xx,5xx)。也就是SpringBoot可以找到一个error/404之类的页面。
最后一行调用了resolveResource(),如下:(代码中的注解就是解释,注意看)
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
//获取所有的存放静态资源的地址
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
//创建错误处理的响应页面的名称,命名格式为xxx.html,比如为4xxx.html 或者 5xxx.html等等
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
//返回ModelAndView
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
总结:
从上面分析errorHtml()处理请求是怎么样的,我们可以自己定制错误页面。
- 有模模板引擎的情况下:使用error/状态码。将错误页面命名为错误状态码.html,放在模板引擎文件夹里面的error文件夹下,发生此状态的错误就会来到对应的页面。由于错误状态码有太多太多,因此我们可以使用4xx/5xx匹配这种类型的所有错误,精确优先(优先寻找 精确的状态码.html)
- 没有模板引擎的情况下:会去静态资源文件夹下的error文件夹寻找
1.2.5 DefaultErrorAttributes
了解完SpringBoot是怎么寻找错误页面后,我们继续了解错误页面上的数据内容是从哪里获取的,以及可以获取什么数据。
错误页面上的数据肯定在modelAndView中,所以我们从controller中找起,前面我们了解过BasicErrorController,因此到BasicErrorController的errorHtml()找, 如下:(注意代码中的注释)
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
//2.model是由getErrorAttributes()获取的
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//1. 这里model被传入modelAndView
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
从上面看到model被传入modelAndView,model是由getErrorAttributes()获取的。点进去看看getErrorAttributes(),如下:
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
WebRequest webRequest = new ServletWebRequest(request);
return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
}
点进去看看errorAttributes,如下:
public abstract class AbstractErrorController implements ErrorController {
private final ErrorAttributes errorAttributes;
点进去看看ErrorAttributes,如下:
public interface ErrorAttributes {
Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace);
Throwable getError(WebRequest webRequest);
}
ErrorAttributes是一个接口,我们将光标放在ErrorAttributes,按ctrl+alt+b,就会打开到如下:
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
最终找到DefaultErrorAttributes,这个类在ErrorMvcAutoConfiguration中是有注册的。
DefaultErrorAttribute中最关键的一个属性是includeException,它是一个布尔值而且缺省值是false(所以它会导致某些异常信息不能被存储到某个Map中),通过它来决定是否添加某些异常信息到errorAttributes(这是一个Map,存储异常信息)中。
private final boolean includeException;
public DefaultErrorAttributes() {
this(false);
}
在DefaultErrorAttributes中找到getErrorAttributes(),如下:
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap();
errorAttributes.put("timestamp", new Date());
this.addStatus(errorAttributes, webRequest);
this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
this.addPath(errorAttributes, webRequest);
return errorAttributes;
}
使用getErrorAttributes()方法,错误页面最终能获取到信息有如下:
- timestamp:时间戳
- status:状态码
- error:错误提示
- exception:异常对象
- message:异常信息
- eerrors:JSR303数据校验都在这里
总结:错误页面最终能获取到信息有timestamp:时间戳, status:状态码,error:错误提示, exception:异常对象,message:异常信息, eerrors:JSR303数据校验都在这里
1.2.6 SpringBoot默认的错误提示
在ErrorMvcAutoConfiguration中,有WhitelabelErrorViewConfiguration静态内部类,如下:
protected static class WhitelabelErrorViewConfiguration {
private final StaticView defaultErrorView = new StaticView();//这里存储了默认的错误页面内容
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;//返回默认错误视图
}
点击StaticView,可以看到如下:
private static class StaticView implements View {
它也是一个静态内部类,它有一个render()方法。
render()方法有部分代码如下:
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
.append("<div id='created'>").append(timestamp).append("</div>")
.append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
.append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
1.2.7 四个核心组件之间的关系
四个核心组件,指的是前面介绍的ErrorPageCustomizer、BasicErrorController、DefaultErrorAttributes、DefaultErrorViewResolver。
关系如下:
1.2.8 总结
2. 定制错误页面
https://blog.csdn.net/qq_40634846/article/details/107710404
整理SpringBoot寻找错误页面的路径,如下:
所以 定制页面的方法 如下:
- 可以在templates/error/下创建状态码.html文件
- 可以在静态资源文件夹/error/下创建状态码.html文件
2.1 静态资源文件夹下面创建一个error文件夹
在静态资源文件夹下面创建一个error文件夹,在里面放置自己的错误页面。
在BasicErrorController中,封装视图的时候,当前项目如果有模板引擎,会先用模板引擎解析,找不到再去静态资源文件夹寻找视图(视图名是error/状态码.html,这是指error文件夹下的状态码.html文件,这是由私有方法实现的,所以外部无法修改)。因此上面的步骤就是SpringBoot先去templates文件夹下找,找不到,再去静态资源文件夹找(前提是你设置了静态资源文件夹;否则默认按SpringBoot的默认静态资源文件夹找,比如resources、public、static、resource)
2.2 WebServerFactoryCustomizer
创建一个MyErrorPageConfig
类,最最最关键的是new ErrorPage()中第二个入参,如果是加了.html
,那么就会直接找页面,如果没有加,那么将它当作请求去找controller,如下:
@Configuration
public class MyErrorPageConfig {
@Bean
public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer(){
return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage1 = new ErrorPage(HttpStatus.NOT_FOUND, "/page/error/4xx.html");
ErrorPage errorPage2 = new ErrorPage(HttpStatus.NOT_FOUND, "/page/error/5xx.html");
factory.addErrorPages(errorPage1, errorPage2);
}
};
}
}
2.3 实现自适应
自适应就是根据发送的/error请求是浏览器还是客户端,使用不同的controller方法进行处理,并返回不同类型的数据
**
创建一个实现了ErrorController接口的MyBasicErrorController类,如下:
/**
* 定制ErrorController,目的是能使SpringBoot找到自己定制的错误页面
* 大部分的代码BasicController一致,关键点是修改错误页面的路径
*/
@Controller
@RequestMapping(value = "/error")
public class MyBasicErrorController implements ErrorController {
@RequestMapping(produces = {"text/html"})//返回给浏览器
public String handlerError(HttpServletRequest request, Model model){
WebRequest webRequest = new ServletWebRequest(request);//对request进行包装,目的是能操作更多的方法
HttpStatus status = this.getStatus(request);//获取status
String path = (String) webRequest.getAttribute("javax.servlet.error.request_uri", 0);
String message = (String) webRequest.getAttribute("javax.servlet.error.message", 0);
if(message.equals("")){
message = "No Available Message";
}
//携带错误数据信息
model.addAttribute("timestamp", new Date());
model.addAttribute("statusCode", status.value());
model.addAttribute("error", status.getReasonPhrase());
model.addAttribute("message", message);
model.addAttribute("path", path);
int i = status.value() / 100;//判断是4xx还是5xx错误
if(i == 4){
return "layuimini/page/error/4xx";//使用自己定制的错误页面
}else if(i == 5){
return "layuimini/page/error/5xx";//使用自己定制的错误页面
}
return null;
}
@RequestMapping//返回给客户端
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
WebRequest webRequest = new ServletWebRequest(request);//对request进行包装,目的是能操作更多的方法
HttpStatus status = this.getStatus(request);//获取status
Map<String, Object> map = new HashMap<>();
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
String path = (String) webRequest.getAttribute("javax.servlet.error.request_uri", 0);
String message = (String) webRequest.getAttribute("javax.servlet.error.message", 0);
map.put("timestamp", new Date());
map.put("statusCode", status.value());
map.put("error", status.getReasonPhrase());
map.put("message", message);
map.put("path", path);
return new ResponseEntity(map, status);
}
}
protected HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
} else {
try {
return HttpStatus.valueOf(statusCode);
} catch (Exception var4) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
}
@Override
public String getErrorPath() {
return "null";
}
}
SpringBoot的错误页面机制的自适应,是由BasicErrorController实现的,而这个BasicErrorController只有在容器中没有ErrorController的情况下,才会被注册进容器,因此我们创建一个实现了ErrorController接口的类,这个BasicErrorController就失效,然后我们仿照BasicErrorController里面的方法来实现自己的controller就可以了。如下:
@Bean
@ConditionalOnMissingBean(
value = {ErrorController.class},//没有ErrorController才会去注册BasicErrorController
search = SearchStrategy.CURRENT
)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(), (List)errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
3. 定制错误数据
https://blog.csdn.net/qq_40634846/article/details/107488716
3.1 自定义错误异常UserNotExistException
定义异常类并继承RuntimeExcception,如下:
public class UserNotExistException extends RuntimeException{
public UserNotExistException() {
super("用户不存在");
}
}
定义controller,如下:
@ResponseBody
@RequestMapping("/hello")
public String hello(@RequestParam("user")String user){
if(user.equals("aaa")){
throw new UserNotExistException();
}
return "hello world!";
}
并且在错误页面获取某些错误信息,如下:
<h1>status:[[ ${status} ]]</h1>
<h2>timestamp:[[ ${timestamp} ]]</h2>
<h2>exception:[[ ${exception} ]]</h2>
<h2>message:[[ ${message} ]]</h2>
浏览器地址输入localhost:8080/crud/hello?user=aaa,就会来到controller并抛出UserNotExistException异常。效果如下:
如果错误页面上的exception为空白,只需在application.properties上配置server.error.include-exception=true
。原理可以参考SpringBoot获取不到${exception}或Day41——错误处理原理&定制错误页面。
3.2 初步定制错误数据
前面已经定义好错误异常UserNotExistException,现在定义处理这个UserNotExistException的controller。如下:
MyExceptionHandler.java
@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;
}
这里定制错误数据code以及message。方法返回值的类型是json。因此,这样的定制错误数据,浏览器或客户端返回的都是json类数据。
3.3 改进定制错误数据——自适应
根据前面学习过的错误原理机制。我们只需将controller方法的返回值改成返回到/error,相当于发送了error请求,这样BasicErrorController就会根据Request Headers请求头的Accept属性决定发送哪种类型的错误视图,是发json数据,还是发text/html数据。这样就能完成自适应了。
如下:
//2. 方式二:自适应返回数据
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String, Object> map = new HashMap<>();
map.put("code", "user.notexist");
map.put("message", "用户出错了");
//实现自适应性,使得能通过发送器/error请求去到错误页面的处理方法
return "forward:/error";
}
效果:
浏览器的错误页面
其他客户端的数据
现在虽然能自适应了,但是状态码却变成了200,而且错误页面也不是我们自己定制的错误页面。
3.4 实现自适应并前往自定制错误页面
3.4.1 分析
从前面的效果可以看到状态码变成了200,而不是SpringBoot默认的4xx/5xx。因此我们应该将状态码设置成4xx/5xx再将视图返回出去。
在BasicErrorController中的errorHtml()中有一句是设置状态码,如下:(注意注释)
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);//2.调用getStatus()获取status
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());//1.设置响应状态码status
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
如上面代码中所述,我们查看getStatus()是什么内容,如下:(注意注释)
protected HttpStatus getStatus(HttpServletRequest request) {
//1.原来它是通过request.getAttribute("javax.servlet.error.status_code")获取状态码
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
return HttpStatus.valueOf(statusCode);
}
catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
如上代码所述,由此 我们可以在异常处理的controller方法中设置状态码,然后这个状态码会被上面的getStatus()方法拿出来,从而实现前往自己的错误页面。
3.4.2 实现
//2. 方式二:自适应返回数据
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String, Object> map = new HashMap<>();
/**
* 传入自己的错误状态码4xx,5xx,使得springboot去到自己定制的错误页面。
* 否则不会进入定制错误页面的解析流程
*/
/**
* SpringBoot是这样取得状态码的:
* Integer statusCode =
* (Integer) request.getAttribute("javax.servlet.error.status_code");
*/
//因此我们识用request.setAttribute即可
request.setAttribute("javax.servlet.error.status_code", 500);
map.put("code", "user.notexist");
map.put("message", "用户出错了");
//实现自适应性,使得能通过发送器/error请求去到错误页面的处理方法
return "forward:/error";
}
在错误页面显示出code、message,如下:
<h1>status:[[ ${status} ]]</h1>
<h2>timestamp:[[ ${timestamp} ]]</h2>
<h2>exception:[[ ${exception} ]]</h2>
<h2>message:[[ ${message} ]]</h2>
<h2>company:[[ ${company} ]]</h2>
<h2>code:[[ ${ext.code} ]]</h2>
<h2>message:[[ ${ext.message} ]]</h2>
效果:
客户端也没有自己定制的错误数据,如下:
总结:能来到自己定制的错误页面,但是与自己定制的错误数据不同
3.5 将自己定制的数据携带出去
3.5.1 分析
将数据封装到视图中是发生在BasicErrorController中的errorHtml()以及error()中的,如下:(注意注释)
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
//2. model是通过getErrorAtributes()获取到的
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//1. model被封装ModelAndView对象中
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
//1.也是通过调用getErrorAttributes()获取到数据
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
getErrorAtributes()是AbstractErrorController的方法,而且AbstractErrorController实现了ErrorController接口(这里很关键),如下:
public abstract class AbstractErrorController implements ErrorController {
...
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
boolean includeStackTrace) {
WebRequest webRequest = new ServletWebRequest(request);
//1.当前方法又调用了errorAttributes.getErrorAttributes()获取数据
return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
}
总结:不管是errorHtml()方法还是error()方法,封装的错误数据是从AbstractErrorController的getErrorAttributes获取到的(而且AbstractErrorController实现了ErrorController接口)。getErrorAttributes()里面又调用了errorAttributes.getErrorAttributes()。
从上面代码看到,最终是errorAttributes调用getErrorAttributes()获取数据。
看看这个getErrorAttributes()内容,如下:
public interface ErrorAttributes {
Map<String, Object> getErrorAttributes(WebRequest webRequest,
boolean includeStackTrace);
Throwable getError(WebRequest webRequest);
}
如上所示,它是一个接口,将光标放在ErrorAttributes,按ctrl+alt+b查看它的实现类,如下:
原来DefaultErrorAttributes是它的实现类。
DefaultErrorAttributes里面的getErrorAttributes()方法如下:
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap();
errorAttributes.put("timestamp", new Date());
this.addStatus(errorAttributes, webRequest);
this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
this.addPath(errorAttributes, webRequest);
return errorAttributes;
}
如上所示,最终找到了封装的错误数据是从DefaultErrorAttributes.getErrorAttributes获得的。
总结:最终最终,封装的错误数据是从DefaultErrorAttributes.getErrorAttributes获得的。
3.5.2 将数据携带出去的过程
SringBoot自动配置是这样做的:
出现错误,发送/error请求,来到BasicErrorController,根据自适应,去errorHtml()或者error()方法及逆行处理。处理的过程中,响应的数据由 getErrorAttributes()(它是AbstractErrorController(实现了ErrorController接口)的方法) 得到,最终会调用errorAttributes.getErrorAttributes()得到数据。最终最终会调用DefaultErrorAttributes的getErrorAttributes()得到数据。
总结一:因此,我们可以编写AbstractController的子类,放在容器中,从而将自己定制的错误数据携带出去。但是有更简单的解决方案,后面讲
自动配置是以上的做法,我们查看ErrorMvcAutoConfiguration,如下:(注意注释)
@Bean
//1.当没有ErrorController,才会注册BasicErrorControlelr。才会有以上的阐述的做法。
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes,
this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
总结二:也就是我们完全可以编写一个ErrorController的实现类,放在容器中,从而将自己定制的错误数据携带出去。但是有更简单的解决方案,后面讲
因为最终会调用DefaultErrorAttributes的getErrorAttributes()得到数据。所以来看看自动配置类ErrorMvcAutoConfiguration中是怎么注册DefaultErrorAttributes的,如下:
@Bean
//1.当没有ErrorAttributes时,才会注册DefaultErrorAttributes
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
}
综上所述,可以编写一个DefaultErrorAttributes的子类来携带自己定制的错误数据。
总结三:编写一个DefaultErrorAttributes的子类来携带自己定制的错误数据。
3.5.3 将数据携带出去的方案
方案一:编写AbstractController的子类或者编写ErrorController的实现类,放到容器中。但是编写controlelr过于沉重繁琐,不推荐此方案。
方案二:编写DefaultErrorAttributes的子类或者ErrorAttributes的实现类,放到容器中。推荐此方案
3.5.4 实现
首先在处理异常的方法(发送/error之前)中将扩展的数据放在ext中,如下:
/**
* 定制错误数据
*/
@ControllerAdvice
public class MyExceptionHandler {
//2. 方式二:自适应返回数据
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String, Object> map = new HashMap<>();
/**
* 传入自己的错误状态码4xx,5xx,使得springboot去到自己定制的错误页面。
* 否则不会进入定制错误页面的解析流程
*/
/**
* SpringBoot是这样取得状态码的:
* Integer statusCode =
* (Integer) request.getAttribute("javax.servlet.error.status_code");
*/
//因此我们使用request.setAttribute即可
request.setAttribute("javax.servlet.error.status_code", 500);
map.put("code", "user.notexist");
map.put("message", "用户出错了");
request.setAttribute("ext", map);//将扩展的数据放在ext中
//实现自适应性,使得能通过发送器/error请求去到错误页面的处理方法
return "forward:/error";
}
编写DefaultErrorAttributes子类,获取SpringBoot的默认错误数据以及在异常处理的方法中自己定制的数据,如下:
package com.atguigu.springboot.component;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.util.Map;
/**
* 给容器中加入我们自己定义的ErrorAttributes
*/
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
/**
* 返回值的map就是页面和json能获得的所有字段,不仅能自适应返回html数据还是json数据,
* 还能返回xxxExceptionhandler定制的数据
*/
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
map.put("company", "atguigu");//这里也可以自定义数据
//webRequest是将requestAttributes包装起来的对象,而requestAttributes是对request包装起来的对象
// webRequest.getAttribute()获取在MyExceptionHandler中封装好的数据
//入参0表示从request域中获取,1表示从session域中获取
//ext是我们异常处理器MyExceptionHandler携带的数据
Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
map.put("ext", ext);
return map;
}
}
效果:
3.6 解决exception显示不出来
3.6.1 情况
在添加了自己编写的MyErrorAttributes的时候,exception对象的信息又显示不出来了。
3.6.2 分析
原因肯定是因为调用super.getErrorAttributes(webRequest, includeStackTrace)时,super调用了入参includeException为false的DefaultErrorAttributes构造器。
因此我们需要传入值为true的includeException到DefaultErrorAttributes构造器中。
3.6.3 解决
在自定义的ErrorAttribute中,通过构造器,将includeException=true
传给DefaultErrorAttributes
解决方案如下:
//MyErrorAttributes是我定义的DefaultErrorAttributes的子类
public MyErrorAttributes() {
//将includeException=true传给DefaultErrorAttributes,使得能获取出exception对象
super(true);
}
完整代码如下:
package com.atguigu.springboot.component;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.util.Map;
/**
* 给容器中加入我们自己定义的ErrorAttributes
*/
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
public MyErrorAttributes() {
//将includeException=true传给DefaultErrorAttributes,使得能获取出exception对象
super(true);
}
/**
* 返回值的map就是页面和json能获得的所有字段,不仅能自适应返回html数据还是json数据,
* 还能返回xxxExceptionhandler定制的数据
*/
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
map.put("company", "atguigu");//这里也可以自定义数据
//webRequest是将requestAttributes包装起来的对象,而requestAttributes是对request包装起来的对象
// webRequest.getAttribute()获取在MyExceptionHandler中封装好的数据
//入参0表示从request域中获取,1表示从session域中获取
//ext是我们异常处理器MyExceptionHandler携带的数据
Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
map.put("ext", ext);
return map;
}
}
3.7 总结
定制错误数据总结,如下: