Spring MVC 静态资源处理

优雅 REST 风格的资源 URL 不希望带 .html 或 .do 等后缀。

由于早期的 Spring MVC 不能很好地处理静态资源,所以在 web.xml 中配置 DispatcherServlet 的请求映射时,往往采用 *.do、*.xhtml 等方式。这就决定了请求 URL 必须是一个带后缀的 URL,而无法采用真正 REST 风格的 URL 。

如果将 DispatcherServlet 请求映射配置为 “/”,则 Spring MVC 将捕获 Web容器所有的请求,包括静态资源的请求,Spring MVC 会将它们当成一个普通请求处理,因找不到对应的处理器而导致错误。

如何让 Spring 框架能够捕获所有 URL 的请求,同时又将静态资源的请求转由 Web 容器处理,是可将 DispatcherServlet 的请求映射配置为 “/” 的前提。由于 REST 是 Spring 的重要功能之一,所以 Spring 团队很看重静态资源处理这项任务,给出了堪称经典的两种解决方案。

在学习这两个方案之前,先调整 web.xml 中 DispatcherServlet 的配置,使其可以捕获所有的请求。

<servlet>
  <servlet-name>smart</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
  <servlet-name>smart</servlet-name>
  <url-pattern>/</url-pattern>
</servlet-mapping>

通过 <url-pattern>/</url-pattern> 的配置,所有 URL 请求都将被 Spring MVC 的 DispatcherServlet 截获。

1.采用 <mvc:default-servlet-handler/>

在 smart-servlet.xml 中配置 <mvc:default-servlet-handler/> 后,会在 Spring MVC 上下文中定义一个 org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler,它将充当一个检查员的角色,对进入 DispatcherServlet 的 URL 进行筛查。如果发现是静态资源的请求,就将该请求转由 Web 应用服务器默认的 Servlet 处理;如果不是静态资源的请求,则由 DispatcherServlet 继续处理。

一般 Web 应用服务器(包括 Tomcat、Jetty、Glassfish、JBoss、Resin、WebLogic 和 WebSphere)默认的 Servlet 名称都是 default,因此,DefaultServletHttpRequestHandler 可以找到它。如果用户所使用的 Web 应用服务器的默认 Servlet 名称不是 default,则需要通过 default-servlet-name 属性显式指定。

<mvc:default-servlet-handler default—serv1et—name="yourServerDefaultServlet Name"/>

2.采用 <mvc:resources/>

<mvc:default-servlet-handler/> 将静态资源的处理经由 Spring MVC 框架交回 Web 应用服务器。而 <mvc:resources/> 更进一步,由 Spring MVC 框架自己处理静态资源,并添加一些有用的附加功能。

首先,<mvc:resources/> 允许静态资源放置在任何地方,如 WEB-INF 目录下、类路径下等,甚至可以将 JavaScript 等静态文件打包到 JAR 包中。通过 location 属性指定静态资源的位置,由于 location 属性是 Resource 类型,因此可以使用诸如 "classpath:" 等的资源前缀指定资源位置。传统 Web 容器的静态资源只能放在 Web 容器的根路径下,<mvc:resources/> 则完全打破了这个限制。

其次,<mvc:resources/> 依据当前著名的 Page Speed、YSlow 等浏览器优化原则对静态资源提供优化。可以通过 cacheSeconds 属性指定静态资源在浏览器端的缓存时间,一般可将该时间设置为一年,以充分利用浏览器端的缓存。在输出静态资源时,会根据配置设置好响应报文头的 Expires 和 Cache-Control 值。

在接收到静态资源的获取请求时,会检查请求头的 Last-Modified 值。如果静态资源没有发生变化,则直接返回 303 响应状态码,指示客户端使用浏览器缓存的数据,而非将静态资源的内容输出到客户端,以充分节省带宽,提高程序性能。

在 smart-servlet.xml 中添加以下配置:

<mvc:resources mapping="/resources/**" location="/,classpath:/META—INF/publicResources/" />

以上配置将 Web 根路径 “/” 及类路径 /META-INF/publicResources/ 下的目录映射为 /resources 路径。假设 Web 根路径下拥有 images 和 js 这两个资源目录,则可以通过如下图所示的方式引用静态资源。

假设类路径 /META-INF/publicResources/ 下还拥有 images/bg1.gif 和 js/test1.js,则也可以在网页中通过 /resources/images/bg1.gif 和 /resources/js/test1.js 进行引用,如下面代码所示。

<script src="<c:url value="/resources/js/test.js"/>" type="text/javascript"></script>

