在SpringBoot中,通过诸多的自动配置简化开发者的复杂的配置流程。
2020/12/12,通过尚硅谷的视频记录一下在SpringBoot2.4.0中对错误页面的定制。
在本地服务器上运行服务器。
1.SpringBoot默认的错误处理机制
模拟环境:
①首先编写一个自定义的运行期异常:
public class UserNotExistException extends RuntimeException {
public UserNotExistException() {
super("用户不存在");
}
}
②编写一个Controller处理"/hello"请求,如果入参user为字符串aaa,则抛出异常
@Controller
public class HelloController {
@ResponseBody
@RequestMapping("/hello")
public String hello(@RequestParam("user") String user) {
if (user.equals("aaa")) {
throw new UserNotExistException();
}
return "Hello World";
}
}
SpringBoot对默认错误异常请求进行处理时:
1.浏览器上会返回一个默认的错误页面
其中页面的响应头中携带"text/html",底层通过BasicErrorController处理请求,调用如下方法
@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 = resolveErrorView(request, response, status, model);
// 返回modelAndView对象进行页面跳转
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
页面通过ErrorPageCustomizer定制,在自动配置类中存在一个private static class StaticView implements View的内部类,通过以下方法自动生成页面
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
return;
}
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
Object timestamp = model.get("timestamp");
Object message = model.get("message");
Object trace = model.get("trace");
if (response.getContentType() == null) {
response.setContentType(getContentType());
}
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>");
if (message != null) {
builder.append("<div>").append(htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
}
2.如果是其他客户端访问则返回json对象
我通过Postman发送get请求,"/hello?user=aaa",返回响应json数据
其中的响应头信息
底层通过BasicErrorController处理请求,调用如下方法
@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);
}
2.参照ErrorMvcAutoConfiguration错误处理的自动配置
ErrorMvcAutoConfiguration中,给容器中添加了以下组件
1.DefaultErrorAttributes
作用 : 帮我们在页面共享信息
/**
通过这个方法获取错误信息
其中入参对象 WebRequest 继承了 RequestAttributes
*/
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = this.getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
...
return errorAttributes;
}
/** @deprecated */
/**
这个方法被弃用了,但是在上面这个方法中被调用
通过返回一个Map集合,将错误信息添加并返回
*/
@Deprecated
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;
}
2.BasicErrorController
作用 : 处理默认"/error"请求,如之前所说
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//MediaType.TEXT_HTML_VALUE = text/html
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request); // 获取状态码
//通过getErrorAttributes方法获取错误信息并封装到model对象
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);
}
3.ErrorPageCustomizer
作用 : 错误页面定制器
/**
* {@link WebServerFactoryCustomizer} that configures the server's error pages.
*/
static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
}
4.DefaultErrorViewResolver
作用 : 默认的错误视图解析器
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
@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
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);
}
}
实现流程:
首先,一旦页面出现4xx或者5xx之类错误,ErrorPageCustomizer就会生效(定制错误的响应规则),就会来到/error 请求,就会被BasicErrorController处理;
**响应页面 : 去哪个页面是由DefaultErrorViewResolver解析得到的。
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new EnumMap<>(Series.class);
//默认配置跳到/error/4xx.html或者/error/5xx.html
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) {
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;
}
3.如何定制错误响应
1)、如何定制错误的页面;
1.1)、有模板引擎的情况下;error/状态码; 【将错误页面命名为 错误状态码.html 放在模板引擎文件夹里面的 error文件夹下】, 发生此状态码的错误就会来到 对应的页面;
我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找精确的状态码.html);
页面能获取的信息;
timestamp:时间戳
status:状态码
error:错误提示
exception:异常对象
message:异常消息
errors:JSR303数据校验的错误都在这里
1.2)、没有模板引擎(模板引擎找不到这个错误页面),静态资源文件夹下找;
1.3)、以上都没有错误页面,就是默认来到SpringBoot默认的错误提示页面;
2)、如何定制错误的json数据;
2.1)、自定义异常处理&返回定制json数据;
通过自己编写一个Handler处理异常请求
@ControllerAdvice
public class MyExceptionHandler {
/**
* 浏览器客户端返回的都是json
* @param e
* @return
*/
@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;
}
}
但是我们发现在客户端中成功定制了json数据,但在页面却也响应了json,与实际效果不符。
2.2)、转发到/error进行自适应响应效果处理
所以我们可以return “forward:/error”,转发到/error进行自适应响应效果处理
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
map.put("code", "user.notExist");
map.put("message", e.getMessage());
//转发到/error
return "forward:/error";
}
虽然实现了自适应效果,但是页面返回200状态码,客户端也没有携带自定义的字段数据
分析:
出现200状态码,是因为它没有解析到错误视图,它要从request中获取状态码,而request通过getAttribute获取,所以综上所述,我们需要传入自己的状态码,才能跳转到错误页面。
再修改一遍我们的代码,可以成功跳转到400错误页面,此处不展示效果图了。
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
//传入我们自己的错误状态码 4xx 5xx
/**
Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
*/
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, 400);
map.put("code", "user.notExist");
map.put("message", e.getMessage());
//转发到/error
return "forward:/error";
}
2.3)、将我们的定制数据携带出去
出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由 getErrorAttributes得到的(是AbstractErrorController(ErrorController)规定的方法);
所以我们要想实现自定义数据,第一种办法就是完全来编写一个ErrorController的实现类【或者是编写AbstractErrorController的子类】,放在容器中;但是这个太复杂,不推荐。
第二种方法,因为页面上能用的数据,或者是json返回能用的数据都是通过errorAttributes.getErrorAttributes得到。errorAttributes的实现类是DefaultErrorAttributes,内部通过容器中DefaultErrorAttributes.getErrorAttributes()方法默认进行数据处理的;所以我们可以自定义编写ErrorAttributes并重写getErrorAttributes()方法,将组件加入容器中。
自定义errorAttributes
//给容器中加入我们自己定义的ErrorAttributes
//@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
//返回值的map就是页面和json能获取的所有字段
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
// 通过super.getErrorAttributes获取原来默认的错误信息
Map<String, Object> map = super.getErrorAttributes(webRequest, options);
//添加我们自定义的信息进去
map.put("actor", "XiaoJiang");
// 通过request获取我们在controller中保存的 异常处理器携带的数据
Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
// 并加入到map集合中
map.put("ext", ext);
return map;
}
}
在我们自定义Controller中
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
//传入我们自己的错误状态码 4xx 5xx
/**
* Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
* String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
*/
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, 400);
map.put("code", "user.notExist");
map.put("message", e.getMessage());
request.setAttribute("ext", map); // 将自定义信息加入到map
//转发到/error
return "forward:/error";
}
最终实现效果: 响应是自适应的,可以通过定制ErrorAttributes改变需要返回的内容,喜大普奔
补充说明 : 在springboot中使用thymeleaf展示数据时,遇到message和exception具体内容无法在页面中展示,需要在application.properties中设置 :
server.error.include-exception=true
server.error.include-message=always
最后,参考视频了解底层实现,如果有什么错误的地方,请大佬们指正!