Tomcat处理静态文件DefaultServlet分析-jsp文件怎么打开

问题的起因是,用网页打开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);


  1. protected void serveResource(HttpServletRequest request,
  2. HttpServletResponse response,
  3. boolean content)
  4. throws IOException, ServletException {
  5. boolean serveContent = content;
  6. // Identify the requested resource path
  7. String path = getRelativePath(request);
  8. if (debug > 0) {
  9. if (serveContent)
  10. log("DefaultServlet.serveResource: Serving resource '" +
  11. path + "' headers and data");
  12. else
  13. log("DefaultServlet.serveResource: Serving resource '" +
  14. path + "' headers only");
  15. }
  16. // 从服务端缓存中读取资源
  17. CacheEntry cacheEntry = resources.lookupCache(path);
  18. //请求资源不存在,则返回404
  19. if (!cacheEntry.exists) {
  20. // Check if we're included so we can return the appropriate
  21. // missing resource name in the error
  22. String requestUri = (String) request.getAttribute(
  23. RequestDispatcher.INCLUDE_REQUEST_URI);
  24. if (requestUri == null) {
  25. requestUri = request.getRequestURI();
  26. } else {
  27. // We're included
  28. // SRV.9.3 says we must throw a FNFE
  29. throw new FileNotFoundException(
  30. sm.getString("defaultServlet.missingResource",
  31. requestUri));
  32. }
  33. response.sendError(HttpServletResponse.SC_NOT_FOUND,
  34. requestUri);
  35. return;
  36. }
  37. // If the resource is not a collection, and the resource path
  38. // ends with "/" or "\", return NOT FOUND
  39. if (cacheEntry.context == null) {
  40. if (path.endsWith("/") || (path.endsWith("\\"))) {
  41. // Check if we're included so we can return the appropriate
  42. // missing resource name in the error
  43. String requestUri = (String) request.getAttribute(
  44. RequestDispatcher.INCLUDE_REQUEST_URI);
  45. if (requestUri == null) {
  46. requestUri = request.getRequestURI();
  47. }
  48. response.sendError(HttpServletResponse.SC_NOT_FOUND,
  49. requestUri);
  50. return;
  51. }
  52. }
  53. boolean isError =
  54. response.getStatus() >= HttpServletResponse.SC_BAD_REQUEST;
  55. // Check if the conditions specified in the optional If headers are
  56. // satisfied.
  57. if (cacheEntry.context == null) {
  58. // Checking If headers
  59. boolean included = (request.getAttribute(
  60. RequestDispatcher.INCLUDE_CONTEXT_PATH) != null);
  61. //checkIfHeaders根据客户端请求头的If-None-Match,If-Modified-Since
  62. //来判断请求资源是否被修改,如果未被修改,则返回304头
  63. //客户端直接从客户端缓存中读取资源文件。
  64. if (!included && !isError &&
  65. !checkIfHeaders(request, response, cacheEntry.attributes)) {
  66. return;
  67. }
  68. }
  69. // Find content type.
  70. String contentType = cacheEntry.attributes.getMimeType();
  71. if (contentType == null) {
  72. contentType = getServletContext().getMimeType(cacheEntry.name);
  73. cacheEntry.attributes.setMimeType(contentType);
  74. }
  75. ArrayList<Range> ranges = null;
  76. long contentLength = -1L;
  77. if (cacheEntry.context != null) {
  78. // Skip directory listings if we have been configured to
  79. // suppress them
  80. if (!listings) {
  81. response.sendError(HttpServletResponse.SC_NOT_FOUND,
  82. request.getRequestURI());
  83. return;
  84. }
  85. contentType = "text/html;charset=UTF-8";
  86. } else {
  87. if (!isError) {
  88. //第一次访问或者ctrl+F5强制刷新或者前面资源已修改过,静态文件处理
  89. //默认返回Etag和Last-Modified,告知客户端下次访问可以
  90. //通过If-None-Match,If-Modified-Since比较返回304来判断是否可使用客户端缓存
  91. if (useAcceptRanges) {
  92. // Accept ranges header
  93. response.setHeader("Accept-Ranges", "bytes");
  94. }
  95. // Parse range specifier
  96. ranges = parseRange(request, response, cacheEntry.attributes);
  97. // ETag header
  98. response.setHeader("ETag", cacheEntry.attributes.getETag());
  99. // Last-Modified header
  100. response.setHeader("Last-Modified",
  101. cacheEntry.attributes.getLastModifiedHttp());
  102. }
  103. // Get content length
  104. contentLength = cacheEntry.attributes.getContentLength();
  105. // Special case for zero length files, which would cause a
  106. // (silent) ISE when setting the output buffer size
  107. if (contentLength == 0L) {
  108. serveContent = false;
  109. }
  110. }
  111. ServletOutputStream ostream = null;
  112. PrintWriter writer = null;
  113. if (serveContent) {
  114. // Trying to retrieve the servlet output stream
  115. try {
  116. ostream = response.getOutputStream();
  117. } catch (IllegalStateException e) {
  118. // If it fails, we try to get a Writer instead if we're
  119. // trying to serve a text file
  120. if ( (contentType == null)
  121. || (contentType.startsWith("text"))
  122. || (contentType.endsWith("xml"))
  123. || (contentType.contains("/javascript")) ) {
  124. writer = response.getWriter();
  125. // Cannot reliably serve partial content with a Writer
  126. ranges = FULL;
  127. } else {
  128. throw e;
  129. }
  130. }
  131. }
  132. // Check to see if a Filter, Valve of wrapper has written some content.
  133. // If it has, disable range requests and setting of a content length
  134. // since neither can be done reliably.
  135. ServletResponse r = response;
  136. long contentWritten = 0;
  137. while (r instanceof ServletResponseWrapper) {
  138. r = ((ServletResponseWrapper) r).getResponse();
  139. }
  140. if (r instanceof ResponseFacade) {
  141. contentWritten = ((ResponseFacade) r).getContentWritten();
  142. }
  143. if (contentWritten > 0) {
  144. ranges = FULL;
  145. }
  146. if ( (cacheEntry.context != null)
  147. || isError
  148. || ( ((ranges == null) || (ranges.isEmpty()))
  149. && (request.getHeader("Range") == null) )
  150. || (ranges == FULL) ) {
  151. // Set the appropriate output headers
  152. if (contentType != null) {
  153. if (debug > 0)
  154. log("DefaultServlet.serveFile: contentType='" +
  155. contentType + "'");
  156. response.setContentType(contentType);
  157. }
  158. if ((cacheEntry.resource != null) && (contentLength >= 0)
  159. && (!serveContent || ostream != null)) {
  160. if (debug > 0)
  161. log("DefaultServlet.serveFile: contentLength=" +
  162. contentLength);
  163. // Don't set a content length if something else has already
  164. // written to the response.
  165. if (contentWritten == 0) {
  166. if (contentLength < Integer.MAX_VALUE) {
  167. response.setContentLength((int) contentLength);
  168. } else {
  169. // Set the content-length as String to be able to use a
  170. // long
  171. response.setHeader("content-length",
  172. "" + contentLength);
  173. }
  174. }
  175. }
  176. InputStream renderResult = null;
  177. if (cacheEntry.context != null) {
  178. if (serveContent) {
  179. // Serve the directory browser
  180. renderResult = render(getPathPrefix(request), cacheEntry);
  181. }
  182. }
  183. // Copy the input stream to our output stream (if requested)
  184. if (serveContent) {
  185. try {
  186. response.setBufferSize(output);
  187. } catch (IllegalStateException e) {
  188. // Silent catch
  189. }
  190. if (ostream != null) {
  191. //这里是content输出,判断是否使用sendfile来输出大文件
  192. if (!checkSendfile(request, response, cacheEntry, contentLength, null))
  193. copy(cacheEntry, renderResult, ostream);
  194. } else {
  195. copy(cacheEntry, renderResult, writer);
  196. }
  197. }
  198. } else {
  199. //
  200. if ((ranges == null) || (ranges.isEmpty()))
  201. return;
  202. // Partial content response.
  203. response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
  204. if (ranges.size() == 1) {
  205. Range range = ranges.get(0);
  206. response.addHeader("Content-Range", "bytes "
  207. + range.start
  208. + "-" + range.end + "/"
  209. + range.length);
  210. long length = range.end - range.start + 1;
  211. if (length < Integer.MAX_VALUE) {
  212. response.setContentLength((int) length);
  213. } else {
  214. // Set the content-length as String to be able to use a long
  215. response.setHeader("content-length", "" + length);
  216. }
  217. if (contentType != null) {
  218. if (debug > 0)
  219. log("DefaultServlet.serveFile: contentType='" +
  220. contentType + "'");
  221. response.setContentType(contentType);
  222. }
  223. if (serveContent) {
  224. try {
  225. response.setBufferSize(output);
  226. } catch (IllegalStateException e) {
  227. // Silent catch
  228. }
  229. if (ostream != null) {
  230. //Range来实现资源文件部分内容传输
  231. if (!checkSendfile(request, response, cacheEntry, range.end - range.start + 1, range))
  232. copy(cacheEntry, ostream, range);
  233. } else {
  234. // we should not get here
  235. throw new IllegalStateException();
  236. }
  237. }
  238. } else {
  239. response.setContentType("multipart/byteranges; boundary="
  240. + mimeSeparation);
  241. if (serveContent) {
  242. try {
  243. response.setBufferSize(output);
  244. } catch (IllegalStateException e) {
  245. // Silent catch
  246. }
  247. if (ostream != null) {
  248. copy(cacheEntry, ostream, ranges.iterator(),
  249. contentType);
  250. } else {
  251. // we should not get here
  252. throw new IllegalStateException();
  253. }
  254. }
  255. }
  256. }
  257. }

静态文件优先从缓存中读取:

首先从存在的资源缓存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释放内存需要更细粒度的控制?


  1. protected CacheEntry cacheLookup(String lookupName) {
  2. if (cache == null)
  3. return (null);
  4. String name;
  5. if (lookupName == null) {
  6. name = "";
  7. } else {
  8. name = lookupName;
  9. }
  10. //无法被缓存的资源:/WEB-INF/lib/, /WEB-INF/classes/
  11. for (int i = 0; i < nonCacheable.length; i++) {
  12. if (name.startsWith(nonCacheable[i])) {
  13. return (null);
  14. }
  15. }
  16. //
  17. CacheEntry cacheEntry = cache.lookup(name);
  18. if (cacheEntry == null) {
  19. cacheEntry = new CacheEntry();
  20. cacheEntry.name = name;
  21. // Load entry
  22. cacheLoad(cacheEntry);
  23. } else {
  24. /*cache有效期5秒,5秒之内不检查原文件是否有修改,超过有效期,
  25. 需要验证原文件的lastModified和ContendLenth和缓存中的是否一致,
  26. 不一致清除缓存,一致则更新缓存的timestap再次5秒有效期。*/
  27. if (!validate(cacheEntry)) {
  28. if (!revalidate(cacheEntry)) {
  29. cacheUnload(cacheEntry.name);
  30. return (null);
  31. } else {
  32. cacheEntry.timestamp =
  33. System.currentTimeMillis() + cacheTTL;
  34. }
  35. }
  36. cacheEntry.accessCount++;
  37. }
  38. return (cacheEntry);
  39. }

Tomcat处理静态文件DefaultServlet分析

推荐阅读