《Spring實戰》學習筆記-第六章:web檢視解析

weixin_33976072發表於2016-04-20

本章主要內容包括:

  • 將model資料展現為HTML
  • JSP檢視的使用

在前面的章節中,我們主要關注點在於編寫控制來處理web請求,同時也建立了一些簡單的檢視來展現請求返回的model資料,本章我們將主要討論在控制器完成請求處理之後和將返回結果展示到使用者的瀏覽器之前,這個過程之間發生了什麼。

理解檢視解析

在之前章節中所編寫的控制器中並沒有直接生成HTML的方法,它只是將資料填充到model中,然後將model傳送到檢視中進行展現。

Spring MVC中定義了一個ViewResolver介面:

public interface ViewResolver {

    View resolveViewName(String viewName, Locale locale) throws Exception;

}

方法resolveViewName(),當給定一個檢視名稱和一個Locale時就會返回一個View例項,View是另外一個介面:

public interface View {
    String getContentType();
    void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;

}

View介面的工作是對model、servlet請求、響應物件進行處理,並將結果輸出到response中。

看起來很簡單啊,我們所需要做的僅僅是編寫ViewResolver和View的實現類來將內容輸出到response中,並在使用者瀏覽器中進行展示即可,但真的是這樣嗎?

雖然可以編寫自定義的實現類,而且有些時候會需要一些特殊的處理,Spring提供了一些現成的實現類:

檢視解析器 描述
BeanNameViewResolver 在Spring的application context中的bean中查詢與檢視名稱相同id
ContentNegotiatingViewResolver 委託給一個或多個人檢視解析器,而選擇哪一個取決於請求的內容型別
FreeMarkerViewResolver 查詢一個基於FreeMarker的模版
InternalResourceViewResolver 在web應用程式的war檔案中查詢檢視
JasperReportsViewResolver 解析為JasperReport報表檔案
ResourceBundleViewResolver 根據屬性檔案(properties file)查詢View實現
TilesViewResolver 通過Tiles模版定義的模版解析,模版的名稱與檢視名稱相同
UrlBasedViewResolver 根據檢視名稱直接解析,當檢視名稱與物理檢視名稱匹配時
VelocityLayoutViewResolver 解析為從不同的Velocity模版組成的Velocity佈局
VelocityViewResolver 解析為Velocity模版
XmlViewResolver 根據XML檔案(/WEB_INF/views.xml)中宣告的View實現進行解析,與BeanNameViewResolver類似
XsltViewResolver 基於XSLT檢視解析

我們沒有足夠的時間和篇幅來討論所有的解析器,上面的每個解析器都對應著一個特定的檢視技術。InternalResourceViewResolver主要用於JSP,TilesViewResolver用於 Apache Tiles檢視,FreeMarkerViewResolver和VelocityViewResolver分別用於FreeMarker和Velocity模版。

建立JSP檢視

Spring對JSP檢視有兩種支援方式:

  • InternalResourceViewResolver:可以將檢視名稱解析到JSP檔案。另外,對JSP中使用的JSTL(JavaServer Pages Standard Tag Library)標籤也提供了支援。
  • Spring提供了兩種JSP標籤庫,一種是form-to-model繫結,另外一種則提供基本的功能。

InternalResourceViewResolver是最簡單也是最常用的一個解析器,下面我們來看一下它是如何使用它來完成任務。

配置JSP檢視解析器

一些檢視解析器(如ResourceBundleViewResolver)是直接的將邏輯檢視名稱對映到一個特定的View介面實現類上,而InternalResourceViewResolver則採用了另外的比較間接的方式。它採用了一種約定,通過給邏輯檢視名稱新增字首和字尾來確定web應用中對應的物理路徑。

假設有一個邏輯檢視名稱為home,如果將所有的JSP檔案都存放在/WEB-INF/views/目錄下,並且主頁的JSP名為home.jsp,那麼可以通過為home新增字首/WEB-INF/views/和字尾.jsp來找到對應的物理檢視路徑,如圖所示。

InternalResourceViewResolver通過為邏輯檢視名稱新增字首和字尾來解析檢視

可以在使用@Bean註解的類進行設定:

    // 配置一個JSP檢視解析器
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

另外,如果採用基於XML的Spring配置,也可以通過如下方式進行配置:

<bean id="viewResolver"
    class="org.springframework.web.servlet.view.InternalResourceViewResolver"
    p:prefix="/WEB-INF/views/" 
    p:suffix=".jsp" />

探索JSTL檢視

