優雅 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} 引用的值是一樣的,所以可以正確訪問到物理靜態資源。這樣,在每次發布新版本后,隨着發布版本號的更改,客戶端就會自動下載新的靜態資源。