Spring Boot项目的404是如何发生的

问题

在日常开发中,假如我们访问一个Sping容器中并不存在的路径,通常会返回404的报错,具体原因是什么呢?

结论

错误的访问会调用两次DispatcherServlet:第一次调用无法找到对应路径时,会给Response设置一个错误状态,第二次是根据这个状态执行预先设置了error属性的DispatcherServlet。而正确的访问只会调用一次DispatcherServlet。

原理

我们知道,基于SpringMvc原理的Spring Boot项目,所有的路由请求默认都是由DispatcherServlet类来负责处理的?

如果开启断点调试会发现在第二次进入DispatcherServlet的doDispatch方法时,便直接返回了404错误:

那么问题的重点应该出现在两次DispatcherServlet的调用上,通过调试可以发现,最后一次的调用分析的意义不大,因为从它的request属性就能看出来,它预先设置了一堆的错误属性,明显就是为了返回错误,走了一遍DispatcherServlet的标准流程。

重点只有两点:

第一次执行DispatcherServlet的过程中发生了什么?

错误的请求为什么会有两次DispatcherServlet调用?

1.1 第一次执行DispatcherServlet

通过断点调试,可以看到在执行DispatcherServlet中的doDispatch方法的时候进入了HttpRequestHandlerAdapter的handle方法

java 复制代码
public class HttpRequestHandlerAdapter implements HandlerAdapter {
    public HttpRequestHandlerAdapter() {
    }
    public boolean supports(Object handler) {
        return handler instanceof HttpRequestHandler;
    }
    @Nullable
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        ((HttpRequestHandler)handler).handleRequest(request, response);
        return null;
    }
......

接着又执行了ResourceHttpRequestHandler的handleRequest方法,在执行的过程中,在容器中没有找到对应的路径,所以对response设置了404错误:

也就是

response.sendError(404)

它的底层实际上是给Response类的一个私有属性errorState,设置了错误状态:

java 复制代码
public boolean setError() {
        return this.errorState.compareAndSet(0, 1);
    }

这样设置有什么用呢?

这就涉及到第二次执行DispatcherServlet的原因了。

1.2 错误请求为什么会执行两次DispatcherServlet

重点在StandardHostValve类的invoke方法中:

java 复制代码
public final void invoke(Request request, Response response) throws IOException, ServletException {
        ......
                try {
                    //第一次执行DispatcherServlet的原因
                    if (!response.isErrorReportRequired()) {
                        //执行正常的请求
                        context.getPipeline().getFirst().invoke(request, response);
                    }
                } catch (Throwable var10) {
                    ExceptionUtils.handleThrowable(var10);
                    this.container.getLogger().error("Exception Processing " + request.getRequestURI(), var10);
                    if (!response.isErrorReportRequired()) {
                        request.setAttribute("javax.servlet.error.exception", var10);
                        this.throwable(request, response, var10);
                    }
                }

                response.setSuspended(false);
                Throwable t = (Throwable)request.getAttribute("javax.servlet.error.exception");
                if (context.getState().isAvailable()) {
                    //第二次执行DispatcherServlet的原因
                    if (response.isErrorReportRequired()) {
                        AtomicBoolean result = new AtomicBoolean(false);
                        response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
                        if (result.get()) {
                            if (t != null) {
                                this.throwable(request, response, t);
                            } else {
                                //执行错误请求
                                this.status(request, response);
                            }
                        }
                    }
                    ......

两次执行的原因是因为

response.isErrorReportRequired()

java 复制代码
public class Response implements HttpServletResponse {
public boolean isErrorReportRequired() {
     return this.getCoyoteResponse().isErrorReportRequired();
 }
......

public final class Response {

public boolean isErrorReportRequired() {
    return this.errorState.get() == 1;
}
.....

也就是根据Response类的私有属性errorState来判断的。

第一次执行DispatcherServlet的时候,由于errorState的值还是初始化值0,所以可以正常执行,执行的时候找不到对应的路径资源,便执行了response.sendError(404)方法,给errorState赋值为1。

这是StandardHostValve类中,invoke执行的

context.getPipeline().getFirst().invoke(request, response);

给errorState赋值1以后,又执行了:

this.status(request, response);

它的执行链路是这样:

status -> custom -> forward -> doForward -> processRequest ->doFilter。

也就是对本次请求执行了一次转发,最后重新调用了一遍

filterChain.doFilter(request, response)

的流程,走了错误调用。

在processRequest方法可以比较看的比较明确:

java 复制代码
private void processRequest(ServletRequest request, ServletResponse response, State state) throws IOException, ServletException {
        DispatcherType disInt = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE");
        if (disInt != null) {
            boolean doInvoke = true;
            if (this.context.getFireRequestListenersOnForwards() && !this.context.fireRequestInitEvent(request)) {
                doInvoke = false;
            }

            if (doInvoke) {
                if (disInt != DispatcherType.ERROR) {
                    state.outerRequest.setAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH", this.getCombinedPath());
                    state.outerRequest.setAttribute("org.apache.catalina.core.DISPATCHER_TYPE", DispatcherType.FORWARD);
                    this.invoke(state.outerRequest, response, state);
                } else {
                    this.invoke(state.outerRequest, response, state);
                }

                if (this.context.getFireRequestListenersOnForwards()) {
                    this.context.fireRequestDestroyEvent(request);
......

等到DispatcherServlet再执行完一次,便能在浏览器看到404报错了。

相关推荐
极光代码工作室1 小时前
基于SpringBoot的课程预约系统
java·springboot·web开发·后端开发
Leighteen2 小时前
`try-finally` 里的 `return`:为什么 `finally` 会悄悄改掉返回值、吞掉异常
java·开发语言
名字还没想好☜3 小时前
Go 的 time.After 在 select 循环里内存泄漏:定时器堆积原理与 timer.Reset 正确姿势
java·数据库·golang·go·goroutine
圆山猫3 小时前
[Virtualization](三):RISC-V H-extension 与 Guest 执行模式
android·java·risc-v
caishenzhibiao3 小时前
期货先行者主图 同花顺期货通指标
java·c语言·c#
史呆芬3 小时前
分布式事务实战:微服务跨服务数据一致性解决方案
java·后端·spring cloud
IT界的老黄牛4 小时前
限流命中后该怎么办:直接丢、阻塞等待、延迟重投三种姿势的取舍
java·rocketmq·redisson·令牌桶·削峰·分布式限流
daad7774 小时前
记录matlab状态机demo
java·网络·matlab
牡丹雅忻15 小时前
AES 加密模式演进:从 ECB、CBC 到 GCM 的 C# 深度实践
java·开发语言·c#
paopaokaka_luck5 小时前
基于springboot3+vue3的智能文库平台(AI智能搜索、AI智能汇总、实时在线状态展示、多格式文档预览与富文本编辑、Echarts图形化分析)
前端·网络·spring boot·网络协议·echarts