當JSP頁面使用JSTL標籤時,就需要配置InternalResourceViewResolver來解析JstlView了。

JSTL格式化標籤需要一個Locale來正確地格式化一些特定語言環境的值,如日期和幣種。訊息標籤可以使用Spring訊息源和Locale來正確地選中訊息並解析到HTML。

要解析JstlView,需要在InternalResourceViewResolver中設定viewClass屬性:

resolver.setViewClass(JstlView.class);

同樣的,xml配置中也需要進行配置:

p:viewClass="org.springframework.web.servlet.view.JstlView" 

使用Spring的JSP庫

Spring提供了兩種JSP標籤庫,一種用於將繫結了model屬性的HTML標籤進行渲染,其他的一些標籤在不同場合下可以用到。

將表單繫結到model

Spring的表單繫結JSP標籤庫共有14種,與原生HTML標籤不同的是,它們可以將一個物件繫結在model,並且可以從model物件的屬性中獲取填充值。標籤庫同時可以用來與使用者互動錯誤資訊。

要使用表單繫結標籤庫,需要在JSP頁面中進行宣告:

<%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf" %>

注意,這裡使用了sf作為字首。

JSP標籤 描述
<sf:checkbox> 生成一個checkbox型別的HTML input標籤
<sf:checkboxes> 生成一組checkbox型別的HTML input標籤
<sf:errors> 通過一個HTML <span>標籤展現欄位的錯誤
<sf:form> 生成一個HTML的<form>標籤,同時為內部標籤的繫結暴露了一個繫結路徑(binding path)
<sf:hidden> 生成一個type為hidden的Html input標籤
<sf:input> 生成一個type為text的Html input標籤
<sf:label> 生成一個HTML <label>標籤
<sf:option> 生成一個HTML的<option>標籤,根據繫結的值來設定selected屬性
<sf:options> 根據繫結的集合、陣列或者map,生成一個option標籤列表
<sf:password> 生成password型別的input標籤
<sf:radiobutton> 生成radio型別的input標籤
<sf:radiobuttons> 生成一組radio型別的input標籤
<sf:select> 生成<select>標籤
<sf:textarea> 生成<textarea>標籤

現在就可以在之前的使用者註冊介面進行使用:

<sf:form method="POST" commandName="spitter">
    First Name: <sf:input path="firstName" /><br/>
    Last Name: <sf:input path="lastName" /><br/>
    Email: <sf:input path="email" /><br/>
    Username: <sf:input path="username" /><br/>
    Password: <sf:password path="password" /><br/>
    <input type="submit" value="Register" />
</sf:form>

使用Spring的form標籤主要有兩個作用,第一是它會自動的繫結來自Model中的一個屬性值到當前form對應的實體物件,預設是command屬性,這樣我們就可以在form表單體裡面方便的使用該物件的屬性了;第二是它支援我們在提交表單的時候使用除GET和POST之外的其他方法進行提交,包括DELETE和PUT等。

這個時候如果Model中存在一個屬性名稱為command的javaBean,在渲染上面的程式碼時就會取command的對應屬性值賦給對應標籤的值。

我們指定form預設自動繫結的是Model的command屬性值,那麼當我的form物件對應的屬性名稱不是command的時候,應該怎麼辦呢?對於這種情況,Spring給我們提供了一個commandName屬性,我們可以通過該屬性來指定我們將使用Model中的哪個屬性作為form需要繫結的command物件。除了commandName屬性外,指定modelAttribute屬性也可以達到相同的效果。

這裡將commandName設定為spitter,因此model中必然存在一個key為spitter的物件,否則表單將不能渲染。這意味著需要對SpitterController進行簡單的改動,以保證model中存在一個Spitter的物件:

// 處理來自/spitter/register的get請求
@RequestMapping(value = "/register", method = RequestMethod.GET)
public String showRegistrationForm(Model model) {
    model.addAttribute(new Spitter());
    return "registerForm";
}

回到表單程式碼中,使用<sf:input>代替了<input>標籤,這個標籤會生成一個HTML的<input>標籤,並且將它的attribute屬性設為text,它的value屬性會根據<sf:input>標籤的path屬性設定的值去model物件中對應的屬性值進行設定。如model中的Spitter物件有一個firstName屬性為Jack,那麼<sf:input path="firstName" />會被解析為含有value="Jack"的input標籤。

為了更好的理解,在一次註冊失敗後,會重定向到註冊頁面,對應的HTML的form標籤如下所示:

