springboot错误处理原理以及自定义异常处理
1.springboot默认错误处理机制
1.1浏览器:默认返回一个错误界面
1.2其他客户端:默认返回json数据
2.springboot异常处理原理
ErrorMvcAutoConfiguration 这个和错误有关的自动配置类给容器中注入了几个组件:DefaultErrorAttributes、BasicErrorController、ErrorPageCustomizer、DefaultErrorViewResolver
DefaultErrorAttributes
:在页面共享错误信息,通过getErrorAttributes
方法获取错误信息
//DefaultErrorAttributes
//@ConditionalOnMissingBean(value = ErrorAttributes.class) 在容器中没有ErrorAttributes时才注入DefaultErrorAttributes
//ErrorAttributes是一个接口,DefaultErrorAttributes是它的实现类
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
if (Boolean.TRUE.equals(this.includeException)) {
options = options.including(Include.EXCEPTION);
}
if (!options.isIncluded(Include.EXCEPTION)) {
errorAttributes.remove("exception");
}
if (!options.isIncluded(Include.STACK_TRACE)) {
errorAttributes.remove("trace");
}
if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
errorAttributes.put("message", "");
}
if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}
return errorAttributes;
}
BasicErrorController
:处理默认的/error
请求(在这里区分是浏览器的请求还是客户端的请求)
//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()));
}
@Controller
//读取配置文件中server.error.path的值,如果没有就读error.path,如果也没有则用/error
@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);
}
}
-
怎么区分是浏览器发送是的请求还是客户端发送的请求呢?因为它们的请求头不同
浏览器发送的请求的请求头
其他客户端发送的请求的请求头 -
ErrorPageCustomizer
: 当系统出现错误时,通过getPath()获取到错误请求路径(/error
),来到/error
请求进行处理
//ErrorPageCustomizer
@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
//注册错误页面的响应规则
//getPath()-->@Value("${error.path:/error}") private String path = "/error";
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
}
DefaultErrorViewResolver
:默认异常视图处理器
//DefaultErrorViewResolver
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
//客户端错误用4xx表示 服务器端错误用5xx表示
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);
}
@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 viewName就是上面方法的status状态码
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.resources.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
处理(响应页面或者响应json数据),去哪个页面是由 DefaultErrorViewResolver
解析得到。
如:发生404.则先会去找404对应的错误页面,如果没有404对应的错误页面则去再去找4xx对应的错误页面,如果也没有则去默认的错误页面
(1)响应页面:
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
//ErrorViewResolver异常视图的解析器 所有的ErrorViewResolver得到ModelAndView
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
3.springboot自定义异常处理界面或消息
3.1定制错误页面
1)、有模板引擎的情况下:error/状态码
将错误页面命名为:错误状态码. html,并且放在模板引擎文件夹里面的error文件夹下,发生此状态码对应的错误请求时就会来到该页面。可以在该文件夹下使用4xx或者5xx作为错误页面的文件名来匹配以4开头或者5开头这些类型的所有错误 。(精确优先,优先寻找精确状态码.html)
错误页面能获取的信息:timestamp:时间戳status:状态码error:错误提示exception:异常对象message:异常消息errors:JSR303数据校验的错误都在这里。DefaultErrorAttributes中getErrorAttributes()方法中返回的errorAttributes中的信息都能在页面获取
<!--thymeleaf模板引擎-->
<h1>status:[[${status}]]</h1>
<h2>timestamp:[[${timestamp}]]</h2>
2)、没有模板引擎(模板引擎找不到这个错误页面),则在静态文件夹下找
3)、以上都没有找到错误页面,则来到springboot的默认的错误页面
- 返回springboot默认页面:
public class BasicErrorController extends AbstractErrorController {
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
...
//在找不到时返回error视图
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
}
//ErrorMvcAutoConfiguration 中注入了一个error视图
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
private final StaticView defaultErrorView = new StaticView();
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
}
}
3.2定制错误json数据
自定义一个异常
public class UserNotExistsException extends RuntimeException {
public UserNotExistsException(){
super("用户不存在");
}
}
1.存在问题:不能自适应,客户端和浏览器都是json数据
//异常处理器 需要用这个@ControllerAdvice注解
@ControllerAdvice
public class MyExceptionHandler {
//1、缺点:没有自适应效果,浏览器和客户段返回的都是json类型数据
//出现异常springmvc就会调用这个方法将异常对象传进来
@ResponseBody
//需要处理什么类型的异常:UserNotExistsException.class
@ExceptionHandler(UserNotExistsException.class)
//处理所有异常
//@ExceptionHandler(Exception.class)
public Map<String,Object> handlerException(Exception e){
//响应成自己想要的json数据
Map<String,Object> map = new HashMap<>();
map.put("code","user.not exists");
map.put("message",e.getMessage());
return map;
}
}
2、存在问题:没有传入错误状态码
@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",e.getMessage());
//转发到/error
return "forward:/error";
}//能自适应,但是网页版的不是到自己定制的页面,是默认的空白页面( ),而且状态码是200
3、存在问题:设置错误状态码:自适应效果,但是自定义数据没有展示
@ExceptionHandler(UserNotExistsException.class)
public String handlerException(Exception e, HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
//需要传入我们自己的错误状态码 4xx 5xx,(不传的话默认是200) 否则就不会进入定制错误页面的解析流程
//源码中是从这里获取装态码的: String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
// Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
request.setAttribute("javax.servlet.error.status_code",500);
map.put("code","user.not exists");//这种写法的问题:这里面的信息是传递不出去的
map.put("message","用户出错啦~");
//转发到/error:因为BasicErrorController处理的/error请求就是自适应的
return "forward:/error";
}
4、自适应响应并且将自定义的数据携带出去:
出现错误以后,会来到/error
请求,会被BasicErrorController
(做了自适应处理)处理,网页或者是json响应出去可以获取的数据是由getErrorAttributes()
方法得到的。(是AbstractErrorController(ErrorController
)中规定的方法);
1、完全来编写一个ErrorControlle
r的实现类【或者是编写AbstractErrorControlle
r的子类】,放在容器中;(麻烦)
2、页面上能用的数据,或者是json返回能用的数据都是通过errorAttributes.getErrorAttributes()
得到;容器中DefaultErrorAttributes.getErrorAttributes();
默认进行数据处理的;所以我们自己重写DefaultErrorAttributes.getErrorAttributes()
即可
- 自定义ErrorAttributes
//给容器中加入我们自己的ErrorAttributes
@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("company","atguigu");
//异常处理器携带的数据
Map<String,Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
map.put("ext",ext);
return map;
}
}
//异常处理器 需要用这个@ControllerAdvice注解
@ControllerAdvice
public class MyExceptionHandler {
//自定义的,并且客户端浏览器都能自适应效果
@ExceptionHandler(UserNotExistsException.class)
public String handlerException(Exception e, HttpServletRequest request){
//响应成自己想要的json数据
Map<String,Object> map = new HashMap<>();
//需要传入我们自己的错误状态码 4xx 5xx,(不传的话默认是200) 否则就不会进入定制错误页面的解析流程
//源码中是从这里获取装态码的: String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
// Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
request.setAttribute("javax.servlet.error.status_code",500);
map.put("code","user.not exists");//这种写法的问题:这里面的信息是传递不出去的
map.put("message","用户出错啦~");
//输出自定义的消息code message
//1.容器中加入自定义的MyErrorAttributes
//2.将自定义的信息加入request,然后MyErrorAttributes取出来在添加到map中
request.setAttribute("ext",map);
//转发到/error:因为BasicErrorController处理的/error请求就是自适应的
return "forward:/error";
}
}
最终的效果:响应是自适应的,可以通过定制ErrorAttributes
改变需要返回的内容。