背景

在使用SpringBoot的过程中,你肯定遇到过404错误。比如下面的代码:

@RestController
@RequestMapping(value = "/hello")
public class HelloWorldController {
@RequestMapping("/test")
public Object getObject1(HttpServletRequest request){
Response response = new Response();
response.success("请求成功...");
response.setResponseTime();
return response;
}
}

当我们使用错误的请求地址(POST http://127.0.0.1:8888/hello/test1?id=98)进行请求时,会报下面的错误:

{
"timestamp": "2020-11-19T08:30:48.844+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/hello/test1"
}

虽然上面的返回很清楚,但是我们的接口需要返回统一的格式,比如:

{
"rtnCode":"9999",
"rtnMsg":"404 /hello/test1 Not Found"
}

这时候你可能会想有Spring的统一异常处理,在Controller类上加@RestControllerAdvice注解。但是这种做法并不能统一处理404错误。

404错误产生的原因

产生404的原因是我们调了一个不存在的接口,但是为什么会返回下面的json报错呢?我们先从Spring的源代码分析下。

{
"timestamp": "2020-11-19T08:30:48.844+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/hello/test1"
}

为了代码简单起见,这边直接从DispatcherServlet的doDispatch方法开始分析。(如果不知道为什么要从这边开始,你还要熟悉下SpringMVC的源代码)。

... 省略部分代码....
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
... 省略部分代码

Spring MVC会根据请求URL的不同,配置的RequestMapping的不同,为请求匹配不同的HandlerAdapter。

对于上面的请求地址:http://127.0.0.1:8888/hello/test1?id=98匹配到的HandlerAdapter是HttpRequestHandlerAdapter。

我们直接进入到HttpRequestHandlerAdapter中看下这个类的handle方法。

@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
((HttpRequestHandler) handler).handleRequest(request, response);
return null;
}

这个方法没什么内容,直接是调用了HttpRequestHandler类的handleRequest(request, response)方法。所以直接进入这个方法看下吧。

@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { // For very general mappings (e.g. "/") we need to check 404 first
Resource resource = getResource(request);
if (resource == null) {
logger.trace("No matching resource found - returning 404");
// 这个方法很简单,就是设置404响应码,然后将Response的errorState状态从0设置成1
response.sendError(HttpServletResponse.SC_NOT_FOUND);
// 直接返回
return;
}
... 省略部分方法
}

整个过程并没有发生任何异常,所以不能触发Spring的全局异常处理机制

到这边还有一个问题没有解决:就是下面的404提示信息是怎么返回的。

{
"timestamp": "2020-11-19T08:30:48.844+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/hello/test1"
}

我们继续往下看。Response响应的被返回,回到org.apache.catalina.core.StandardHostValve类的invoke方法。(不要问我为什么知道是在这里?Debug的能力是需要自己摸索出来的,自己调试多了,你也就会了)

@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException { Context context = request.getContext();
if (context == null) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
sm.getString("standardHost.noContext"));
return;
} if (request.isAsyncSupported()) {
request.setAsyncSupported(context.getPipeline().isAsyncSupported());
} boolean asyncAtStart = request.isAsync();
boolean asyncDispatching = request.isAsyncDispatching(); try {
context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
if (!asyncAtStart && !context.fireRequestInitEvent(request.getRequest())) {
return;
}
try {
if (!asyncAtStart || asyncDispatching) {
context.getPipeline().getFirst().invoke(request, response);
} else {
if (!response.isErrorReportRequired()) {
throw new IllegalStateException(sm.getString("standardHost.asyncStateError"));
}
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
container.getLogger().error("Exception Processing " + request.getRequestURI(), t);
if (!response.isErrorReportRequired()) {
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
throwable(request, response, t);
}
}
response.setSuspended(false); Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
if (!context.getState().isAvailable()) {
return;
}
// 在这里判断请求是不是发生了错误,错误的话就进入StandardHostValve的status(Request request, Response response)方法。
// Look for (and render if found) an application level error page
if (response.isErrorReportRequired()) {
if (t != null) {
throwable(request, response, t);
} else {
status(request, response);
}
} if (!request.isAsync() && !asyncAtStart) {
context.fireRequestDestroyEvent(request.getRequest());
}
} finally {
// Access a session (if present) to update last accessed time, based
// on a strict interpretation of the specification
if (ACCESS_SESSION) {
request.getSession(false);
}
context.unbind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
}
}

