如何使用 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

posted @ 2019-09-15 22:32  develon  阅读(3222)  评论(0编辑  收藏  举报