如何使用 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 DefaultServletHttpRequestHandler 来处理静态资源_第1张图片

我们可以做更多事情, 比如对静态资源请求增加判断, 判断文件是否存在, 存在再转发到 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 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()
    
    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

你可能感兴趣的:(如何使用 DefaultServlet DefaultServletHttpRequestHandler 来处理静态资源)