您的位置:首页 > 运维架构 > Tomcat

Tomcat处理静态文件DefaultServlet分析

2014-04-16 00:38 369 查看
问题的起因是,用网页打开tomcat7服务器上一个只有静态内容的jsp页面,里面链接了gif文件,F5刷新的时候,css和gif文件请求返回304 not Mofified 头,而jsp请求还是返回200(想搞破坏,把jsp也可以在通过304返回头直接读取客户端缓存,但jsp是servlet只能在servlet容器中运行。。。)

因为jsp文件请求时tomcat的JspServlet处理的,而css和html、gif等静态文件默认是tomcat的DefaultServlet处理的(在tomcat配置文件conf/web.xml中有配置)。

先来看下tomcat7的DefaultServlet的源码:

服务端通过resource.lookupCache(path)从服务端缓存中读取资源获得CacheEntry,如果请求资源不存在CacheEntry.exists为false,则返回404。然后通过checkIfHeaders(request, response, cacheEntry.attributes)方法根据客户端请求头的If-None-Match,If-Modified-Since来判断请求资源是否被修改,如果未被修改,则返回304头,户端直接从客户端缓存中读取资源文件。如果第一次访问或者ctrl+F5强制刷新或者资源已修改过,默认返回Etag和Last-Modified头,,告知客户端下次访问可以通过If-None-Match,If-Modified-Since比较返回304来判断是否可使用客户端缓存。还有设置文件内容,content-length,range头等,最后输出内容:

if (!checkSendfile(request, response, cacheEntry, contentLength, null))

copy(cacheEntry, renderResult, ostream);

如果文件超过48k,判断是否使用sendfile来输出大文件。

请求头中通过range请求部分下载,则:

if (!checkSendfile(request, response, cacheEntry, range.end - range.start + 1, range))

copy(cacheEntry, ostream, range);

