先说一个前提, 在Tomcat中, 所有的请求都是交给不同的实现了 javax.servlet.Servlet 接口的实现类来处理的.
而所有请求就包括了静态资源, 今天我们就从源码层面上看看Tomcat的静态资源处理过程:
1. DefaultServlet
DefaultServlet 就是Tomcat中负责处理静态资源的Servlet, service() 方法是请求处理入口, 可以看到, 这里所有的请求都交给了 doGet() 方法, 而 doGet() 又调用了 serveResource() 方法来处理.
public class DefaultServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if (req.getDispatcherType() == DispatcherType.ERROR) {
doGet(req, resp);
} else {
super.service(req, resp);
}
}
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
// Serve the requested resource, including the data content
serveResource(request, response, true, fileEncoding);
}
}
那接下来看看 serveResource() 方法
protected void serveResource(HttpServletRequest request,
HttpServletResponse response,
boolean content,
String encoding)
throws IOException, ServletException {
...
// 1. 获取静态资源的相对路径
String path = getRelativePath(request, true);
...
if (path.length() == 0) {
// 根路径, 按照文件目录处理, 暂不关注
doDirectoryRedirect(request, response);
return;
}
// 2. 尝试获取网络资源
WebResource resource = resources.getResource(path);
// 一些校验和响应属性设置
...
ServletOutputStream ostream = null;
PrintWriter writer = null;
if (serveContent) {
// 3. 获取响应输出流
try {
ostream = response.getOutputStream();
} catch (IllegalStateException e) {
...
}
}
if (resource.isDirectory() ||
isError ||
( (ranges == null || ranges.isEmpty())
&& request.getHeader("Range") == null ) ||
ranges == FULL ) {
// 资源是目录或者其他特殊情况, 我们先不看
...
} else {
if ((ranges == null) || (ranges.isEmpty()))
return;
// Partial content response.
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
if (ranges.size() == 1) {
Range range = ranges.get(0);
response.addHeader("Content-Range", "bytes "
+ range.start
+ "-" + range.end + "/"
+ range.length);
long length = range.end - range.start + 1;
response.setContentLengthLong(length);
if (contentType != null) {
if (debug > 0)
log("DefaultServlet.serveFile: contentType='" +
contentType + "'");
response.setContentType(contentType);
}
if (serveContent) {
try {
response.setBufferSize(output);
} catch (IllegalStateException e) {
// Silent catch
}
if (ostream != null) {
if (!checkSendfile(request, response, resource,
range.end - range.start + 1, range))
// 4. 将资源写入输出流
copy(resource, ostream, range);
} else {
// we should not get here
throw new IllegalStateException();
}
}
} else {
response.setContentType("multipart/byteranges; boundary="
+ mimeSeparation);
if (serveContent) {
try {
response.setBufferSize(output);
} catch (IllegalStateException e) {
// Silent catch
}
if (ostream != null) {
// 4. 将资源写入输出流
copy(resource, ostream, ranges.iterator(), contentType);
} else {
// we should not get here
throw new IllegalStateException();
}
}
}
}
}
这个就是请求到达DefaultServlet后资源的获取及响应流程, 重点的方法有几个:
getRelativePath() 解析资源相对路径
resources.getResource() 获取资源
copy() 将资源写入到输出流
那么你可能会问, Tomcat怎么知道什么请求是静态资源要交给DefaultServlet处理?
2. Mapper组件
我们知道, Mapper组件的作用是在请求到达Tomcat后, 通过 map() 方法映射返回Wrapper(Servlet的包裹类) 的
详细信息可以到这篇文章:
而在MappedContext中有一个属性ContextVersion, ContextVersion中又有几个重要属性:
protected static final class ContextVersion extends MapElement<Context> {
...
public MappedWrapper defaultWrapper = null;
public MappedWrapper[] exactWrappers = new MappedWrapper[0];
public MappedWrapper[] wildcardWrappers = new MappedWrapper[0];
public MappedWrapper[] extensionWrappers = new MappedWrapper[0];
...
}
这里后面几个Wrapper集合, 对应的是不同的匹配规则匹配出来的Wrapper, 而第一个defaultWrapper则是对应配置为"/"的Servlet的Wrapper, 在我们不做覆盖配置的情况下, 这里就是DefaultServlet的包裹Wrapper了.
在Mapper的map()方法中, Tomcat会根据请求信息找到对应的应用下的所有上面的这些Wrapper, 并和上面的三个Wrapper集合进行比对, 如果找到能处理的Wrapper返回, 如果找不到就返回defaultWrapper, 也就是DefaultServlet来当做静态资源来处理了.