由于 <mvc:resources/> 可以将多个物理路径映射为一个逻辑路径,因此,一个用逻辑路径表示的资源在多个物理路径下都存在。对于这个问题,<mvc:resources/> 的处理机制是,只要在一个物理路径下找到匹配的资源后就返回,查找的顺序和物理路径在 location 中的配置顺序一致。

聪明的读者可能会问:既然将 Web 根路径 “/” 映射为 “/resources/**”,是否可以在网页中通过 "/resources/WEB-INF/web.xml” 访问这个敏感的文件呢?答案是否定的。Spring MVC 在处理映射的静态资源时,会查看引用路径是否包含 WEB-INF 或 META-INF。如果包括,则直接返回 null 值,以保护安全文件不泄露出去。当然,如果将 /WEB-INF/ 设置在 location 属性中,则可以通过 /resources/web.xml 的 URL 查看到 web.xml。

<mvc:resources mapping="/resources/**" location="/WEB-INF/"/>

所以使用 <mvc:resources/> 时需要特别注意,不要一不小心将不期望暴露的资源泄露出去。

通过 <mvc:resources/> 的 cache-period 属性可以设置静态资源在客户端浏览器中的缓存有效时间。

<mvc:resources mapping="/resources/**" location="/,classpath:/META—INF/publicResources/"  cache-period="31536000"/>

 一般情况下,将 cache-period 设置为一年,以便充分利用客户端的缓存数据。

在发布新版本的应用时,即使服务器端的 JavaScript、css 等静态资源文件已经发生了变化,但是由于客户端浏览器本身缓存管理机制的问题,客户端并不会从服务器端下载新的静态资源。一个好的解决办法是:网页中引用静态资源的路径添加应用的发布版本号,这样在发布新的部署版本时,由于版本号的变更造成网页中静态资源路径发生更改,从而使这些静态资源成为“新的资源”,客户端浏览器就会下载这个“新的资源”,而不会使用缓存中的数据。针对这个解决思路,可以通过 <mvc:resources/> 的静态资源逻辑路径给出一个通用的解决方案。

将发布版本号包含到 <mvc:resources/> 的静态资源逻辑路径中。首先创建一个 ServletContextAware 实现类,如下面代码所示。

import javax.servlet.ServletContext;

import org.springframework.web.context.ServletContextAware;

public class ResourcePathExposer implements ServletContextAware {
    private ServletContext servletContext;
    private String resourceRoot;

    public void init() {
        String version = "1.2.1";//①在实际应用中,可以在外部属性文件或数据库中保存应用的发布版本号,在此处获取之。此处仅仅提供一个模拟值。
        resourceRoot = "/resources-" + version;//②资源逻辑路径带上应用的发布版本号
        getServletContext().setAttribute("resourceRoot", 
                getServletContext().getContextPath()+resourceRoot);//③在资源逻辑路径暴露到ServletContext的属性列表中
    }

    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    public String getResourceRoot() {
        return resourceRoot;
    }

    public ServletContext getServletContext() {
        return servletContext;
    }    
}

在 ResourcePathExposer 中获取应用程序的发布版本号,产生一个带版本号的静态资源路径 resourceRoot,同时将其值发布到 ServletContext 中,这样 JSP 文件就可以通过 ${resourceRoot} 引用其值了。

接下来要调整中的配置,以便使用带版本的静态资源逻辑路径。

<bean id="rpe" class="com.smart.web.ResourcePathExposer" init-method="init"/>
<mvc:resources mapping="#{rpe.resourceRoot}/**" location="/" cache-period="31536000"/>

在①处配置好 ResourcePathExposer,并指定其初始化方法为 init(),以便在容器启动时让其初始化 resourceRoot 的值。由于其实现了 ServletContextAware 接口,因此,Spring 会在初始化该 Bean 时将 ServletContext 引用注入进来。

在②处通过 Spring EL 表达式引用 ResourcePathExposer 的 resourceRoot 属性值,生成动态的静态资源逻辑路径。

最后调整网页中引用静态资源的方式,如下面代码所示。

<script src="<c:url value="${resourceRoot}/js/test.js"/>" type="text/javascript"></script>

由于引用的 resourceRoot 值和 <mvc:resources/> 通过 #{rpe.resourceRoot} 引用的值是一样的,所以可以正确访问到物理静态资源。这样,在每次发布新版本后,随着发布版本号的更改,客户端就会自动下载新的静态资源。

原文地址:https://www.cnblogs.com/jwen1994/p/11210845.html