<form id="spitter" action="/spitter/spitter/register" method="POST">
    First Name:
        <input id="firstName" name="firstName" type="text" value="J"/><br/>
    Last Name:
        <input id="lastName" name="lastName" type="text" value="B"/><br/>
    Email:
        <input id="email" name="email" type="text" value="jack"/><br/>
    Username:
        <input id="username" name="username" type="text" value="jack"/><br/>
    Password:
        <input id="password" name="password" type="password" value=""/><br/>
    <input type="submit" value="Register" />
</form>

值得注意的是,從Spring3.1開始,<sf:input>標籤允許使用type屬性來宣告一些特殊的HTML5型別,如data、range和email等,例如,可以這樣來宣告email:
Email: <sf:input path="email" type="email" /><br/>

這樣就會解析為:
Email: <input id="email" name="email" type="email" value="jack"/><br/>

錯誤資訊展示

當存在驗證錯誤時,錯誤的詳細資訊會被存放model資料中並被request攜帶,所要做的就是對model中的錯誤資訊進行展示,使用<sf:errors>標籤即可。

例如:

First Name: <sf:input path="firstName" />
  <sf:errors path="firstName" cssClass="error"/><br/>

這裡將<sf:errors>的path屬性設定為firstName,那麼就會展示Spitter model物件的firstName的驗證錯誤資訊,如果沒有錯誤,那麼就不會對其進行解析。如果有,會將其解析為<span>標籤。

First Name: <input id="firstName"
    name="firstName" type="text" value="J"/>
<span id="firstName.errors">size must be between 2 and 30</span>

另外一種展示錯誤資訊的方式是將它們放在一起進行展示,如:

<sf:form method="POST" commandName="spitter">
    <sf:errors path="*" element="div" cssClass="errors" />
    ...
</sf:form>  

這裡的path屬性使用了*,這表明<sf:errors>標籤會解析所有屬性的錯誤資訊。需要注意的是,這裡設定了屬性element為div,預設情況下errors會被解析為<span>標籤,適用於只有一條錯誤資訊時。但是當有多條錯誤資訊時,就需要使用<div>,這樣錯誤資訊就會解析為<div>標籤。

現在還需要對需要更正的屬性進行高亮顯示,可以通過使用<sf:label>標籤以及它的cssErroeClass屬性來實現:

<sf:form method="POST" commandName="spitter">
    <sf:errors path="*" element="div" cssClass="errors" />
    <sf:label path="firstName" cssErrorClass="error">First Name</sf:label>:
        <sf:input path="firstName" cssErrorClass="error"/><br/>
...
</sf:form>

<sf:label>標籤也有一個path屬性,用來顯示對於的model物件中的屬性,如果沒有驗證錯誤,那麼會將其解析為<label>標籤:
<label for="firstName">First Name</label>

如果出現了驗證錯誤訊息,那麼就會解析成:
<label for="firstName" class="error">First Name</label>

類似的,<sf:input>將其cssErrorClass屬性設定為error,如果出現驗證錯誤,那麼解析後的<input>標籤的class屬性會被設定為error。可以自定義屬性資訊:

span.error {
    color: red;
}

label.error {
    color: red;
}

input.error {
    background-color: #ffcccc;
}

div.errors {
    background-color: #ffcccc;
    border: 2px solid red;
}

現在可以為使用者展示一個比較美觀的驗證錯誤資訊,另外還可以在Spitter類中為驗證資訊設定message屬性,從而可以得到比較友好的驗證資訊:

@NotNull
@Size(min = 5, max = 16, message = "{username.size}")
private String username;

@NotNull
@Size(min = 5, max = 25, message = "{password.size}")
private String password;

@NotNull
@Size(min = 2, max = 30, message = "{firstName.size}")
private String firstName;

@NotNull
@Size(min = 2, max = 30, message = "{lastName.size}")
private String lastName;

@NotNull
@Email(message = "{email.valid}")
private String email;

對於每一個屬性,為@Size標註的message設定了一個字串,其中的值使用了大括號包括,那麼大括號之間的值對應的真實內容可以通過properties檔案進行設定:

firstName.size=First name must be between {min} and {max} characters long.
lastName.size=Last name must be between {min} and {max} characters long.
username.size=Username must be between {min} and {max} characters long.
password.size=Password must be between {min} and {max} characters long.
email.valid=The email address must be valid.

其中的min和max是@Size標註中設定的。

當使用者提交了一個不合法的註冊資訊時,可以得到如下圖這樣的錯誤提示資訊:


對驗證錯誤資訊進行友好地展示

Spring基礎標籤庫

