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} 引用的值是一样的,所以可以正确访问到物理静态资源。这样,在每次发布新版本后,随着发布版本号的更改,客户端就会自动下载新的静态资源。