如何使用 DefaultServlet DefaultServletHttpRequestHandler 來處理靜態資源


我們都知道 Tomcat 是 Servlet 容器, 而 DefaultServlet 就是 Tomcat 的 Servlet 實現, 能夠處理對靜態資源的 HttpServletRequest 請求
然而它既不是 Spring MVC 的組件, 也很難實例化(反正我是失敗了)

如果能夠使用 DefaultServlet 來提供容器的服務就好了, 經過研究發現 Spring 框架提供了一個類: DefaultServletHttpRequestHandler , 它能轉發靜態資源的請求
源代碼如下:

package org.springframework.web.servlet.resource;

import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.util.StringUtils;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.context.ServletContextAware;

public class DefaultServletHttpRequestHandler implements HttpRequestHandler, ServletContextAware {

	/** Default Servlet name used by Tomcat, Jetty, JBoss, and GlassFish */
	private static final String COMMON_DEFAULT_SERVLET_NAME = "default";

	/** Default Servlet name used by Google App Engine */
	private static final String GAE_DEFAULT_SERVLET_NAME = "_ah_default";

	/** Default Servlet name used by Resin */
	private static final String RESIN_DEFAULT_SERVLET_NAME = "resin-file";

	/** Default Servlet name used by WebLogic */
	private static final String WEBLOGIC_DEFAULT_SERVLET_NAME = "FileServlet";

	/** Default Servlet name used by WebSphere */
	private static final String WEBSPHERE_DEFAULT_SERVLET_NAME = "SimpleFileServlet";


	private String defaultServletName;

	private ServletContext servletContext;

	/**
	 * Set the name of the default Servlet to be forwarded to for static resource requests.
	 */
	public void setDefaultServletName(String defaultServletName) {
		this.defaultServletName = defaultServletName;
	}

	/**
	 * If the {@code defaultServletName} property has not been explicitly set,
	 * attempts to locate the default Servlet using the known common
	 * container-specific names.
	 */
	@Override
	public void setServletContext(ServletContext servletContext) {
		this.servletContext = servletContext;
		if (!StringUtils.hasText(this.defaultServletName)) {
			if (this.servletContext.getNamedDispatcher(COMMON_DEFAULT_SERVLET_NAME) != null) {
				this.defaultServletName = COMMON_DEFAULT_SERVLET_NAME;
			}
			else if (this.servletContext.getNamedDispatcher(GAE_DEFAULT_SERVLET_NAME) != null) {
				this.defaultServletName = GAE_DEFAULT_SERVLET_NAME;
			}
			else if (this.servletContext.getNamedDispatcher(RESIN_DEFAULT_SERVLET_NAME) != null) {
				this.defaultServletName = RESIN_DEFAULT_SERVLET_NAME;
			}
			else if (this.servletContext.getNamedDispatcher(WEBLOGIC_DEFAULT_SERVLET_NAME) != null) {
				this.defaultServletName = WEBLOGIC_DEFAULT_SERVLET_NAME;
			}
			else if (this.servletContext.getNamedDispatcher(WEBSPHERE_DEFAULT_SERVLET_NAME) != null) {
				this.defaultServletName = WEBSPHERE_DEFAULT_SERVLET_NAME;
			}
			else {
				throw new IllegalStateException("Unable to locate the default servlet for serving static content. " +
						"Please set the 'defaultServletName' property explicitly.");
			}
		}
	}


	@Override
	public void handleRequest(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		RequestDispatcher rd = this.servletContext.getNamedDispatcher(this.defaultServletName);
		if (rd == null) {
			throw new IllegalStateException("A RequestDispatcher could not be located for the default servlet '" +
					this.defaultServletName + "'");
		}
		rd.forward(request, response);
	}

}

我們可以寫一個控制器, 提供一個注解了 @RequestMapping 但沒有映射路徑的方法, 它將成為除了 *.jsp 之外的所有請求的最后一道 Handler . (Spring 5.2.0 好像刪除了該特性)
.jsp 請求是不被 DispatcherServlet 處理的, 這將導致不由 Spring 控制的 404 等錯誤, 即使 Servlet Mapping 設置的是 "/".
要想 DispatcherServlet 真正意義上地處理所有請求, 可以加上 "
.jsp" 映射, 不過這將導致 *.jsp 請求無法被編譯, 它最多作為文本文件發送給用戶.

需要注意的是 DefaultServletHttpRequestHandler 需要調用 setServletContext() 注入一個 ServletContext 實例, ServletContext 實例可以通過 WebApplicationContext 實例獲得, 同時它們都是 Spring 框架的組件, 可以在組件鏈上自動填充.

package spring.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler;

import develon.lib.Log;
import spring.tool.ContextTool;

@Controller
public class DefautlController {
	public static DefaultServletHttpRequestHandler defaultServletHandler = null; // 該對象可以轉發靜態資源請求到容器, 但是無法處理 .jsp 文件
	
	{
		if (defaultServletHandler == null) {
			defaultServletHandler = new DefaultServletHttpRequestHandler();
			defaultServletHandler.setServletContext(ContextTool.getServletContext());
		}
	}
	