處理表單繫結標籤庫之外,Spring還提供了一個跟基本的JSP標籤庫。要使用該標籤庫,需要在頁面中做以下宣告:
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>

宣告瞭之後,就可以在頁面中使用如下的JSP標籤了:

JSP標籤 描述
<s:bind> 通常和form一起用,用以指明表單要提交到哪個類或類的屬性中去
<spring:escapeBody> 對標籤中的內容進行轉義處理
<spring:hasBindErrors> 用於將特定物件(request屬性中)中繫結的的errors解析出來
<spring:htmlEscape> 設定當前頁面的預設的HTML轉義值
<spring:message> 根據code取得訊息資源,並將其解析為一個page、request、session或者application範圍的變數(由var或者scope屬性指定)
<spring:nestedpath> <spring:bind>配置巢狀路徑
<s:theme> <spring:message>相同,只不過處理的是theme訊息
<spring:transform> 來轉換表單中不與bean中的屬性一一對應的那些屬性
<s:url> <spring:message>相同,只不過處理的是URI模版變數
<s:eval> <spring:message>相同,只不過處理的是SpEL表示式

展示訊息的國際化支援

使用<s:message>標籤可以對引用外部屬性檔案的檔案進行完美地解析,如:
<h1><s:message code="spittr.welcome" /></h1>

這裡<s:message>標籤會從某個屬性檔案中根據key值spittr.welcome讀取對應的文字並解析到頁面中,在這之前需要對這個key-value進行配置。

Spring有一些實現自MessageSource介面的訊息源類,其中一個比較常用的就是ResourceBundleMessageSource,它可以從properties檔案中載入訊息,下面的@Bean方法對該類進行了配置:

@Bean
public MessageSource messageSource() {
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    messageSource.setBasename("messages");
    return messageSource;
}

其中的關鍵在於設定了basename屬性,之後ResourceBundleMessageSource就可以對classpath根路徑下相對應的的properties檔案進行解析。

另外,還可以使用ReloadableResourceBundleMessageSource,它可以在不重新編譯或者重啟專案的情況下重新載入訊息屬性:

@Bean
public MessageSource messageSource() {
    ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
    messageSource.setBasename("file:///etc/spittr/messages");
    messageSource.setCacheSeconds(10);
    return messageSource;
}

與ResourceBundleMessageSource主要的區別在於basename的設定,這裡可以將其設定為classpath(需要字首classpath:)、檔案系統(file:)或者專案的根目錄(不加任何字首)。上面程式碼中設定的就是在檔案系統中的/etc/spittr目錄中查詢檔名稱為messages的檔案。

下面建立一個名為messages.properties的檔案,並新增如下內容:
spittr.welcome=Welcome to Spittr!

構建URL:<s:url>

<s:url>標籤的主要功能就是建立URL,並將其分配給一個變數或者解析到響應中。作為JSTL的<c:url>標籤的簡單替換,它也有這一些新功能。

<s:url>標籤需要一個servlet上下文相關的URL,並對其進行解析。例如:
<a href="<s:url href="/spitter/register" />">Register</a>

如果servlet上下文是spittr,那麼上面的連結將會被解析為:
<a href="/spittr/spitter/register">Register</a>

將servlet上下文作為連結字首新增到目標連結中。

另外也可以使用<s:url>標籤構建URL並分配到變數中:

<s:url href="/spitter/register" var="registerUrl" />
<a href="${registerUrl}">Register</a>

預設情況下,URL變數是page範圍內的。但是也可以通過設定scope屬性將其設定為application、session或者request範圍內的:
<s:url href="/spitter/register" var="registerUrl" scope="request" />

如果想為URL新增引數,可以通過<s:param>標籤進行新增。例如,下面的<s:url>標籤通過<s:param>為/spittles設定了max和count屬性:

<s:url href="/spittles" var="spittlesUrl">
    <s:param name="max" value="60" />
    <s:param name="count" value="20" />
</s:url>

現在看起來<s:url><c:url>沒有什麼區別嘛。但是如果需要建立一個含有路徑引數的URL時怎麼處理?如何讓一個href中有一個可以替換的path引數?

使用<s:param>就可以處理:

<s:url href="/spitter/{username}" var="spitterUrl">
    <s:param name="username" value="jbauer" />
</s:url>

href中有一個佔位符,通過<s:param>來指定這個佔位符的值。

另外,<s:url>也可以實現URL的轉義,通過設定htmlEscape屬性可以完成URL中的HTML轉義:

<s:url value="/spittles" htmlEscape="true">
    <s:param name="max" value="60" />
    <s:param name="count" value="20" />