protected void serveResource(HttpServletRequest request,
HttpServletResponse response,
boolean content)
throws IOException, ServletException {

boolean serveContent = content;

// Identify the requested resource path
String path = getRelativePath(request);
if (debug > 0) {
if (serveContent)
log("DefaultServlet.serveResource:  Serving resource '" +
path + "' headers and data");
else
log("DefaultServlet.serveResource:  Serving resource '" +
path + "' headers only");
}
// 从服务端缓存中读取资源
CacheEntry cacheEntry = resources.lookupCache(path);
//请求资源不存在,则返回404
if (!cacheEntry.exists) {
// Check if we're included so we can return the appropriate
// missing resource name in the error
String requestUri = (String) request.getAttribute(
RequestDispatcher.INCLUDE_REQUEST_URI);
if (requestUri == null) {
requestUri = request.getRequestURI();
} else {
// We're included
// SRV.9.3 says we must throw a FNFE
throw new FileNotFoundException(
sm.getString("defaultServlet.missingResource",
requestUri));
}

response.sendError(HttpServletResponse.SC_NOT_FOUND,
requestUri);
return;
}

// If the resource is not a collection, and the resource path
// ends with "/" or "\", return NOT FOUND
if (cacheEntry.context == null) {
if (path.endsWith("/") || (path.endsWith("\\"))) {
// Check if we're included so we can return the appropriate
// missing resource name in the error
String requestUri = (String) request.getAttribute(
RequestDispatcher.INCLUDE_REQUEST_URI);
if (requestUri == null) {
requestUri = request.getRequestURI();
}
response.sendError(HttpServletResponse.SC_NOT_FOUND,
requestUri);
return;
}
}

boolean isError =
response.getStatus() >= HttpServletResponse.SC_BAD_REQUEST;

// Check if the conditions specified in the optional If headers are
// satisfied.
if (cacheEntry.context == null) {

// Checking If headers
boolean included = (request.getAttribute(
RequestDispatcher.INCLUDE_CONTEXT_PATH) != null);
//checkIfHeaders根据客户端请求头的If-None-Match,If-Modified-Since
//来判断请求资源是否被修改,如果未被修改,则返回304头
//客户端直接从客户端缓存中读取资源文件。
if (!included && !isError &&
!checkIfHeaders(request, response, cacheEntry.attributes)) {
return;
}

}

// Find content type.
String contentType = cacheEntry.attributes.getMimeType();
if (contentType == null) {
contentType = getServletContext().getMimeType(cacheEntry.name);
cacheEntry.attributes.setMimeType(contentType);
}

ArrayList<Range> ranges = null;
long contentLength = -1L;

if (cacheEntry.context != null) {

// Skip directory listings if we have been configured to
// suppress them
if (!listings) {
response.sendError(HttpServletResponse.SC_NOT_FOUND,
request.getRequestURI());
return;
}
contentType = "text/html;charset=UTF-8";

} else {
if (!isError) {
//第一次访问或者ctrl+F5强制刷新或者前面资源已修改过,静态文件处理
//默认返回Etag和Last-Modified,告知客户端下次访问可以
//通过If-None-Match,If-Modified-Since比较返回304来判断是否可使用客户端缓存
if (useAcceptRanges) {
// Accept ranges header
response.setHeader("Accept-Ranges", "bytes");
}

// Parse range specifier
ranges = parseRange(request, response, cacheEntry.attributes);

// ETag header
response.setHeader("ETag", cacheEntry.attributes.getETag());

// Last-Modified header
response.setHeader("Last-Modified",
cacheEntry.attributes.getLastModifiedHttp());
}

// Get content length
contentLength = cacheEntry.attributes.getContentLength();
// Special case for zero length files, which would cause a
// (silent) ISE when setting the output buffer size
if (contentLength == 0L) {
serveContent = false;
}

}

ServletOutputStream ostream = null;
PrintWriter writer = null;

if (serveContent) {

// Trying to retrieve the servlet output stream

try {
ostream = response.getOutputStream();
} catch (IllegalStateException e) {
// If it fails, we try to get a Writer instead if we're
// trying to serve a text file
if ( (contentType == null)
|| (contentType.startsWith("text"))
|| (contentType.endsWith("xml"))
|| (contentType.contains("/javascript")) ) {
writer = response.getWriter();
// Cannot reliably serve partial content with a Writer
ranges = FULL;
} else {
throw e;
}
}

}

// Check to see if a Filter, Valve of wrapper has written some content.
// If it has, disable range requests and setting of a content length
// since neither can be done reliably.
ServletResponse r = response;
long contentWritten = 0;
while (r instanceof ServletResponseWrapper) {
r = ((ServletResponseWrapper) r).getResponse();
}
if (r instanceof ResponseFacade) {
contentWritten = ((ResponseFacade) r).getContentWritten();
}
if (contentWritten > 0) {
ranges = FULL;
}

if ( (cacheEntry.context != null)
|| isError
|| ( ((ranges == null) || (ranges.isEmpty()))
&& (request.getHeader("Range") == null) )
|| (ranges == FULL) ) {

// Set the appropriate output headers
if (contentType != null) {
if (debug > 0)
log("DefaultServlet.serveFile:  contentType='" +
contentType + "'");
response.setContentType(contentType);
}
if ((cacheEntry.resource != null) && (contentLength >= 0)
&& (!serveContent || ostream != null)) {
if (debug > 0)
log("DefaultServlet.serveFile:  contentLength=" +
contentLength);
// Don't set a content length if something else has already
// written to the response.
if (contentWritten == 0) {
if (contentLength < Integer.MAX_VALUE) {
response.setContentLength((int) contentLength);
} else {
// Set the content-length as String to be able to use a
// long
response.setHeader("content-length",
"" + contentLength);
}
}
}

InputStream renderResult = null;
if (cacheEntry.context != null) {

if (serveContent) {
// Serve the directory browser
renderResult = render(getPathPrefix(request), cacheEntry);
}

}

// Copy the input stream to our output stream (if requested)
if (serveContent) {
try {
response.setBufferSize(output);
} catch (IllegalStateException e) {
// Silent catch
}
if (ostream != null) {
//这里是content输出,判断是否使用sendfile来输出大文件
if (!checkSendfile(request, response, cacheEntry, contentLength, null))
copy(cacheEntry, renderResult, ostream);
} else {
copy(cacheEntry, renderResult, writer);
}
}

} 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;
if (length < Integer.MAX_VALUE) {
response.setContentLength((int) length);
} else {
// Set the content-length as String to be able to use a long
response.setHeader("content-length", "" + 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) {
//Range来实现资源文件部分内容传输
if (!checkSendfile(request, response, cacheEntry, range.end - range.start + 1, range))
copy(cacheEntry, 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) {
copy(cacheEntry, ostream, ranges.iterator(),
contentType);
} else {
// we should not get here
throw new IllegalStateException();
}
}

}

}
}
静态文件优先从缓存中读取:
首先从存在的资源缓存cache中查找,未找到则从不存在的资源的缓存notFoundCache中查找。如果找到,检查缓存是否有效。cache默认有效期5秒,5秒之内不检查原文件是否有修改,超过有效期,需要验证原文件的lastModified和ContendLenth和缓存中的是否一致,不一致清除缓存,一致则更新缓存的timestap再次5秒有效期。(这样的话,如果修改css或js等静态文件,如果测试的人一直访问(5秒间隔内)这个页面,导致静态文件一直从服务端缓存中读取,那样无论是否强制刷新修改都不会生效啊。)未找到则生成CacheEntry,加载到相应的cache 或notFoundCache中。当然,nonCacheable数组默认/WEB-INF/lib/,
/WEB-INF/classes/路径下文件都不从缓存获取。这里cache缓存设计成一个有序数组,而notFoundCache设计为一个HashMap,cache操作稍微复杂点。难道是因为cache释放内存需要更细粒度的控制?

protected CacheEntry cacheLookup(String lookupName) {
if (cache == null)
return (null);
String name;
if (lookupName == null) {
name = "";
} else {
name = lookupName;
}
//无法被缓存的资源:/WEB-INF/lib/, /WEB-INF/classes/
for (int i = 0; i < nonCacheable.length; i++) {
if (name.startsWith(nonCacheable[i])) {
return (null);
}
}
//
CacheEntry cacheEntry = cache.lookup(name);
if (cacheEntry == null) {
cacheEntry = new CacheEntry();
cacheEntry.name = name;
// Load entry
cacheLoad(cacheEntry);
} else {
/*cache有效期5秒,5秒之内不检查原文件是否有修改,超过有效期,
需要验证原文件的lastModified和ContendLenth和缓存中的是否一致,
不一致清除缓存,一致则更新缓存的timestap再次5秒有效期。*/
if (!validate(cacheEntry)) {
if (!revalidate(cacheEntry)) {
cacheUnload(cacheEntry.name);
return (null);
} else {
cacheEntry.timestamp =
System.currentTimeMillis() + cacheTTL;
}
}
cacheEntry.accessCount++;
}
return (cacheEntry);
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: