404页面是谁返回给浏览器的?

今天来看看 404 页面是谁返回的?
最近突然想到很久之前学过的关于Servlet的一个知识点:就是当一个请求没有对应的Servlet去处理时,就会交给Tomcat的 org.apache.catalina.servlets.DefaultServlet 去处理,当 org.apache.catalina.servlets.DefaultServlet 也不知道怎么处理这个请求的时候就会抛出一个404错误。
1 先看DefaultServlet的简介吧
org.apache.catalina.servlets.DefaultServlet在Tomcat 中是一个全局的Servlet,只要是在Tomcat下面部署的Web应用都会用到它。DefaultServlet是在Tomcat的安装目录conf下面的Web.xml里面配置的。如下截图:

打开这个Web.xml文件,如下截图:


再来看一下Web.xml文件里面关于DefaultServlet的介绍:

Tomcat的安装目录conf下面Web.xml文件里面关于DefaultServlet的介绍
The default servlet for all web applications, that serves static resources. It processes all requests that are not mapped to other servlets with servlet mappings (defined either here or in your own web.xml file. This servlet supports the following initialization parameters (default values are in square brackets):
翻译成中文就是: 这个Servlet默认是为所有的Web应用处理静态资源的。它可以处理所有找不到映射的请求(它可以定义在这里也可以定义在你项目里面的web.xml。这个servlet支持下面初始化参数,默认值在方括号中。)
注意这个DefaultServlet只能处理静态资源,比如css文件、图片文件、html文件。 jsp文件 DefaultServlet是 处理不了 的,jsp文件由专门的 org.apache.jasper.servlet.JspServlet 来处理。如果一个请求看不出来是动态的还是静态的,默认当作静态请求,交给DefaultServlet处理。
2 先看404页面长什么样子
看完了DefaultServlet的介绍,我们先来启动一个JavaWeb项目故意访问一个不存在的请求看一下。

可以看到tomcat给浏览器返回了一个404页面,并且这个页面的文字是中文的,看起来好像还有css样式。这个404的html页面是谁返回的?我们用 Fiddler工具 抓包看一下Tomcat返回给浏览器的到底是什么东西?

抓到的内容如下:
<!doctype html>
<html lang="zh">
<title>HTTP状态 404 - 未找到</title>
<style type="text/css">
body {font-family:Tahoma,Arial,sans-serif;}
h1, h2, h3, b {color:white;background-color:#525D76;}
h1 {font-size:22px;} h2 {font-size:16px;}
h3 {font-size:14px;}
p {font-size:12px;}
a {color:black;}
.line {height:1px;background-color:#525D76;border:none;}
</style>
</head>
<h1>HTTP状态 404 - 未找到</h1>
<hr class="line" /><p><b>类型</b> 状态报告</p>
<p><b>消息</b> 请求的资源[/JavaWeb/index1.image]不可用</p><p><b>描述</b> 源服务器未能找到目标资源的表示或者是不愿公开一个已经存在的资源表示。</p>
<hr class="line" />
<h3>Apache Tomcat/7.0.104</h3>
</body>
</html>
可以看到Tomcat返回给浏览器的内容是一个html页面,并且这个页面上的字都是中文的。我们的JavaWeb应用并没有编写这样的html页面。看了上面关于DefaultServlet的介绍,我怀疑这个404页面是DefaultServlet返回的,我们来看一下DefaultServlet的源码。
3 DefaultServlet的源码
在Tomcat的安装目录lib文件下面有很多jar包,如下截图:

我们从DefaultServlet的完整包名 org.apache.catalina.servlets.DefaultServlet 猜测这个类应该在 catalina.jar 这个jar包里面。使用 jd-gui 反编译工具看一下 catalina.jar 包里面的代码:

可以看到DefaultServlet跟我们平时开发的Servlet是一样的,都是继承了HttpServlet。这就好办了,我们直接去找核心方法doGet、doPost、service这个三个方法。


大致看了一下源码,可以看到doPost方法里面调用的是doGet方法,doGet方法里面调用的是serveResource方法。service方法有俩个分支:一个是doGet方法,一个是调用父类(HttpServlet)的service方法。从源码可以看出来,不管是 post请求 还是 get请求 最终都会交给serveResource方法去处理,那么我们来看一下serveResource方法,如下截图:

注意看这行代码
response.sendError(404, sm.getString("defaultServlet.missingResource", new Object[] { requestUri }));
这行代码通过HttpServletResponse的sendError方法返回一个404错误。我们来看一下HttpServletResponse的源码,如下截图:


注意sendError方法有俩个参数,一个int类型,一个String类型。看到这里我怀疑刚才那个404页面上的中文就是通过这里传进去的。再回头看一下这行代码:
response.sendError(404, sm.getString("defaultServlet.missingResource", new Object[] { requestUri }));
这里面的sm.getString("defaultServlet.missingResource", new Object[] { requestUri })是什么东西?
看下源码:

还是一样从完整的包路径org.apache.tomcat.util.res.StringManager猜测出来这个StringManager类应该在 tomcat-util.jar 里面。

StringManager类的源码,如下:

当我看到Locale的时候,我已经明白为什么404页面上为啥有中文了。这不就是学JAVA时的国际化知识点吗?果然,我在Tomcat的安装目录下面找到了i18n-zh-CN.jar的jar包了。赶紧打开看一下:

我们在DefaultServlet源码里面看到这样一行代码 protected static final StringManager sm = StringManager.getManager("org.apache.catalina.servlets");传了一个包路径,刚好在tomcat-i18n-zh-CN.jar里面能找到一样的包路径,顺着这个包路径可以找到这样一个properties文件,如下截图:

把这个properties文件里面的复制出来,放在eclipse里面进行查看。然后在eclipse里面安装一个插件就可以查看properties文件里面的中文了。安装步骤如下:

搜索Properties Editor并进行Install(安装),





到这里就明白了,
response.sendError(404, sm.getString("defaultServlet.missingResource", new Object[] { requestUri }));
这里的sm.getString("defaultServlet.missingResource", new Object[] { requestUri })实际上就是 "请求的资源[{0}]不可用",里面的{0}被替换成requestUri了。

4 不能光看源码,要找证据
看到这里,我们只找到了404页面上的一部分中文数据,那剩下的中文都是在哪里传过去的呢?还有404的html代码DefaultServlet是在哪里返回的?还有这个404到底是不是在serveResource方法返回的,我们也不确定。因为我们只是看源码看到了有404这个错误,但实际上代码到底走不走这里我们都不确定。 如果你往下看DefaultServlet的源码,你会发现renderHtml这个方法。但是renderHtml这个方法跟404的html页面没有任何关系。

刚好,之前我看过阿里巴巴公司开源的神器 Arthas ,利用这个工具可以追踪一下当发生404的时候DefaultServlet类有没有被调用?如果调用了,调用了DefaultServlet类的哪些方法?
4.2 Arthas的trace命令
启动Arthas并监控tomcat的JVM进程,然后执行这个命令: trace org.apache.catalina.servlets.DefaultServlet *

使用这个命令可以监控DefaultServlet这个类的所有方法,并且打印方法的调用路径。看下Arthas的官方文档怎么说。

Arthas官方文档
https://arthas.aliyun.com/doc/trace.html
先执行trace命令,然后在浏览器上面故意访问一个不存在的地址,让它报404错误。

从上面的截图中,我们可以看到DefaultServlet类的service方法被调用了,然后service方法又调用了doGet方法,又调用了serveResource方法,最终在serveResource方法的第852行执行了 response.sendError(404, sm.getString("defaultServlet.missingResource", new Object[] { requestUri })); 这行代码,然后serveResource方法碰见return就结束了。

现在可以确定当发生404错误的时候,是由DefaultServlet类中的serveResource方法的第852行通过HttpServletResponse的sendError方法返回出去的。但是404的html代码是在哪返回出去的还是没找到。我们上面看过HttpServletResponse的源码,知道HttpServletResponse是一个抽象接口类,那404的html源码是不是在HttpServletResponse的实现类中返回的呢?我们需要想办法找到调用DefaultServlet类的service方法的地方,看看它调用DefaultServlet类的service方法传的HttpServletResponse实现类到底是什么?
4.3 Arthas的stack命令
Arthas的stack命令可以 输出当前方法被调用的调用路径 ,我们可以使用这个命令看看当发生404错误的时候,是谁在调用DefaultServlet类的service方法? 执行这个命令: stack org.apache.catalina.servlets.DefaultServlet service

通过上图可以看到DefaultServlet类的service方法是从org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:452)这里,一路调过来的。我们看下CoyoteAdapter.service的源码

可以看到HttpServletResponse的真正实现类是org.apache.catalina.connector.Response,看下Response的源码

在Response的源码里面没有发现什么重要线索,我们继续顺着stack org.apache.catalina.servlets.DefaultServlet service这个命令的结果一步一步找下去,找到最后你会发现你也找不到404的html代码到底是在哪返回的。不过stack这个命令功劳也不小,帮我们发现了是谁在调用DefaultServlet类的service方法。那接下来怎么办?怎么才能找到404的html代码到底是在哪里返回的呢?
4.2 看Tomcat真正的源码
要知道Tomcat是开源的,我们干脆去github上面把Tomcat的真正源码下载下来,直接在Tomcat的java文件里面搜索一下看看。

然后使用git把Tomcat的源码下载下来

使用git命令进行下载

下载完成使用文本编辑 Sublime Text 打开tomcat的源码。然后我们需要想想怎么搜,搜什么?我们在看一遍404页面上面都有什么?

看完之后我觉得搜 HTTP状态 404 - 未找到 这句话最合适。但是这句话肯定不是写死在代码里面的,肯定是写在properties这个国际化文件里面的,我们去tomcat-i18n-zh-CN.jar这个国际化资源jar包里面找一找。而且这句话里面的404肯定也不是写死的,因此我们只搜 HTTP状态 这个几个字。


根据搜索结果,我们知道 HTTP状态 这个几个字在下面这个文件里面:

根据上面的搜索结果,我们再搜 errorReportValve.statusHeader 这个东西,Tomcat的代码里面肯定会引用 errorReportValve.statusHeader 这个东西。

根据搜索结果我们发现了一个比较熟悉的类就是 org.apache.catalina.valves.ErrorReportValve.java 。这个类我们在stack org.apache.catalina.servlets.DefaultServlet service这个命令的结果里面看到过。这个类在调用DefaultServlet类的service方法的调用路径上面有。赶紧去看看ErrorReportValve.java的源码。

在这里我们发现了很多线索,发现了下面这些代码
reason = smClient.getString("http." + statusCode + ".reason");
description = smClient.getString("http." + statusCode + ".desc");
smClient.getString("errorReportValve.statusHeader",
然后再跟我们之前用Fiddler工具抓包,抓到的404html代码对比以下发现是一致的。那404的html代码到底是不是这些代码返回的呢?我们在看一下stack org.apache.catalina.servlets.DefaultServlet service这个命令的结果截图,就可以肯定是这些代码返回的了。




到这里就结束了,我们终于知道404这个页面是怎么返回出去的了。阿里巴巴的神器Arthas帮了我们大忙,要不是Arthas光看源码估计很难找到这些代码。
5 再说一个Servlet的小知识
我们平时开发Servlet接口时,步骤都是一样的。先继承javax.servlet.http.HttpServlet这个抽象类,然后重写doGet和doPost方法。很少有人会去重写service这个方法。那么service这个方法有什么用呢?实际上,当一个请求过来的时候,Tomcat把请求交给对应的Servlet去处理的时候Tomcat首先会调用Servlet类的service这个方法,service这个方法再根据请求的method类型决定到底是调用Servlet类的doGet还是doPost方法。 我们可以看一下javax.servlet.http.HttpServlet这个抽象类的service方法是怎么实现的,看下图:

看懂了没?当然我们也可以在自己的Servlet类里面重写这个service方法,此时当一个请求被我们的servlet处理的时候,我们自己的Servlet类的service方法就会被Tomcat通过反射被调用,你可以直接在service方法里面写相关的业务代码。也可以在service方法里面根据条件去决定调用doGet或者doPost方法。
大家注意一下自己平时写的servlet接口,你们重写doGet和doPost方法,一般都是这样写的:
1.在doGet方法里面调用doPost方法,然后在doPost里面写项目相关的业务代码。
2.在doPost方法里面调用doGet方法,然后在doGet方法里面写项目相关的业务代码。
但是你知道你为什么这样写吗?因为这样写,可以保证无论调用方调用我们的Servlet接口的时候用的是什么类型的method来调用,你这个servlet都可以处理。实际上这样写是不规范的,规范的是我们在开发servlet接口的时候就规定这个servlet只能用get或者post的方式来调用,不能随意调用。
还有你知道你为啥很自觉的就重写doGet或者doPost方法吗?因为你的老师就是这么教你的,因为你不重写doGet或者doPost方法,并且也不重写service方法的时候,你的servlet会报错。为什么会报错,看javax.servlet.http.HttpServlet的源码,如下截图:

在HttpServlet的源码里面,doGet和doPost方法默认直接就报400错误了。
6 覆盖DefaultServlet
假如我们在我们自己的JavaWeb项目的web.xml里面定义了一个Servlet并且将这个Servlet的url-pattern配置为/,就相当于我们自己的servlet覆盖了tomcat的DefaultServlet。静态资源文件还有404等等,所有意外的错误请求都得由我们自己处理了。
6.1 看SpringMVC是怎么覆盖DefaultServlet的
SpringMVC的官方文档
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-servlet

Spring MVC allows for mapping the DispatcherServlet to / (thus overriding the mapping of the container’s default Servlet), while still allowing static resource requests to be handled by the container’s default Servlet. It configures a DefaultServletHttpRequestHandler with a URL mapping of /** and the lowest priority relative to other URL mappings.
这段英文的意思是,SpringMVC允许DispatcherServlet的url mapping映射配置为/(这将覆盖web容器里面的默认servlet), 然而SpringMVC仍然允许由WEB容器里面默认的servlet处理静态资源 。通过将DefaultServletHttpRequestHandler的URL mapping配置为/** 这个配置相对于其他的URL mapping优先级是比较低的。
This handler forwards all requests to the default Servlet. Therefore, it must remain last in the order of all other URL HandlerMappings. That is the case if you use mvc:annotation-driven . Alternatively, if you set up your own customized HandlerMapping instance, be sure to set its order property to a value lower than that of the DefaultServletHttpRequestHandler, which is Integer.MAX_VALUE.
这段英文的意思是:这个处理器(DefaultServletHttpRequestHandler)会将所有的请求转发给默认的servlet。在这之前,请确保它必须在所有的URL 请求处理中排在最后。使用 mvc:annotation-driven 这种方式,就是这种情况。如果你自定义了HandlerMapping的实例,请确保你自己的HandlerMapping的实例优先级在DefaultServletHttpRequestHandler之前,即你自定义的HandlerMapping实例的优先级的order值应该小于Integer.MAX_VALUE的值。
注意,在SpringMVC中如果你将DispatcherServlet的url mapping映射配置为/的话,DispatcherServlet就会覆盖tomcat容器中的DefaultServlet,然后SpringMVC提供了一个DefaultServletHttpRequestHandler类,这个DefaultServletHttpRequestHandler类的URL mapping是/**,然后DefaultServletHttpRequestHandler类会将静态资源转发给tomcat的DefaultServlet去处理,DefaultServletHttpRequestHandler类自己并不处理静态资源。这就是上面SpringMVC官方文档中说的:“ 然而SpringMVC仍然允许由WEB容器里面默认的servlet处理静态资源 。”
当SpringMVC的DispatcherServlet的url mapping映射配置为/的时候,SpringMVC的DispatcherServlet就会代替Tomcat的DefaultServlet处理所有的请求。当SpringMVC的DispatcherServlet发现请求是静态资源(css,html,图片)或者这个请求在SpringMVC的容器中没有匹配的HandlerMapping的时候(404),SpringMVC的DispatcherServlet会把这个请求交给SpringMVC的DefaultServletHttpRequestHandler类去处理。DefaultServletHttpRequestHandler类当然也不会处理静态资源(css,html,图片)和404错误,DefaultServletHttpRequestHandler类会寻找当前Web容器中的默认Servlet,找到之后DefaultServletHttpRequestHandler类就会把静态资源(css,html,图片)和404错误交给Web容器的Servlet去处理。
我们还是借助Arthas看下当发生404的时候SpringMVC是怎么处理的?下面的截图中的程序,我用的tomcat6。我们看了上面SpringMVC的官方文档了解到静态资源和404错误依然是用Tomcat的DefaultServlet处理的。所以,我们依然使用Arthas的trace命令来监控DefaultServlet类里面的方法有没有被调用,截图如下: 先启动项目,然后故意访问一个不存在的页面,这个请求不能是jsp。 Arthas命令: trace org.apache.catalina.servlets.DefaultServlet *

从上图可以看到,当SpringMVC的DispatcherServlet的url mapping映射配置为/的时候,发生404时Tomcat的DefaultServlet里面的doGet方法依然被调用了。接下来,我们再看一下DefaultServlet里面的doGet方法是被谁调用的。 stack org.apache.catalina.servlets.DefaultServlet doGet

从上图可以看到(从下往上看),发生404的时候,SpringMVC的调用栈如下: at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:844) 接下来 at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:859) 接下来 at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:968) 接下来 at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893) 接下来 at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:959 接下来 at org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter.handle(HttpRequestHandlerAdapter.java:51) 接下来 at org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler.handleRequest(DefaultServletHttpRequestHandler.java:122) 终于看到 DefaultServletHttpRequestHandler 这个类了,我们来看下 DefaultServletHttpRequestHandler 的源码,如下截图:

在DefaultServletHttpRequestHandler源码这里可以看到,SpringMVC将请求转发给Web容器的默认Servlet了。
所以说,即使SpringMVC的DispatcherServlet的url mapping映射配置为/时,SpringMVC也是不处理静态资源和404错误的。静态资源和404错误依然是有Web容器的默认Servlet去处理的。

6.2 将Web容器中默认的Servlet名字修改掉
The caveat to overriding the / Servlet mapping is that the RequestDispatcher for the default Servlet must be retrieved by name rather than by path. The DefaultServletHttpRequestHandler tries to auto-detect the default Servlet for the container at startup time, using a list of known names for most of the major Servlet containers (including Tomcat, Jetty, GlassFish, JBoss, Resin, WebLogic, and WebSphere). If the default Servlet has been custom-configured with a different name, or if a different Servlet container is being used where the default Servlet name is unknown, then you must explicitly provide the default Servlet’s name, as the following example shows:
这段英文的意思是:SpringMVC获取Web容器中的默认Servlet的时候,是依赖Web容器中默认的名字去找的,不是依赖类路径去找的。DefaultServletHttpRequestHandler会尝试按照默认的名字去Web容器中找,主流的Web容器有这几个(Tomcat, Jetty, GlassFish, JBoss, Resin, WebLogic, and WebSphere)。如果你修改了Web容器中默认的Servlet名字,你必须主动告诉SpringMVC,像下面这样。

我们还是来看一下DefaultServletHttpRequestHandler的源码,如下截图:

我们来试一下,我们把Web容器中默认的Servlet名字改掉,然后不告诉SpringMVC的话,会发生什么情况。 首先去Tomcat的安装目录conf下面打开web.xml将默认的Servlet名字改掉。


然后启动项目,访问一个静态资源或者不存在的资源,看一下:

可以看到SpringMVC报错了。

再来看下一DefaultServletHttpRequestHandler的源码,如下截图:

我们将修改后的名字告诉SpringMVC,再来看一下。

<mvc:default-servlet-handler default-servlet-name="default123"/>
然后启动项目,访问一个静态资源或者不存在的资源,看一下:

可以看到SpringMVC不报错了,成功的将请求交给Web容器中默认的Servlet处理了。并且成功返回了404错误页面。
6.3 Weblogic容器
从DefaultServletHttpRequestHandler的源码里面我们知道了Weblogic容器里面默认的Servlet名字叫FileServlet。但是知道这个没用,由于Weblogic是Oracle公司的收费软件,不是开源软件。所以不像Tomcat一样这么自由,什么东西都可以自由修改和自由配置。Weblogic容器默认的Servlet名字,我到现在都不知道怎么改。类似Tomcat的conf里面的全局配置web.xml文件我都没找到。不过FileServlet这个类在weblogic.jar这个jar包里面。weblogic.jar这个jar包在 weblogic的安装路径下面:/weblogic的安装路径/weblogic/wls/wlserver_10.3/server/lib。谁知道怎么修改Weblogic容器里面默认的Servlet名字,可以评论分享一下,非常感谢。
6.3 Weblogic的404页面

用Filddler抓包抓到的内容如下:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Draft//EN">
<TITLE>Error 404--Not Found</TITLE>
</HEAD>
<BODY bgcolor="white">
<FONT FACE=Helvetica><BR CLEAR=all>
<TABLE border=0 cellspacing=5><TR><TD><BR CLEAR=all>
<FONT FACE="Helvetica" COLOR="black" SIZE="3"><H2>Error 404--Not Found</H2>
</FONT></TD></TR>
</TABLE>
<TABLE border=0 width=100% cellpadding=10><TR><TD VALIGN=top WIDTH=100% BGCOLOR=white><FONT FACE="Courier New"><FONT FACE="Helvetica" SIZE="3"><H3>From RFC 2068
<i>Hypertext Transfer Protocol -- HTTP/1.1</i>:</H3>
</FONT><FONT FACE="Helvetica" SIZE="3"><H4>10.4.5 404 Not Found</H4>
</FONT><P><FONT FACE="Courier New">The server has not found anything matching the Request-URI. No indication is given of whether the condition is temporary or permanent.</p>
<p>If the server does not wish to make this information available to the client, the status code 403 (Forbidden) can be used instead. The 410 (Gone) status code SHOULD be used if the server knows, through some internally configurable mechanism, that an old resource is permanently unavailable and has no forwarding address.</FONT></P>
</FONT></TD></TR>