</s:url>

上面的標籤將會被解析為:
/spitter/spittles?max=60&count=20

另一方面,如果想在JavaScript程式碼中使用URL,那麼可以設定javaScriptEscape屬性為true。

<s:url value="/spittles" var="spittlesJSUrl" javaScriptEscape="true">
    <s:param name="max" value="60" />
    <s:param name="count" value="20" />
</s:url>

<script>
    var spittlesUrl = "${spittlesJSUrl}"
</script>

上述程式碼會被解析為:

<script>
    var spittlesUrl = "\/spitter\/spittles?max=60&count=20"
</script>

內容轉義:<s:escapeBody>

有時想在頁面展示一段HTML程式碼,一般的要在頁面顯示字元<>需要用<>代替,但是這種做法明顯的很笨重而且難讀。這種情況下可以使用<s:escapeBody>標籤:

<s:escapeBody htmlEscape="true">
<h1>Hello</h1>
</s:escapeBody>

上述程式碼會被解析為:
<h1>Hello</h1>

當然,該標籤頁支援JavaScript程式碼,只需將其javaScriptEscape屬性設為true即可:

<s:escapeBody javaScriptEscape="true">
<h1>Hello</h1>
</s:escapeBody>

使用Apache Tiles檢視

假設要為所有頁面新增一個通用的頁首和頁尾,一般的做法是為每個JSP頁面新增HTML程式碼,顯然這種方法在後期不方便進行維護。

剛好的方法是使用排版引擎,例如Apache Tiles,來定義所有頁面中的通用頁面排版。

配置Tiles檢視解析器

為了在Spring中使用Tiles,必須配置一些bean。需要一個TilesConfigurer,它主要用來定位以及載入tile定義。另外還需要TilesViewResolver來解析tile定義中的邏輯檢視。

對於這兩個元件,Apache Tiles 2和3中使用了不同的包:org.springframework.web.servlet
.view.tiles2和org.springframework.web.servlet
.view.tiles3,這裡我們使用3版本。

下面新增TilesConfigurer的bean定義:

@Bean
public TilesConfigurer tilesConfigurer() {
    TilesConfigurer tiles = new TilesConfigurer();
    // 指定tile定義的位置
    tiles.setDefinitions(new String[] { "/WEB-INF/layout/tiles.xml" });
    // 開啟重新整理
    tiles.setCheckRefresh(true);
    return tiles;
}

在配置TilesConfigurer時,最重要的屬性就是definitions,該屬性使用一個String陣列作為引數,用來指定tile定義檔案的位置。可以指定多個位置,還可以使用萬用字元。例如可以使用如下的配置來指定TilesConfigurer尋找/WEB-INF目錄下的任意名為tiles.xml的檔案:

tiles.setDefinitions(new String[] {
    "/WEB-INF/**/tiles.xml"
});

Ant風格的**模式表明要在/WEB-INF/下的所有目錄查詢名為tiles.xml的檔案。

下面來配置TilesViewResolver

@Bean
public ViewResolver tilesViewResolver(){
    return new TilesViewResolver();
}

另外,也可以使用XML的方式配置:

<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
    <property name="definitions">
        <list>
            <value>/WEB-INF/layout/tiles.xml.xml</value>
            <value>/WEB-INF/views/**/tiles.xml</value>
        </list>
    </property>
</bean>
<bean id="viewResolver" class="org.springframework.web.servlet.view.tiles3.TilesViewResolver" />

定義tile配置檔案

Apache Tiles提供了一個DTD(document type definition)用來指定XML中tile的定義。每個定義由<definition>元素組成,該元素又有一個或多個<put-attribute>,如:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE tiles-definitions PUBLIC
       "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
       "http://tiles.apache.org/dtds/tiles-config_3_0.dtd">

<tiles-definitions>

    <!-- 定義一個基礎tile -->
    <definition name="base" template="/WEB-INF/layout/page.jsp">
        <put-attribute name="header" value="/WEB-INF/layout/header.jsp" />
        <put-attribute name="footer" value="/WEB-INF/layout/footer.jsp" />
    </definition>

    <definition name="home" extends="base">
        <put-attribute name="body" value="/WEB-INF/views/home.jsp" />
    </definition>
    <definition name="registerForm" extends="base">
        <put-attribute name="body" value="/WEB-INF/views/registerForm.jsp" />
    </definition>
    <definition name="profile" extends="base">
        <put-attribute name="body" value="/WEB-INF/views/profile.jsp" />
    </definition>
    <definition name="spittles" extends="base">
        <put-attribute name="body" value="/WEB-INF/views/spittles.jsp" />
    </definition>
    <definition name="spittle" extends="base">
        <put-attribute name="body" value="/WEB-INF/views/spittle.jsp" />
    </definition>