这个方法会根据返回的响应判断是不是发生了错了,如果发生了error,则进入StandardHostValve的status(Request request, Response response)方法。这个方法“兜兜转转”又进入了StandardHostValve的custom(Request request, Response response,ErrorPage errorPage)方法。这个方法中将请求重新forward到了"/error"接口。

 private boolean custom(Request request, Response response,
ErrorPage errorPage) { if (container.getLogger().isDebugEnabled()) {
container.getLogger().debug("Processing " + errorPage);
}
try {
// Forward control to the specified location
ServletContext servletContext =
request.getContext().getServletContext();
RequestDispatcher rd =
servletContext.getRequestDispatcher(errorPage.getLocation());
if (rd == null) {
container.getLogger().error(
sm.getString("standardHostValue.customStatusFailed", errorPage.getLocation()));
return false;
}
if (response.isCommitted()) {
rd.include(request.getRequest(), response.getResponse());
} else {
// Reset the response (keeping the real error code and message)
response.resetBuffer(true);
response.setContentLength(-1);
// 1: 重新forward请求到/error接口
rd.forward(request.getRequest(), response.getResponse());
response.setSuspended(false);
}
return true;
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
container.getLogger().error("Exception Processing " + errorPage, t);
return false;
}
}

上面标号1处的代码重新将请求forward到了/error接口。所以如果我们开着Debug日志的话,你会在后台看到下面的日志。

[http-nio-8888-exec-7] DEBUG org.springframework.web.servlet.DispatcherServlet:891 - DispatcherServlet with name 'dispatcherServlet' processing POST request for [/error]
2020-11-19 19:04:04.280 [http-nio-8888-exec-7] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping:313 - Looking up handler method for path /error
2020-11-19 19:04:04.281 [http-nio-8888-exec-7] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping:320 - Returning handler method [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)]
2020-11-19 19:04:04.281 [http-nio-8888-exec-7] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory:255 - Returning cached instance of singleton bean 'basicErrorController'

上面是/error的请求日志。到这边还是没说明为什么能返回json格式的404返回格式。我们继续往下看。

到这边为止,我们好像没有任何线索了。但是如果仔细看上面日志的话,你会发现这个接口的处理方法是:

org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)]

我们打开BasicErrorController这个类的源代码,一切豁然开朗。

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
@RequestMapping(produces = "text/html")
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 ? new ModelAndView("error", model) : modelAndView);
} @RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}
... 省略部分方法
}

BasicErrorController是Spring默认配置的一个Controller,默认处理/error请求。BasicErrorController提供两种返回错误一种是页面返回、当你是页面请求的时候就会返回页面,另外一种是json请求的时候就会返回json错误。

自定义404错误处理类

我们先看下BasicErrorController是在哪里进行配置的。

在IDEA中,查看BasicErrorController的usage,我们发现这个类是在ErrorMvcAutoConfiguration中自动配置的。

@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class })
public class ErrorMvcAutoConfiguration { @Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
this.errorViewResolvers);
}
... 省略部分代码
}

从上面的配置中可以看出来,只要我们自己配置一个ErrorController,就可以覆盖掉BasicErrorController的行为。

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class CustomErrorController extends BasicErrorController { @Value("${server.error.path:${error.path:/error}}")
private String path; public CustomErrorController(ServerProperties serverProperties) {
super(new DefaultErrorAttributes(), serverProperties.getError());
} /**
* 覆盖默认的JSON响应
*/
@Override
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { HttpStatus status = getStatus(request);
Map<String, Object> map = new HashMap<String, Object>(16);
Map<String, Object> originalMsgMap = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
String path = (String)originalMsgMap.get("path");
String error = (String)originalMsgMap.get("error");
String message = (String)originalMsgMap.get("message");
StringJoiner joiner = new StringJoiner(",","[","]");
joiner.add(path).add(error).add(message);
map.put("rtnCode", "9999");
map.put("rtnMsg", joiner.toString());
return new ResponseEntity<Map<String, Object>>(map, status);
} /**
* 覆盖默认的HTML响应
*/
@Override
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
//请求的状态
HttpStatus status = getStatus(request);
response.setStatus(getStatus(request).value());
Map<String, Object> model = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.TEXT_HTML));
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
//指定自定义的视图
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}
}

默认的错误路径是/error,我们可以通过以下配置进行覆盖:

server:
error:
path: /xxx

更详细的内容请参考Spring Boot的章节。

简单总结

  • 如果在过滤器(Filter)中发生异常,或者调用的接口不存在,Spring会直接将Response的errorStatus状态设置成1,将http响应码设置为500或者404,Tomcat检测到errorStatus为1时,会将请求重现forward到/error接口;
  • 如果请求已经进入了Controller的处理方法,这时发生了异常,如果没有配置Spring的全局异常机制,那么请求还是会被forward到/error接口,如果配置了全局异常处理,Controller中的异常会被捕获;
  • 继承BasicErrorController就可以覆盖原有的错误处理方式。

