優雅 REST 風格的資源 URL 不但願帶 .html 或 .do 等後綴。javascript
因爲早期的 Spring MVC 不能很好地處理靜態資源,因此在 web.xml 中配置 DispatcherServlet 的請求映射時,每每採用 *.do、*.xhtml 等方式。這就決定了請求 URL 必須是一個帶後綴的 URL,而沒法採用真正 REST 風格的 URL 。css
若是將 DispatcherServlet 請求映射配置爲 「/」,則 Spring MVC 將捕獲 Web容器全部的請求,包括靜態資源的請求,Spring MVC 會將它們當成一個普通請求處理,因找不到對應的處理器而致使錯誤。html
如何讓 Spring 框架可以捕獲全部 URL 的請求,同時又將靜態資源的請求轉由 Web 容器處理,是可將 DispatcherServlet 的請求映射配置爲 「/」 的前提。因爲 REST 是 Spring 的重要功能之一,因此 Spring 團隊很看重靜態資源處理這項任務,給出了堪稱經典的兩種解決方案。java
在學習這兩個方案以前,先調整 web.xml 中 DispatcherServlet 的配置,使其能夠捕獲全部的請求。web
<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 截獲。spring
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} 引用的值是同樣的,因此能夠正確訪問到物理靜態資源。這樣,在每次發佈新版本後,隨着發佈版本號的更改,客戶端就會自動下載新的靜態資源。