</tiles-definitions>

每個<definition>元素定義了一個tile,代表著一個JSP模版。一個tile同時也可以代表其他在主模版中被嵌入的模版。對應base tile,它表示一個header JSP模版和footer JSP模版。

下面是page.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="s"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="t"%>
<%@ page session="false"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Spittr</title>
<link rel="stylesheet" type="text/css"
    href="<s:url value="/resources/style.css" />">
</head>
<body>
    <!-- 頭部 -->
    <div id="header">
        <t:insertAttribute name="header" />
    </div>
    <!-- 正文 -->
    <div id="content">
        <t:insertAttribute name="body" />
    </div>
    <!-- 尾部 -->
    <div id="footer">
        <t:insertAttribute name="footer" />
    </div>
</body>
</html>

其中的關鍵在於如何使用<t:insertAttribute>標籤從Tile標籤庫插入到其他模版中。使用該標籤來引入header、body、footer屬性,最終的佈局如下圖所示:

基本佈局

其中,header和footer屬性在tile定義檔案中分別指明瞭,但是body屬性呢?它在哪裡設定呢?

base tile的主要作用是作為一個基礎模版用來作為其他tile定義擴充套件使用的。那麼擴充套件了base的tile就繼承了base的header和footer屬性(也可以進行重寫)。它們自己也設定了一個body屬性用來引用一個JSP模版。

如home tile,它繼承自base,所以它繼承了base的所有屬性。即使home tile的定義非常簡單,但是它相當於如下定義:

<definition name="home" template="/WEB-INF/layout/page.jsp">
    <put-attribute name="header" value="/WEB-INF/layout/header.jsp" />
    <put-attribute name="footer" value="/WEB-INF/layout/footer.jsp" />
    <put-attribute name="body" value="/WEB-INF/views/home.jsp" />
</definition>

hsader.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="s"%>
<a href="<s:url value="/" />"><img
    src="<s:url value="/resources" />/images/spittr_logo_50.png" border="0" /></a>

footer.jsp:

Copyright © Craig Walls

每一個繼承自base的tile都定義了自己的body模版:

home.jsp:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>
<h1>Welcome to Spittr</h1>
<a href="<c:url value="/spittles" />">Spittles</a> |
<a href="<c:url value="/spitter/register" />">Register</a>

這裡的關鍵在於一個頁面的公共部分已經在page.jsp、header.jsp、footer.jsp中捕獲,這樣就可以在所有頁面裡進行重複利用,簡化後期的維護工作。

如下圖所示,頁面中包含一些樣式和圖片,但是這些與Tiles是沒有關聯的,因此這裡就不再進行詳細的闡述。但是,從這個頁面可以看出頁面是如何通過各個不同的tile元件組成的。

通過Tile載入的Spittr的首頁

使用Thymeleaf

雖然JSP已經使用了較長的時間,並且在Java web中使用的也很廣泛,但是它也有自身的一些缺陷。明顯的就是JSP是以HTML或者XML的形式展現的。大多數的JSP模版都使用HTML的格式,並使用各種JSP標籤庫。雖然這些標籤庫可以在JSP中進行動態的解析,但是卻很難有一個格式良好的頁面。比如,可以在HTML中使用下面的JSP標籤:
<input type="text" value="<c:out value="${thing.name}"/>" />

當閱讀一個沒有解析的JSP頁面時,常常很難讀懂,簡直就是一場視覺災難!因為JSP並不是真正的HTML,很多web瀏覽器和編輯器很難對JSP進行解析。

另外,JSP與servlet規範是緊密耦合的,這就意味著它只能使用在以servlet為基礎的web應用中。

近年內有湧現出很多要替代JSP作為java應用的檢視技術,其中一個有力的競爭者就是:Thymeleaf。Thymeleaf不需要依賴標籤庫,並且是可編輯的、可以解析到HTML中。另外,它與servlet規範是沒有耦合的,因此它可以在JSP不能使用的環境進行使用。下面,我們來看一下如何在Spring MVC中使用Thymeleaf

配置Thymeleaf檢視解析器

為了在Spring中使用Thymeleaf,需要配置3個bean:

  • ThymeleafViewResolver:用來從邏輯檢視中解析出Thymeleaf模版;
  • SpringTemplateEngine:對模版進行處理,並給出結果;
  • TemplateResolver:用來載入Thymeleaf模版;

