SpringBoot异常处理原理&定制错误页面/数据
一 、原理
可以参照ErrorMvcAutoConfiguration
类,错误处理的自动配置类:
主要是给容器添加了以下组件:
ErrorPageCustomizer
@Override
// 注册错误页面
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
// 主要是这里的getPath方法,返回了下面/error的路径
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
// 系统出现错误后来到error请求进行处理(就是web.xml注册的错误页面规则)
@Value("${error.path:/error}")
private String path = "/error";
BasicErrorController(处理默认/error请求)
浏览器和客户端出现异常返回的数据是不一样的,浏览器会返回错误的页面,客户端会返回一个json数据浏览器
发送请求中请求头Accept
的数据为 text/html
客户端
(PostMan)请求头中Accept
的数据为 /
Accept : 位于请求头中,表明客户端想要接受的数据类型
Content-Type : 位于请求体中,表明客户端发送的数据类型
// 标注是一个Controller
@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);
// getErrorAttributes 方法内部会用到 DefaultErrorAttributes
// 主要就是获取响应出去的数据
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
// 去那个页面作为错误页面:包含页面地址和页面内容
// 方法内部会遍历所有的异常视图解析器
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
// 找不到视图解析器时,就返回SpringBoot默认的页面
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);
}
// getErrorAttributes主要就是获取响应出去的数据
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
// 遍历所有的ErrorViewResolver异常视图解析器
// ErrorViewResolver其实就是 DefaultErrorViewResolver
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
// 这里调用每个 DefaultErrorViewResolver 的resolveErrorView方法
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
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可以去找到某个页面
// 例如 errpr/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);
}
DefaultErrorAttributes
// 帮我们在页面共享信息
@Override
@Deprecated
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
// 向错误页面存放错误数据 errorAttributes
Map<String, Object> errorAttributes = new LinkedHashMap<>();
// timestamp 时间戳
errorAttributes.put("timestamp", new Date());
// 方法内部添加状态码和错误提示
addStatus(errorAttributes, webRequest);
// 添加异常对象exception 和异常消息message ,以及 jsr303数据校验的错误errors
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
//
addPath(errorAttributes, webRequest);
return errorAttributes;
}
方法中放到页面的数据都可以在页面中使用,例如使用模版引擎可以直接获取到这些数据
thymeleaf模版引擎:
[[${status}]]
[[${timestamp}]]
...
timestamp : 时间戳
status : 状态码
error : 错误消息
excetyipn : 异常对象
message : 异常消息
errors : jsr303数据校验的错误
二、步骤
- 一旦系统出现错误后:
ErrorPageCustomizer
就会生效(定制错误的相应机制
),然后来到/error
请求,被BasicErrorController
处理,根据不同的客户端,返回不同的数据(页面或者json数据) - 响应出去的数据都是由
getErrorAttributes
得到的(是AbstractErrorController
规定的方法)
三、定制错误页面(前后端不分离)
- 有模版引擎的情况下会去找:
error/状态码
- 在
templates
目录下创建error
目录,在error目录下创建错误页面
(以错误状态码命名的html,例如404.html
),发生此状态码的错误时就会来到对应的页面 - 可以使用
4xx.html 和 5xx.html
作为错误页面的名字来匹配这种类型的所有错误(精准优先与这种方式)
- 没有模版引擎(模版引擎找不到这个错误页面)的情况,
静态资源文件夹
下找相应的页面
只需在静态资源文件夹下创建相关的页面即可(这种情况是无法在后台渲染的
,只能返回静态的页面
) - 以上都没有错误页面的情况下,就是SpringBoot默认的错误页面
四、定制错误数据(后端统一返回json,没有页面)
/**
* 异常处理器
*/
@ControllerAdvice
public class MyExceptionHandler {
// 出现指定异常后,返回设定的数据
@ResponseBody
// 捕获 UserNotExistException 自定义的异常
@ExceptionHandler(UserNotExistException.class)
// 这里的返回值可以定义一个通用的类来封装
public Map<String,Object> userNotExist(){
Map<String,Object> result = new Hashtable<>();
result.put("code","userNotExist");
result.put("message","用户不存在");
return result;
}
}
- 上面的方法,设定后不管是浏览器还是客户端,当Controller抛出指定异常后,返回的都是我们设定的数据(
如果没有捕获这个异常则会出现500页面
) - 如果用户手动输入一些不存在的url(正常返回404页面),SpringBoot中有它自己的一套
404异常的返回数据格式
,例如
{
"timestamp": "2018-09-26T17:03:41.161+0800",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/process-instance/overview1"
}
但是这样的结果并不是我们想要的,因此需要我们来设定修改返回值:(04错误是不经过Controller的,所以使用@ControllerAdvice或@RestControllerAdvice无法获取到404错误)
(当前端的请求url不存在的时候404,会抛出NoHandlerFoundException异常
)
- 捕获NoHandlerFoundException异常
@ResponseBody
@ExceptionHandler(NoHandlerFoundException.class)
public Map<String,Object> noHandlerFound(){
Map<String,Object> result = new Hashtable<>();
result.put("code","404");
result.put("message","请求错误");
return result;
}
- 修改配置文件
# true时 当url不存在时抛出NoHandlerFoundException
spring.mvc.throw-exception-if-no-handler-found=true
#不要为我们工程中的资源文件建立映射
spring.resources.add-mappings=false
这种方式不太适用实际开发,例如如果项目使用swagger时,访问/swagger-ui.html会出现404异常