Spring Boot优雅地处理404异常的更多相关文章

  1. 如何在 Spring Boot 优雅关闭加入一些自定义机制

    个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判.如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 i ...

  2. 解决spring boot中rest接口404,500等错误返回统一的json格式

    在开发rest接口时,我们往往会定义统一的返回格式,列如: { "status": true, "code": 200, "message" ...

  3. Spring Boot @ControllerAdvice+@ExceptionHandler处理controller异常

    需求: 1.spring boot 项目restful 风格统一放回json 2.不在controller写try catch代码块简洁controller层 3.对异常做统一处理,同时处理@Vali ...

  4. Spring Boot中Restful Api的异常统一处理

    我们在用Spring Boot去向前端提供Restful Api接口时,经常会遇到接口处理异常的情况,产生异常的可能原因是参数错误,空指针异常,SQL执行错误等等. 当发生这些异常时,Spring B ...

  5. Spring Boot 优雅的配置拦截器方式

    https://my.oschina.net/bianxin/blog/2876640 https://cs.xieyonghui.com/java/55.html 其实spring boot拦截器的 ...

  6. Java spring boot 2.0连接mysql异常:The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone

    解决办法:application.yml提示信息表明数据库驱动com.mysql.jdbc.Driver'已经被弃用了.应当使用新的驱动com.mysql.cj.jdbc.Driver' com.my ...

  7. Spring boot 解决 hibernate no session异常

    启动类中加入 @Beanpublic OpenEntityManagerInViewFilter openEntityManagerInViewFilter(){ return new OpenEnt ...

  8. Spring Boot 知识笔记(全局异常)

    通过ControllerAdvice和ExceptionHandler捕获异常和错误信息,向前端返回json格式的状态码及异常描述信息. 1.新建一个Controller,抛出一个异常. packag ...

  9. Spring Boot使用thymeleaf模板时报异常:template might not exist or might not be accessible by any of the configured Template Resolvers

    错误如下: template might not exist or might not be accessible by any of the configured Template Resolver ...

随机推荐

  1. 利用RNN进行中文文本分类(数据集是复旦中文语料)

    利用TfidfVectorizer进行中文文本分类(数据集是复旦中文语料) 1.训练词向量 数据预处理参考利用TfidfVectorizer进行中文文本分类(数据集是复旦中文语料) ,现在我们有了分词 ...

  2. allure测试报告

    首先如果你没有安装 pytest 库的话,先使用 pip 安装一下: pip install pytest 另外还需要安装 pytest 支持 allure 报告的插件库: pip install a ...

  3. Flink on Yarn三部曲之一:准备工作

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  4. Python字典的初识、增删改查及嵌套

    为什么要有字典? 列表可以存储大量的数据,但数据间的关联型不强 列表的查询速度相对慢 dict:字典,容器型数据类型 数据类型的分类: 可变与不可变 可变(不可哈希)的数据类型: 列表list,字典d ...

  5. C++实现求离散数学命题公式的真值表

    一.实验内容 (1)求任意一个命题公式的真值表. (2)利用真值表求任意一个命题公式的主范式. (3)利用真值表进行逻辑推理. 注:(2)和(3)可在(1)的基础上完成. 二.实验目的 真值表是命题逻 ...

  6. Linux系统安装Redis(2020最新最详细)

    2020最新Linux系统发行版ContOS7演示安装Redis 为防止操作权限不足,建议切换root用户,当然如果你对Linux命令熟悉,能够自主完成权限更新操作,可以不考虑此推荐. 更多命令学习推 ...

  7. 微服务从nacos配置中心获得配置信息

    一,安装nacos, 略 二,创建父工程和微服务工程 service1, service2,以idea为例 1, new -> project -> Maven -> 填写group ...

  8. tp5伪静态--宝塔

    location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; break; } }

  9. 今日sb题之 sdnuoj 1064

    1 #include <iostream> 2 #include <string> 3 #include <stdio.h> 4 #include <cmat ...

  10. 通过两行代码即可调整苹果电脑 Launchpad 图标大小

    之前用 13 寸 Mac 的时候我还没觉得,后来换了 16 寸就发现有点不对劲了.因为 Mac 的高分辨率,当你进入 Launchpad 界面,应用图标的大小可能会让你怀疑:这特么是苹果的设计吗?有点 ...