下面使用Java類的方式進行宣告:

// Thymeleaf檢視解析器
@Bean
public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
    ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
    viewResolver.setTemplateEngine(templateEngine);
    return viewResolver();
}

// Thymeleaf驅動
@Bean
public TemplateEngine templateEngine(TemplateResolver templateResolver) {
    SpringTemplateEngine templateEngine = new SpringTemplateEngine();
    templateEngine.setTemplateResolver(templateResolver);
    return templateEngine;
}

// 模版解析器
@Bean
public TemplateResolver templateResolver() {
    TemplateResolver templateResolver = new ServletContextTemplateResolver();
    templateResolver.setPrefix("/WEB-INF/templates/");
    templateResolver.setSuffix(".html");
    templateResolver.setTemplateMode("HTML5");
    return templateResolver;
}

也可以使用XML配置檔案的方式進行配置:

<bean id="viewResolver" class="org.thymeleaf.spring3.view.ThymeleafViewResolver"
    p:templateEngine-ref="templateEngine" />
<bean id="templateEngine" class="org.thymeleaf.spring3.SpringTemplateEngine"
    p:templateResolver-ref="templateResolver" />
<bean id="templateResolver"
    class="org.thymeleaf.templateresolver.ServletContextTemplateResolver"
    p:prefix="/WEB-INF/templates/" p:suffix=".html" p:templateMode="HTML5" />

ThymeleafViewResolver是Spring MVC檢視解析器ViewResolver的一個實現,和其他檢視解析器一樣,它會對一個邏輯檢視名稱進行解析,此時最終的檢視會是一個Thymeleaf模版。

注意,ThymeleafViewResolver中注入了一個SpringTemplateEngine的bean類,SpringTemplateEngine可以用來對模版進行轉換和解析。

TemplateResolver用來定位最終的模版。

定義Thymeleaf模版

Thymeleaf模版主要是HTML檔案,並沒有一些特殊的標籤或者標籤庫。它是通過自定義名稱空間的方式來向標準HTML中新增Thymeleaf屬性的。比如下面的例子:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spittr</title>
<link rel="stylesheet" type="text/css" th:href="@{/resources/style.css}"></link>
</head>
<body>
    <h1>Welcome to Spittr</h1>
    <a th:href="@{/spittles}">Spittles</a> |
    <a th:href="@{/spitter/register}">Register</a>
</body>
</html>

主頁模版非常簡單,只使用了th:href屬性。該屬性就像HTML的href屬性,使用起來也是一樣的。它的特別之處在於它可以包含Thymeleaf表示式。它會對href屬性進行解析,這就是Thymeleaf表示式的工作原理:它們對應著標準的HTML屬性,並且會解析一些計算值。這種情況下,所有的th:href屬性會使用@{}表示式來得出相關的上下文URL路徑(類似於JSTL中的<c:url>標籤)。

Thymeleaf模版不像JSP,它是可以編輯甚至可以自然的解析,不需要準備其他任何處理過程。當然,需要Thymeleaf對模版進行處理從而獲取到預想的輸出。但是不需要做其他特殊的處理,home.html就可以裝載到瀏覽器中,如圖所示:

Thymeleaf模版可以像處理HTML檔案一樣進行解析

如上圖所示,JSP檔案的標籤庫宣告也會顯示出來,並且在超連結前面會有一些奇怪的標記。

相反,Thymeleaf模版解析得比較完美,唯一的不足就是超連結的解析。瀏覽器沒有將th:href解析為href,所有link沒有解析為超連結的樣式。

Spring的JSP標籤擅長使用繫結,如果摒棄使用JSP,那麼該如何使用屬性繫結呢?

使用Thymeleaf進行繫結

表單繫結是Spring MVC的一個重要特性,沒有正確的表單繫結,你就必須保證HTML的表單欄位是正確命名的,並且是和後臺的物件屬性是一一對映的。同時還要保證當驗證失敗對錶單進行展示時屬性值可以正確地set到相應的物件屬性中去。

比如registration.jsp中的First Name屬性:

<sf:label path="firstName" cssErrorClass="error">First Name</sf:label>:
    <sf:input path="firstName" cssErrorClass="error" />
<br />

這裡<sf:input>標籤會被解析為HTML的<input>標籤,並且其value屬性會根據後臺物件的firstName屬性進行設定。同時使用了<sf:label>和cssErrorClass屬性用來在出現驗證錯誤時解析該標籤。

但是本節中我們要討論的是如何在Thymeleaf中使用動態繫結,而不是JSP,比如下面的程式碼:

<label th:class="${#fields.hasErrors('firstName')}? 'error'">
    First Name</label>:
<input type="text" th:field="*{firstName}"
    th:class="${#fields.hasErrors('firstName')}? 'error'" /><br/>

這裡使用了th:class屬性,該屬性會被解析為一個class屬性,並且其值是使用給定的表示式計算而來。該屬性會對firstName值進行檢查是否存在校驗錯誤,如果存在,那麼class屬性解析後就會包含error,如果沒有錯誤,那麼class屬性就不會進行解析。

<input>標籤使用了th:field屬性來從後臺物件中解析出firstName屬性。這裡使用了th:field屬性與後臺物件的firstName屬性進行了繫結,這樣可以同時得到為firstName設定的value屬性和name屬性。

下面的程式碼中驗證了Thymeleaf的資料繫結:

      <form method="POST" th:object="${spitter}">
        <div class="errors" th:if="${#fields.hasErrors('*')}">
          <ul>
            <li th:each="err : ${#fields.errors('*')}" 
                th:text="${err}">Input is incorrect</li>
          </ul>
        </div>
        <label th:class="${#fields.hasErrors('firstName')}? 'error'">First Name</label>: 
          <input type="text" th:field="*{firstName}"  
                 th:class="${#fields.hasErrors('firstName')}? 'error'" /><br/>
  
        <label th:class="${#fields.hasErrors('lastName')}? 'error'">Last Name</label>: 
          <input type="text" th:field="*{lastName}"
                 th:class="${#fields.hasErrors('lastName')}? 'error'" /><br/>
  
        <label th:class="${#fields.hasErrors('email')}? 'error'">Email</label>: 
          <input type="text" th:field="*{email}"
                 th:class="${#fields.hasErrors('email')}? 'error'" /><br/>
  
        <label th:class="${#fields.hasErrors('username')}? 'error'">Username</label>: 
          <input type="text" th:field="*{username}"
                 th:class="${#fields.hasErrors('username')}? 'error'" /><br/>
  
        <label th:class="${#fields.hasErrors('password')}? 'error'">Password</label>: 
          <input type="password" th:field="*{password}"  
                 th:class="${#fields.hasErrors('password')}? 'error'" /><br/>
        <input type="submit" value="Register" />

      </form>

上述程式碼使用了相同的Thymeleaf屬性和*{}表示式來對後臺物件進行繫結。值得注意的是,我們在form的頂部使用Thymeleaf來解析所有的異常。<div>元素使用了th:if屬性來對是否存在錯誤進行校驗,如果有錯誤,那麼就會對<div>進行解析,否則就不解析。

<div>中的列表是無序的,針對變數err中的每一個error,每個<li>中的th:each會解析為一個<li>標籤。<li>標籤也有一個th:text屬性,該屬性會對錶達式的值進行計算,並將結果解析到對應的<li>標籤中。最終,針對每個error,都會有一個<li>進行展示。

你也許會對${}*{}包含的表示式有疑惑。${}表示式是變數表示式,如${spitter}。一般的,都是一些OGNL(Object-Graph Navigation Language,物件圖導航語言)表示式,但是當使用Spring時,它們就是SpEL表示式。比如${spitter}會解析為key是spitter的model物件。

*{}表示式則是選擇表示式,變數表示式根據整個的SpEL上下文進行計算,而選擇表示式則是根據指定的物件進行計算。在上述表單中,選中的物件是在<form>標籤中根據th:object屬性指定的,即來自model的spitter物件,因此,*{password}表示式會被解析為spitter物件的password屬性。

總結

對請求的處理僅僅是Spring MVC的一半內容,如果來自控制器的結果準備進行展示,那麼所產生的model資料需要解析到views中並在使用者的瀏覽器中進行展示。Spring在試圖解析方面是非常靈活的,並且可以提供一些創造性的選項,包括傳統的JSP和較為高階的Apache Tile頁面引擎。

本章大致介紹了檢視以及Spring提供的檢視解析,同時對如何使用JSP和Apache Tile進行了研究,另外還有Thymeleaf。

不知道是我見識少還是我沒怎麼關注前端技術,好像還沒有見人使用過Tile和Thymeleaf,因此本文翻譯過程中顯得很單薄,程式碼也不完善,讀完之後只能有一個大體的瞭解,請見諒。


如果覺得有用,歡迎關注我的微信,有問題可以直接交流:

你的關注是對我最大的鼓勵!

相關文章