	@RequestMapping(name = "default")
	public void forwardToDefaultServlet(HttpServletRequest request, HttpServletResponse response) {
		try {
			defaultServletHandler.handleRequest(request, response);
			Log.d("代理: " + request.getRequestURI() + "->" + response.getStatus());
		} catch (Exception e) {
			e.printStackTrace();
			response.setStatus(500);
		}
	}

}

現在我們甚至可以不需要顯示配置默認 Servlet 的處理了,

	@Override
	public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
//		configurer.enable();
	}

看看效果如何:

我們可以做更多事情, 比如對靜態資源請求增加判斷, 判斷文件是否存在, 存在再轉發到 DefaultServlet 上

package spring.controller;

import java.io.File;
import java.util.HashMap;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler;

import spring.tool.ContextTool;

@Controller
public class DefautlController {
	public static DefaultServletHttpRequestHandler defaultServletHandler = null; // 該對象可以轉發靜態資源請求到容器, 但是無法處理 .jsp 文件
	public static HashMap<String, Boolean> staticFiles = new HashMap<>(); // 用哈希表存放請求文件是否存在的緩存, 避免每次都訪問文件系統
	
	{
		if (defaultServletHandler == null) {
			defaultServletHandler = new DefaultServletHttpRequestHandler();
			defaultServletHandler.setServletContext(ContextTool.getServletContext());
		}
	}
	
	@RequestMapping(name = "default")
	public void forwardToDefaultServlet(HttpServletRequest request, HttpServletResponse response) {
		try {
			String path = request.getServletPath();
			Boolean isExists = staticFiles.get(path); // default null
			if (isExists == null) {
				boolean pathExists = new File(ContextTool.getServletContext().getRealPath(path)).exists();
				isExists = pathExists;
				staticFiles.put(path, pathExists);
			}
			if (isExists)
				defaultServletHandler.handleRequest(request, response);
			else
				response.setStatus(404);
			switch (response.getStatus()) {
			case 200:
			case 301:
			case 302:
			case 304:
			case 404:
				break;
			default: // 將其它狀態碼統一為 502
				response.setStatus(502);
			}
		} catch (Exception e) {
			e.printStackTrace();
			response.setStatus(500); // 轉發異常, 發送 500 狀態碼
		}
	}

}

這樣就不會有多余的 Context 了

$ curl sm/Log/js/index.sfd -v
* STATE: INIT => CONNECT handle 0x6000579a0; line 1404 (connection #-5000)
* Added connection 0. The cache now contains 1 members
* STATE: CONNECT => WAITRESOLVE handle 0x6000579a0; line 1440 (connection #0)
*   Trying 192.168.126.1...
* TCP_NODELAY set
* STATE: WAITRESOLVE => WAITCONNECT handle 0x6000579a0; line 1521 (connection #0)
* Connected to sm (192.168.126.1) port 80 (#0)
* STATE: WAITCONNECT => SENDPROTOCONNECT handle 0x6000579a0; line 1573 (connection #0)
* Marked for [keep alive]: HTTP default
* STATE: SENDPROTOCONNECT => DO handle 0x6000579a0; line 1591 (connection #0)
> GET /Log/js/index.sfd HTTP/1.1
> Host: sm
> User-Agent: curl/7.59.0
> Accept: */*
>
* STATE: DO => DO_DONE handle 0x6000579a0; line 1670 (connection #0)
* STATE: DO_DONE => WAITPERFORM handle 0x6000579a0; line 1795 (connection #0)
* STATE: WAITPERFORM => PERFORM handle 0x6000579a0; line 1811 (connection #0)
* HTTP 1.1 or later with persistent connection, pipelining supported
< HTTP/1.1 404
* Server Fast Tomcat is not blacklisted
< Server: Fast Tomcat
< Content-Length: 0
< Date: Mon, 16 Sep 2019 15:46:08 GMT
<
* STATE: PERFORM => DONE handle 0x6000579a0; line 1980 (connection #0)
* multi_done
* Connection #0 to host sm left intact
* Expire cleared

更新

由於種種原因, 我還是選擇了重寫 DefaultServlet 來實現靜態資源的處理, 這其實有多種好處(比如添加Context-Type字段)

package emcat

import global
import org.apache.catalina.servlets.DefaultServlet
import java.util.HashMap
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import java.io.File

/**
 * 靜態資源處理器
 */
class StaticServlet(val webappDir: String = "./webapps") : DefaultServlet() {
	val list = HashMap<String, Boolean>()
	
	override fun service(req: HttpServletRequest, resp: HttpServletResponse) {
		global.log("靜態請求 ${ req.getMethod() } ${ req.getRequestURI() }")
		val path = req.getServletPath()
		var isExists: Boolean? = list.get(path)
		if (isExists == null) {
			// 查詢文件存在否
			val file = File("${ webappDir }/${ path }")
			global.log(file.getAbsolutePath())
			isExists = file.exists()
			list.put(path, isExists)
		}
		if (isExists)
			return super.service(req, resp)
		global.log("404 for ${ req.getServletPath() }")
		resp.setStatus(500)
	}
}

嵌入式Tomcat + Spring + Kotlin 項目模板 : https://github.com/develon2015/MyCat


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM