Thymeleaf+Spring整合


前言

這個教程介紹了Thymeleaf與Spring框架的集成,特別是SpringMvc框架。

注意Thymeleaf支持同Spring框架的3.和4.版本的集成,但是這兩個版本的支持是封裝在thymeleaf-spring3和thymeleaf-spring4這兩個獨立的庫中,項目中需要根據實際情況分別引用。

樣例代碼針對的是spring4.,但一般情況下,spring3.也可以無縫使用,所需要的僅僅是改變一下引用庫。

1 Thymeleaf同Spring的整合

Thymeleaf與Spring進行整合后,可以在SpringMVC應用中完全替代JSP文件。

集成后你將:

  • 就像控制JSP一樣,使用SpringMvc的@Controller注解來映射Thymeleaf的模板文件。
  • 在模板中使用SpringEL表達式來替換OGNL
  • 在模板中創建的表單,完全支持Beans和結果的綁定,包括使用PropertyEditor,轉換,和驗證等。
  • 可以通過Spring來管理國際化文件顯示國際化信息。

注意,在使用本教程之前,您應該充分了解Thymeleaf的標准方言。

2 Spring標准方言

為了更加方便,更快捷的集成,Thymeleaf提供了一套能夠與Spring正確工作的特有方言。

這套方言基於Thymeleaf標准方言實現,它在類org.thymeleaf.spring.dialect.SpringStandardDialect中,事實上,他繼承於org.thymeleaf.standard.StandardDialect中。

除了已經出現在標准方言中的所有功能,Spring中還有以下特點:

  • 不適用OGNL,而是SpringEL做完變量表達式,因此,所有的${...}和*{...}表達式將用Spring的表達式引擎進行處理。
  • 訪問應用context中的beans可以使用SpringEL語法:${@myBean.doSomething()}
  • 基於表格處理的新屬性:th:field,th:errors和th:errorclass,除此還有一個th:object的新實現,允許它使用表單命令選擇器(??)。
  • 一個新的表達式:#themes.code(...),相當於jsp自定義標簽中的spring:theme。
  • 在spring4.0集成中的一個新的表達式:#mvc.uri(...),相當於jsp自定義標簽中的spring:mvcUrl(...)

注意,上述這些方言特性是不能再普通的TemplateEngine對象中使用的,應該配置一個org.thymeleaf.spring4.SpringTemplateEngine來執行。

一個配置的簡單例子:

<bean id="templateResolver" class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
	<property name="prefix" value="/WEB-INF/templates/" />
	<property name="suffix" value=".html" />
</bean>

<bean id="templateEngine"   class="org.thymeleaf.spring4.SpringTemplateEngine">
	<property name="templateResolver" ref="templateResolver" />
</bean>

視圖和視圖解釋器

SpringMvc中的視圖和視圖解釋器

Spring有兩個符合其模板系統核心的接口:

  • org.springframework.web.servlet.View
  • org.springframework.web.servlet.ViewResolver

視圖模型頁面在應用中,讓我修改和預定義他的行為的頁面,可將其作為Bean來定義,視圖是負責渲染實際的HTML,通常由一些模板引擎來負責,如JSP和Thymeleaf。

ViewResolvers是一個獲取特定操作和語言的的視圖對象的對象。通常,controller會向ViewResolvers要求轉發到一個特定的視圖(視圖名為控制器返回的字符串)。然后在順序執行應用中所有的視圖解析器,直到有一個能夠解析這個視圖。在這種情況下,視圖對象返回並控制傳遞給他的一個html渲染相。

注意,在一個應用中,並不是所有的頁面都被定義為視圖,但是只有那些行為我們希望以特定的方式進行非標准方式操作或者進行特定配置,例如,一些特殊的bean。如果一個ViewResolver請求一個view但沒有響應的bean(這是一個常見的情況),一個新的視圖對象將被臨時創建並返回。

一個SpringMVC中Jsp+JSTL視圖解釋器的典型配置如下:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
  <property name="prefix" value="/WEB-INF/jsps/" />
  <property name="suffix" value=".jsp" />
  <property name="order" value="2" />
  <property name="viewNames" value="*jsp" />
</bean>

根據他的屬性就足夠知道他是怎么配置的了:

  • viewClass:建立視圖實例的類,在JSP解析的時候所必須的,但是現在我們使用Thymeleaf,所以它是不需要的。
  • prefix和suffix,和Thymeleaf的TemplateResolver對象的方式一直,設置前綴和后綴屬性。
  • order:設置在視圖解析器查詢鏈中的順序
  • viewNames:允許定義視圖名稱(可通過通配符),定義內的視圖由視圖解析器解析。

Thymeleaf中的視圖和視圖解析器

Thymeleaf和Spring類似,同樣是對應兩個接口:

  • org.thymeleaf.spring4.view.ThymeleafView
  • org.thymeleaf.spring4.view.ThymeleafViewResolver

這兩個類將用於處理控制器返回Thymeleaf執行的結果。

Thymeleaf視圖解析器的配置同樣和JSP是非常相似的:

<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
  <property name="templateEngine" ref="templateEngine" />
  <property name="order" value="1" />
  <property name="viewNames" value="*.html,*.xhtml" />
</bean>

它的templateEngin的值當然是前一章定義的SpringTemplateEngin對象,另外兩個參數都是可選的,並且也之前的JSP 視圖解析器配置的時候參數含義相同

需要注意一點,我們並不需要配置前綴和后綴,因為這些已經在模板解析器中指定,並會依次傳遞到模板引擎中。

如果我們想定義一個View的bean並設置一些靜態變量該如何做呢?很簡單:

<bean name="main" class="org.thymeleaf.spring4.view.ThymeleafView">
<property name="staticVariables">
	<map>
  	<entry key="footer" value="foot信息" />
	</map>
 </property>
</bean>

模板配置

Spring基礎配置

在與Spring配合使用的時候,Thymeleaf提供了ITemplateResolver和與之相關聯的IResourceResolver的與Spring資源處理器相結合的實現,這些是:

  • org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver用於解析模板.
  • org.thymeleaf.spring4.resourceresolver.SpringResourceResourceResolver主要供內部使用.

這個模板解析器允許應用使用標准Spring資源解析語法來解析模板程序,它可以這樣配置:

<bean id="templateResolver"
  class="org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver">
	<property name="suffix" value=".html" />
	<property name="templateMode" value="HTML5" />
</bean>

然后就可以像這樣使用視圖:

@RequestMapping("/doit")
public String doIt() {
    ...
    return "classpath:resources/templates/doit";
}

注意Spring基礎的資源解析器不會被默認使用,它只是一個除了Thymeleaf核心所提供的模板資源解析器之外的模板資源解析器。

麝香生長管理系統

示例代碼可以從此處下載下載

簡介

有很多人都喜歡麝香,每年春天我們都會在小花盆里放上優良的土壤,還有麝香的種子,將它們放在陽光下,耐心的等待它們的生長。

但是今年我們受夠了靠貼標簽來知道每個花盆里種的是什么,所以我們決定使用Spring+Thymeleaf來制作一個應用,用於管理我們的一個培育目錄,這個應用叫:春葉培育管理員系統。

同Thymeleaf教程中的古泰虛擬商店一樣,這個春葉培育管理系統將會設計到Spring+Thymeleaf的最重要的部分。

業務層

我們將為我們的應用配置一個簡單的業務層,首先看看數據模型:

用幾個簡單的服務類提供所需的業務方法:

@Service
public class SeedStarterService {

    @Autowired
    private SeedStarterRepository seedstarterRepository; 

    public List<SeedStarter> findAll() {
        return this.seedstarterRepository.findAll();
    }

    public void add(final SeedStarter seedStarter) {
        this.seedstarterRepository.add(seedStarter);
    }

}

和:

@Service
public class VarietyService {

    @Autowired
    private VarietyRepository varietyRepository; 

    public List<Variety> findAll() {
        return this.varietyRepository.findAll();
    }

    public Variety findById(final Integer id) {
        return this.varietyRepository.findById(id);
    }

}

Spring MVC配置

接下來我們需要在應用中建立MVC配置文件,它將不僅包括SpringMvc的資源處理和注解掃描,還創建了模板引擎和視圖解釋器的實例。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/mvc
                           http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
                           http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    
    
  <!-- **************************************************************** -->
  <!--  RESOURCE FOLDERS CONFIGURATION                                  -->
  <!--  Dispatcher configuration for serving static resources           -->
  <!-- **************************************************************** -->
  <mvc:resources location="/images/" mapping="/images/**" />
  <mvc:resources location="/css/" mapping="/css/**" />
    

  <!-- **************************************************************** -->
  <!--  SPRING ANNOTATION PROCESSING                                    -->
  <!-- **************************************************************** -->
  <mvc:annotation-driven conversion-service="conversionService" />
  <context:component-scan base-package="thymeleafexamples.stsm" />


  <!-- **************************************************************** -->
  <!--  MESSAGE EXTERNALIZATION/INTERNATIONALIZATION                    -->
  <!--  Standard Spring MessageSource implementation                    -->
  <!-- **************************************************************** -->
  <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basename" value="Messages" />
  </bean>


  <!-- **************************************************************** -->
  <!--  CONVERSION SERVICE                                              -->
  <!--  Standard Spring formatting-enabled implementation               -->
  <!-- **************************************************************** -->
  <bean id="conversionService" 
        class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
      <set>
        <bean class="thymeleafexamples.stsm.web.conversion.VarietyFormatter" />
        <bean class="thymeleafexamples.stsm.web.conversion.DateFormatter" />
      </set>
    </property>
  </bean>


  <!-- **************************************************************** -->
  <!--  THYMELEAF-SPECIFIC ARTIFACTS                                    -->
  <!--  TemplateResolver <- TemplateEngine <- ViewResolver              -->
  <!-- **************************************************************** -->

  <bean id="templateResolver"
        class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
    <property name="prefix" value="/WEB-INF/templates/" />
    <property name="suffix" value=".html" />
    <property name="templateMode" value="HTML5" />
  </bean>
    
  <bean id="templateEngine"
        class="org.thymeleaf.spring4.SpringTemplateEngine">
    <property name="templateResolver" ref="templateResolver" />
  </bean>
   
  <bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
    <property name="templateEngine" ref="templateEngine" />
  </bean>    

    
</beans>

注意:這里選擇了HTML5作為模板模式。

控制器

當然,這個應用程序中還需要一個控制器,由於這個應用只有一個頁面,用戶種子的生長的查看和添加,所以只需要一個控制器就可以了:

@Controller
public class SeedStarterMngController {

    @Autowired
    private VarietyService varietyService;
    
    @Autowired
    private SeedStarterService seedStarterService;

    ...

}

現在看看在這個控制器中可以添加什么?

模型屬性(ModelAttribute注解)

@ModelAttribute("allTypes")
public List<Type> populateTypes() {
    return Arrays.asList(Type.ALL);
}
    
@ModelAttribute("allFeatures")
public List<Feature> populateFeatures() {
    return Arrays.asList(Feature.ALL);
}
    
@ModelAttribute("allVarieties")
public List<Variety> populateVarieties() {
    return this.varietyService.findAll();
}
    
@ModelAttribute("allSeedStarters")
public List<SeedStarter> populateSeedStarters() {
    return this.seedStarterService.findAll();
}

方法映射

接下來是控制器最重要的一部分了,那就是方法映射(RequestMapping),一個表單頁和一個新的種子對象添加頁。

@RequestMapping({"/","/seedstartermng"})
public String showSeedstarters(final SeedStarter seedStarter) {
    seedStarter.setDatePlanted(Calendar.getInstance().getTime());
    return "seedstartermng";
}

@RequestMapping(value="/seedstartermng", params={"save"})
public String saveSeedstarter(
        final SeedStarter seedStarter, final BindingResult bindingResult, final ModelMap model) {
    if (bindingResult.hasErrors()) {
        return "seedstartermng";
    }
    this.seedStarterService.add(seedStarter);
    model.clear();
    return "redirect:/seedstartermng";
}

配置轉換服務##

為了在模板視圖中更加方便的使用日期和我們自己定義的各種對象,我們注冊的了一個轉換服務在上下文中:

<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
  ...    
  <mvc:annotation-driven conversion-service="conversionService" />
  ...
  <!-- **************************************************************** -->
  <!--  CONVERSION SERVICE                                              -->
  <!--  Standard Spring formatting-enabled implementation               -->
  <!-- **************************************************************** -->
  <bean id="conversionService"
        class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
      <set>
        <bean class="thymeleafexamples.stsm.web.conversion.VarietyFormatter" />
        <bean class="thymeleafexamples.stsm.web.conversion.DateFormatter" />
      </set>
    </property>
  </bean>
  ...
</beans>

轉換服務允許我們注冊兩個org.springframework.format.Formatter接口的實現,關於Spring轉換的更多信息,請查驗文檔

首先看一下DateFormatter,它的日期格式定義的字符串定義在Message.properties文件中,並且以date.format作為key.

public class DateFormatter implements Formatter<Date> {

    @Autowired
    private MessageSource messageSource;


    public DateFormatter() {
        super();
    }

    public Date parse(final String text, final Locale locale) throws ParseException {
        final SimpleDateFormat dateFormat = createDateFormat(locale);
        return dateFormat.parse(text);
    }

    public String print(final Date object, final Locale locale) {
        final SimpleDateFormat dateFormat = createDateFormat(locale);
        return dateFormat.format(object);
    }

    private SimpleDateFormat createDateFormat(final Locale locale) {
        final String format = this.messageSource.getMessage("date.format", null, locale);
        final SimpleDateFormat dateFormat = new SimpleDateFormat(format);
        dateFormat.setLenient(false);
        return dateFormat;
    }

}

VarietyFormatter可以自動轉換我們的各種實體,將他們用在表單上(基本通過id)

public class VarietyFormatter implements Formatter<Variety> {

    @Autowired
    private VarietyService varietyService;


    public VarietyFormatter() {
        super();
    }

    public Variety parse(final String text, final Locale locale) throws ParseException {
        final Integer varietyId = Integer.valueOf(text);
        return this.varietyService.findById(varietyId);
    }


    public String print(final Variety object, final Locale locale) {
        return (object != null ? object.getId().toString() : "");
    }
}

在之后的內容,我們會學習更多的關於formatter的內容。

種子生長列表

首先,在/WEB-INF/templatesseedstartermng.html頁將顯示一個當前的已培育種子的列表,為此我們需要一些額外的信息,和通過表達式執行一些模型屬性:

<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}">

  <h2 th:text="#{title.list}">List of Seed Starters</h2>
  
  <table>
    <thead>
      <tr>
        <th th:text="#{seedstarter.datePlanted}">Date Planted</th>
        <th th:text="#{seedstarter.covered}">Covered</th>
        <th th:text="#{seedstarter.type}">Type</th>
        <th th:text="#{seedstarter.features}">Features</th>
        <th th:text="#{seedstarter.rows}">Rows</th>
      </tr>
    </thead>
    <tbody>
      <tr th:each="sb : ${allSeedStarters}">
        <td th:text="${{sb.datePlanted}}">13/01/2011</td>
        <td th:text="${sb.covered}? #{bool.true} : #{bool.false}">yes</td>
        <td th:text="#{${'seedstarter.type.' + sb.type}}">Wireframe</td>
        <td th:text="${#strings.arrayJoin(
                           #messages.arrayMsg(
                               #strings.arrayPrepend(sb.features,'seedstarter.feature.')),
                           ', ')}">Electric Heating, Turf</td>
        <td>
          <table>
            <tbody>
              <tr th:each="row,rowStat : ${sb.rows}">
                <td th:text="${rowStat.count}">1</td>
                <td th:text="${row.variety.name}">Thymus Thymi</td>
                <td th:text="${row.seedsPerCell}">12</td>
              </tr>
            </tbody>
          </table>
        </td>
      </tr>
    </tbody>
  </table>
</div>

這里幾乎是全部代碼,現在分別查看每一個片段。

首先,這一部分將只在有種子在培育的時候顯示,我們將使用th:unless屬性來通過#lists.iEmpty(...)方法來實現這個目標。

<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}">

objects的工具類,比如#lists是SpringEL表達式,他就像在OGNL表達式中同樣的方式使用。

接下來是一些國際化的文本:

<h2 th:text="#{title.list}">List of Seed Starters</h2>

<table>
  <thead>
    <tr>
      <th th:text="#{seedstarter.datePlanted}">Date Planted</th>
      <th th:text="#{seedstarter.covered}">Covered</th>
      <th th:text="#{seedstarter.type}">Type</th>
      <th th:text="#{seedstarter.features}">Features</th>
      <th th:text="#{seedstarter.rows}">Rows</th>
      ...

在這個SpringMVC應用程序中,我們通過一個bean定義了一個MessageSource在我們的spring的XML配置文件中:

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
  <property name="basename" value="Messages" />
</bean>

basename表示我們將使用message打頭的資源文件,如Message_en.properties或者Message_ZH_cn.properties,比如英文版如下:

title.list=Lista de semilleros

date.format=dd/MM/yyyy
bool.true=sí
bool.false=no

seedstarter.datePlanted=Fecha de plantación
seedstarter.covered=Cubierto
seedstarter.type=Tipo
seedstarter.features=Características
seedstarter.rows=Filas

seedstarter.type.WOOD=Madera
seedstarter.type.PLASTIC=Plástico

seedstarter.feature.SEEDSTARTER_SPECIFIC_SUBSTRATE=Sustrato específico para semilleros
seedstarter.feature.FERTILIZER=Fertilizante
seedstarter.feature.PH_CORRECTOR=Corrector de PH

在表格的第一列,將顯示種子的培育開始時間,我們將通過定義的DateFormatter將它自動格式化顯示,為了做到這一點,將使用${{}}語法,這個語法將自動應用Spring的轉換服務。

<td th:text="${{sb.datePlanted}}">13/01/2011</td>

下面將顯示花盆中是否有種子,通過改變bean的布爾值屬性將布爾值轉換為國際化的是和否。:

<td th:text="${sb.covered}? #{bool.true} : #{bool.false}">yes</td>

下一步將展示花盆的類型,它的類型是有兩個值的枚舉型(值分別為木制和塑料),這也是我為什么在配置文件中定義了seedstarter.type.WOOD和seedstarter.type.PLAStIC兩個屬性的原因。

但為了獲取國際化之后的值,我們需要給實際值增加seedstarter.type的前綴,來生成Message 屬性的key返回所需的值:

<td th:text="#{${'seedstarter.type.' + sb.type}}">Wireframe</td>

列表中最困難的部分就是功能列,因為在這里需要顯示左右的功能,如"電加熱,草皮",這里講采用逗號分隔原有枚舉數組的方式。

注意這樣也是有些困難的,因為這些枚舉需要根據他的類型進行具體化,需要:

  • 給特征數組的所有元素規划響應的前綴,
  • 獲得從步驟1相對應的外部信息
  • 把所有從步驟2獲取的信息,用逗號分隔

為了實現這一點,我們創建了如下的代碼:

<td th:text="${#strings.arrayJoin(
               #messages.arrayMsg(
                   #strings.arrayPrepend(sb.features,'seedstarter.feature.')),
               ', ')}">Electric Heating, Turf</td>

列表的最有一列很簡單,事實上,它有一個嵌套表,用於顯示每一行的內容。

<td>
  <table>
    <tbody>
      <tr th:each="row,rowStat : ${sb.rows}">
        <td th:text="${rowStat.count}">1</td>
        <td th:text="${row.variety.name}">Thymus Thymi</td>
        <td th:text="${row.seedsPerCell}">12</td>
      </tr>
    </tbody>
  </table>
</td>

創建表單

處理命令對象

SpringMVC的表單支持bean就是命令對象,這個對象通過對象領域模型的方式提供get和set方法,在瀏覽器建立獲取用戶輸入值的輸入框架。

Thymeleaf需要你顯示的在form標簽內通過th:object屬性指定命令對象:

<form action="#" th:action="@{/seedstartermng}" th:object="${seedStarter}" method="post">
    ...
</form>

這個th:object與其他的的地方用途是一直的,但是事實上在這種特定情況下,為了與SpringMVC框架的正確整合增加了一些特定的限制:

  • 在form標簽中的th:object的值必須是變量表達式(${...}),只能指定屬性模型屬性的名字,而不能使用屬性導航,這意味着,表達式${seedStarter}是正確的,而${seedStarter.data}則不是。
  • 一個form標簽內只能指定一個th:object屬性,這與html中form標簽不能嵌套的特性相一致。

input

下面是如何將一個input插入到表單中

<input type="text" th:field="*{datePlanted}" />

正象上邊的代碼所示,新增了一個th:field的屬性,這是SpringMVC集成的一個重要特征,它幫你完成了表單bean和輸入框之間的繁重的綁定工作。可以看出他在from中的路徑屬性和SpringMVC的jsp標簽庫一樣。

th:field屬性的不同行為取決於它所附加的不同標簽,包括<input>,<select><textarea>(還包括標簽的不同type屬性類型),在這種情況下,時間上上面哪行代碼會是這樣的:

<input type="text" id="datePlanted" name="datePlanted" th:value="*{datePlanted}" />

事實上,可能比上邊的代碼還要多一些東西,因為th:fild還可能會注冊一個Spring的轉換服務,包括之前我們看到的DateFormatter(甚至這個表達式中沒使用雙大括號),因此,這個日期也將被正確的格式化。

th:field的值必須使用選擇表達式,這樣將在這個環境中使用表單bean,而不是上下文變量或SpringMVC的模型屬性。

相反對於th:object這類,它的表達式可以使用屬性導航(事實上在JSP的<form:input標簽中,可以使用任何的路徑屬性表達式)

注意th:field屬性也可以在HTML5的的新增類型中使用,如<input type="datetime"><input type="color">等,有效的增加了對SpringMVC對HTML5支持的完整性。

復選框

th:field也可以用在checkbox中,比如如下代碼:

<div>
  <label th:for="${#ids.next('covered')}" th:text="#{seedstarter.covered}">已種植</label>
  <input type="checkbox" th:field="*{covered}" />
</div>

注意這里有一些除了復選框之外的好東西,比如外部label和它使用的#ids.next("covered")方法,用於當改id的復選框執行的時候獲取它的id值。

那么為什么我們需要這個字段的id屬性動態生成呢?因為復選框可能是多值的,因此它會給id值添加一個序列號后綴(內部使用#ids.seq(...)函數)來保證同一屬性的復選框有不同的id值。

我們可以看看多值的復選框:

<ul>
  <li th:each="feat : ${allFeatures}">
    <input type="checkbox" th:field="*{features}" th:value="${feat}" />
    <label th:for="${#ids.prev('features')}" 
           th:text="#{${'seedstarter.feature.' + feat}}">Heating</label>
  </li>
</ul>

注意這次我們增加了一個th:value屬性,因為這次的特征屬性不是一個布爾值,而是一個數組。

一般情況下,它的輸出為:

<ul>
  <li>
    <input id="features1" name="features" type="checkbox" value="SEEDSTARTER_SPECIFIC_SUBSTRATE" />
    <input name="_features" type="hidden" value="on" />
    <label for="features1">Seed starter-specific substrate</label>
  </li>
  <li>
    <input id="features2" name="features" type="checkbox" value="FERTILIZER" />
    <input name="_features" type="hidden" value="on" />
    <label for="features2">Fertilizer used</label>
  </li>
  <li>
    <input id="features3" name="features" type="checkbox" value="PH_CORRECTOR" />
    <input name="_features" type="hidden" value="on" />
    <label for="features3">PH Corrector used</label>
  </li>
</ul>

我們可以看到一個序列后綴增加在每一個id的屬性中,#ids.prev(....)函數允許我們把檢索最后一個序列值,生成的一個特定的id。

用不着擔心那些隱藏域的名稱為"_features":這是為了避免瀏覽器將未選中的復選框的值在表單提交是沒有自動發送而故意添加的。

還應注意到,如果我們的表單bean中的feature屬性已經包含了一些特定的值,那么th:field還將會自動在相應的標簽中增加checked="checked"屬性。

單選框

單選框的用法和一個非布爾值的多選框使用方式類似,只是他不是多選:

<ul>
  <li th:each="ty : ${allTypes}">
    <input type="radio" th:field="*{type}" th:value="${ty}" />
    <label th:for="${#ids.prev('type')}" th:text="#{${'seedstarter.type.' + ty}}">Wireframe</label>
  </li>
</ul>

下拉列表

下拉列表包含兩個部分:<select>標簽和它包含的<option>標簽。在創建這種表單域的時候,只有<select>標簽需要導入th:field屬性,但th:value屬性卻在<option>標簽中非常重要,因為他們提供了目前選選擇框的選項(使用和非布爾復選框和單選框類似的手段)

使用類型作為下拉列表:

<select th:field="*{type}">
  <option th:each="type : ${allTypes}" 
          th:value="${type}" 
          th:text="#{${'seedstarter.type.' + type}}">Wireframe</option>
</select>

這段代碼理解起來很容易,只是注意屬性優先級讓我們可以在option標簽內使用th:each屬性。

動態域

由於SpringMVC的高級表單綁定功能,使得我們可以使用復雜的SpringEL表達式來綁定動態表單域到表單bean中。這將允許我們在SeedStarter bean中創建一個新的Row對象,並將這個row的域添加到用戶請求的form中。

為了做到這一點,我們需要在控制器中提供一些新的映射方法,它將根據我們的特定請求的參數來決定添加或刪除一行我們定義的SeedStarter.

@RequestMapping(value="/seedstartermng", params={"addRow"})
public String addRow(final SeedStarter seedStarter, final BindingResult bindingResult) {
    seedStarter.getRows().add(new Row());
    return "seedstartermng";
}

@RequestMapping(value="/seedstartermng", params={"removeRow"})
public String removeRow(
        final SeedStarter seedStarter, final BindingResult bindingResult, 
        final HttpServletRequest req) {
    final Integer rowId = Integer.valueOf(req.getParameter("removeRow"));
    seedStarter.getRows().remove(rowId.intValue());
    return "seedstartermng";
}

現在給form添加一個動態table

<table>
  <thead>
    <tr>
      <th th:text="#{seedstarter.rows.head.rownum}">Row</th>
      <th th:text="#{seedstarter.rows.head.variety}">Variety</th>
      <th th:text="#{seedstarter.rows.head.seedsPerCell}">Seeds per cell</th>
      <th>
        <button type="submit" name="addRow" th:text="#{seedstarter.row.add}">Add row</button>
      </th>
    </tr>
  </thead>
  <tbody>
    <tr th:each="row,rowStat : *{rows}">
      <td th:text="${rowStat.count}">1</td>
      <td>
        <select th:field="*{rows[__${rowStat.index}__].variety}">
          <option th:each="var : ${allVarieties}" 
                  th:value="${var.id}" 
                  th:text="${var.name}">Thymus Thymi</option>
        </select>
      </td>
      <td>
        <input type="text" th:field="*{rows[__${rowStat.index}__].seedsPerCell}" />
      </td>
      <td>
        <button type="submit" name="removeRow" 
                th:value="${rowStat.index}" th:text="#{seedstarter.row.remove}">Remove row</button>
      </td>
    </tr>
  </tbody>
</table>

這里出現了很多東西,但都不難理解,除了這一句:

<select th:field="*{rows[__${rowStat.index}__].variety}">
    ...
</select>

如果你記得Thymeleaf教程,那么應該明白__${...}__是一種預處理表達式的語法。這是一個在處理整個表達式之前的內部表達式,但為什么用這種方式指定行的索引呢,下面這種方式不行么:

<select th:field="*{rows[rowStat.index].variety}">
    ...
</select>

嗯事實上,是不行的,他的問題是SpringEL表達式不執行數值中括號里邊的表達式變量,索引執行上邊的語句時,會得到一個錯誤的結果,就是字面形式的row[rowStat.index] (而不是row[0],row[1])而不是行集合中的正確位置,這就是為什么在這里需要預處理。

讓我們看看產生的html后按"添加行"按鈕幾次:

<tbody>
  <tr>
    <td>1</td>
    <td>
      <select id="rows0.variety" name="rows[0].variety">
        <option selected="selected" value="1">Thymus vulgaris</option>
        <option value="2">Thymus x citriodorus</option>
        <option value="3">Thymus herba-barona</option>
        <option value="4">Thymus pseudolaginosus</option>
        <option value="5">Thymus serpyllum</option>
      </select>
    </td>
    <td>
      <input id="rows0.seedsPerCell" name="rows[0].seedsPerCell" type="text" value="" />
    </td>
    <td>
      <button name="removeRow" type="submit" value="0">Remove row</button>
    </td>
  </tr>
  <tr>
    <td>2</td>
    <td>
      <select id="rows1.variety" name="rows[1].variety">
        <option selected="selected" value="1">Thymus vulgaris</option>
        <option value="2">Thymus x citriodorus</option>
        <option value="3">Thymus herba-barona</option>
        <option value="4">Thymus pseudolaginosus</option>
        <option value="5">Thymus serpyllum</option>
      </select>
    </td>
    <td>
      <input id="rows1.seedsPerCell" name="rows[1].seedsPerCell" type="text" value="" />
    </td>
    <td>
      <button name="removeRow" type="submit" value="1">Remove row</button>
    </td>
  </tr>
</tbody>

驗證和錯誤信息

讓我們看看當有錯誤的時候如何給一個表單域一個CSS類:

<input type="text" th:field="*{datePlanted}" 
               th:class="${#fields.hasErrors('datePlanted')}? fieldError" />

可以看到,#fields.hasErrors(...)函數接受一個表達式參數(datePlanted),返回一個布爾值告訴field該字段是否有驗證錯誤。

我們可以根據他們各自的field獲取所有的錯誤:

<ul>
  <li th:each="err : ${#fields.errors('datePlanted')}" th:text="${err}" />
</ul>

通過迭代,我們可以使用th:errors,一個專門用於創建一個通過制定選擇器篩選的錯誤列表的屬性,通過
分隔。

<input type="text" th:field="*{datePlanted}" />
<p th:if="${#fields.hasErrors('datePlanted')}" th:errors="*{datePlanted}">Incorrect date</p>

簡單錯誤基礎css樣式,th:errorclass

在上邊的例子中,如果字段有錯誤,將為表單的input域設置一個css類,因為這種方式很常見,Thymeleaf提供了一個特定的屬性為 th:errorclass

應用於form域的標簽(input,select,textarea等),它將從現有的name屬性th:field屬性字段的名詞相同的屬性,如果發生錯誤,則將制定的css類追加到標簽中。

<input type="text" th:field="*{datePlanted}" class="small" th:errorclass="fieldError" />

如果datePlanted發生錯誤,則:

<input type="text" id="datePlanted" name="datePlanted" value="2013-01-01" class="small fieldError" />

全部錯誤

如果我們想要在form中顯示所有的錯誤呢?我們只需要通過'*'或'all'(等價)來查詢#field.hasErrors(...)方法和#field.errors(...)方法:

<ul th:if="${#fields.hasErrors('*')}">
  <li th:each="err : ${#fields.errors('*')}" th:text="${err}">Input is incorrect</li>
</ul>

在上邊的例子中,我們得到所有的錯誤並迭代他們:

<ul>
  <li th:each="err : ${#fields.errors('*')}" th:text="${err}" />
</ul>

建立一個以
分隔的列表:

<p th:if="${#fields.hasErrors('all')}" th:errors="*{all}">Incorrect date</p>

最后,注意#field.hasErrors("")等效的屬性#fields.hasAnyErrors()和#fields.errors()的等效的#fields.allErrors(),可以使用喜歡的任何語法。

<div th:if="${#fields.hasAnyErrors()}">
  <p th:each="err : ${#fields.allErrors()}" th:text="${err}">...</p>
</div>

全局錯誤

Spring表單還有一種錯誤,全局錯誤,都是些不與窗體的任何特定字段關聯的錯誤。

Thymeleaf提供了一個global的常量來訪問這些錯誤。

<ul th:if="${#fields.hasErrors('global')}">
  <li th:each="err : ${#fields.errors('global')}" th:text="${err}">Input is incorrect</li>
</ul>
<p th:if="${#fields.hasErrors('global')}" th:errors="*{global}">Incorrect date</p>

以及等效的#field.hasGlobalErrors()和#field.globalErrors()方法。

<div th:if="${#fields.hasGlobalErrors()}">
  <p th:each="err : ${#fields.globalErrors()}" th:text="${err}">...</p>
</div>

在表單外部顯示錯誤

表單驗證錯誤也可以在表單外部顯示,方法是通過變量(即${...})的內部選擇變量(*{...})增加表單bean的名字作為前綴的方式。

<div th:errors="${myForm}">...</div>
<div th:errors="${myForm.date}">...</div>
<div th:errors="${myForm.*}">...</div>

<div th:if="${#fields.hasErrors('${myForm}')}">...</div>
<div th:if="${#fields.hasErrors('${myForm.date}')}">...</div>
<div th:if="${#fields.hasErrors('${myForm.*}')}">...</div>

<form th:object="${myForm}">
    ...
</form>

富錯誤對象

Thymeleaf提供了以bean的形式(代替單純的String)提供錯誤信息的能力,包括fieldName(String),message(String),和global(String)屬性的錯誤。

這些錯誤可以通過工具方法#fields.datailedErrors()來實現:

<ul>
    <li th:each="e : ${#fields.detailedErrors()}" th:class="${e.global}? globalerr : fielderr">
        <span th:text="${e.global}? '*' : ${e.fieldName}">The field name</span> |
        <span th:text="${e.message}">The error message</span>
    </li>
</ul>

它仍然是一個原型

現在程序已經好了,現在看一下創建的html模板頁面。

使用Thymeleaf框架的一大好處就是,所有這些功能加入到網頁后,網頁仍然可作為原型使用(所以我們說他是天然模板),打開瀏覽器,不執行程序直接運行seedstartermng.html:

可以看到,雖然他沒有運行起來,不是一個有效的數據,但它是一個完全有效的,可以直接顯示的原型,試想一下,如果是jsp的話,那會怎樣呢?

轉換服務

配置

就像前文所說,Thymeleaf可以在上下文中注冊一個轉換服務,再次看一下他的配置信息

<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
  ...    
  <mvc:annotation-driven conversion-service="conversionService" />
  ...
  <!-- **************************************************************** -->
  <!--  CONVERSION SERVICE                                              -->
  <!--  Standard Spring formatting-enabled implementation               -->
  <!-- **************************************************************** -->
  <bean id="conversionService"
        class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
      <set>
        <bean class="thymeleafexamples.stsm.web.conversion.VarietyFormatter" />
        <bean class="thymeleafexamples.stsm.web.conversion.DateFormatter" />
      </set>
    </property>
  </bean>
  ...
</beans>

${{...}}語法

轉換服務可以通過${{...}}語法很輕松的實現對象到字符串的轉換或格式化:

  • 變量語法${{...}}
  • 選擇變量語法*{{...}}

例如,將一個Integer型轉換為字符串類型,並通過逗號來分隔:

<p th:text="${val}">...</p>
<p th:text="${{val}}">...</p>

返回結果為:

<p>1234567890</p>
<p>1,234,567,890</p>

表單中使用

我們之前看到的每一個th:field屬性都將始終使用轉換服務:

<input type="text" th:field="*{datePlanted}" />

等效於:

<input type="text" th:field="*{{datePlanted}}" />

注意這是唯一一種在表達式中使用單大括號的轉換服務。

#conversions工具對象

conversions工具對象表達式允許手動執行轉換服務:

<p th:text="${'Val: ' + #conversions.convert(val,'String')}">...</p>

工具對象表達式的語法為:

  • conversions.convert(Object,Class):將對象轉換為指定的類

  • conversions.convert(Object,String):和上邊相同,但是指定的目標為String類(java.lang包名可以省略)

渲染片段模板

Thymeleaf提供了將一個模板只渲染一部分,並作為一個片段返回的能力。

這是一個非常有用的組件化工具,比如,它可以用於執行AJAX的Controller的調用,用於在已經加載的瀏覽器中返回一個片段標簽(如用於更新選擇,啟用禁用按鈕等)。

片段渲染可以使用Thymeleaf的片段規范:一個實現了org.thymeleaf.fragment.IFragmentSpec接口的對象。

最常用的一個實現是org.thymeleaf.standard.fragment.StandardDOMSelectorFragmentSpec類,它允許一個片段規范包括之前說過的th:insert,th:replace使用DOM選擇器。

在視圖bean中指定片段

視圖bean是在應用程序上下文中聲明的org.thymeleaf.spring4.view.ThymeleafView的bean,它允許這樣定義一個片段:

<bean name="content-part" class="org.thymeleaf.spring4.view.ThymeleafView">
  <property name="templateName" value="index" />
  <property name="fragmentSpec">
    <bean class="org.thymeleaf.standard.fragment.StandardDOMSelectorFragmentSpec"
          c:selectorExpression="content" />
  </property>
</bean>

通過上邊的bean的定義,如果controller返回一個content-part(bean的名字),

@RequestMapping("/showContentPart")
public String showContentPart() {
    ...
    return "content-part";
}

Thymeleaf將只返回index模板的content片段。一旦前綴后綴都設置並匹配,那么它可能為/WEB-INF/templates/index.html,

<!DOCTYPE html>
<html>
  ...
  <body>
    ...
    <div th:fragment="content">
      只有這里渲染!!
    </div>
    ...
  </body>
</html>

另外應該注意到,因為Thymeleaf可以使用DOM選擇器,所有我們可以不用任何th:fragment屬性,而只用id屬性來選擇一個片段,如:

<bean name="content-part" class="org.thymeleaf.spring4.view.ThymeleafView">
  <property name="fragmentSpec">
    <bean class="org.thymeleaf.standard.fragment.StandardDOMSelectorFragmentSpec"
          c:selectorExpression="#content" />
  </property>
  <property name="templateName" value="index" />
</bean>

同樣完美的適用:

<!DOCTYPE html>
<html>
  ...
  <body>
    ...
    <div id="content">
       只有這里渲染!!
    </div>
    ...
  </body>
</html>

通過控制權的返回值指定片段

不聲明一個視圖bean,可以從控制器自己就可以使用與片段相同的語法,類似於th:insert,th:rplace屬性等,如:

@RequestMapping("/showContentPart")
public String showContentPart() {
    ...
    return "index :: content";
}

當然,同樣可以使用基於DOM選擇器的功能,所有我們也可以是選擇使用基於標准的HTML屬性,如id="content"

@RequestMapping("/showContentPart")
public String showContentPart() {
    ...
    return "index :: #content";
}

也可以使用參數:

@RequestMapping("/showContentPart")
public String showContentPart() {
    ...
    return "index :: #content ('myvalue')";
}

先進的集成功能

與RequestDataValueProcessor集成

現在Thymeleaf無縫的與Spring的RequestDataValueProcessor接口集成,這個接口允許攔截鏈接URLS,表達URLS和表達域的值,以及為了啟用安全,如抵御CSRF而自動透明的添加一些隱藏域。

在應用的上下文中可以簡單的配置RequestDataValueProcessor:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                     http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
 
    ...
 
    <bean name="requestDataValueProcessor"
          class="net.example.requestdata.processor.MyRequestDataValueProcessor" />
 
</beans>

Thymeleaf將通過這種方式使用它:

  • 在渲染URL之前,th:href和th:src將會調用RequestDataValueProcessor.processUrl(...)
  • 在渲染表單的action屬性之前,th:action會調用RequestDataValueProcessor.processAction(...),另外他會檢查
    標簽,因為一般來說這是使用action的唯一一個地方,並且在的關閉標簽
    之前執行RequstDataValueProcessor.getExtraHiddenFields(...)用來新增返回的hidden域。
  • 在渲染value屬性之前,th:value會調用RequestDataProcessor.processFormFieldValue(...),除非在這個標簽中存在了th:field(這時候th:field屬性起作用)
  • 當存在th:field的時候,在渲染value屬性之前會調用RequestDataValueProcessor.processFormFieldValue(...)處理這個屬性值(<textarea>處理內容值)

此功能只有Spring3.x以后使用

綁定地址到Controller

在Spring4.1之后的版本中,Spring允許通過注解直接從從視圖鏈接到控制器,而不需要知道這些控制器映射的URI.

在Thymeleaf中可以通過#mvc.url(...)表達式方法調用Controller中符合駝峰命名規則的方法(get,set),調用的方式為方法的名字,即相當於jsp的spring:mvcUrl(...)自定義方法。

比如

public class ExampleController {
    @RequestMapping("/data")
    public String getData(Model model) { ... return "template" }
    @RequestMapping("/data")
    public String getDataParam(@RequestParam String type) { ... return "template" }
}

下邊是一個鏈接到它的方法:

<a th:href="${(#mvc.url('EC#getData')).build()}">獲取Data參數</a>
<a th:href="${(#mvc.url('EC#getDataParam').arg(0,'internal')).build()}">獲取Data參數</a>

查閱更多這種機制可以查看這里

Spring WebFlow的集成

基礎配置

Thymeleaf-spring4集成包包括與Spring WebFlow 2.3.x的集成

WebFlow包括當特定的事件(過渡)被觸發時渲染頁面片段的一些Ajax的功能,未來讓Thymeleaf參加這些Ajax請求,我們將使用一個不通過的視圖解析器的實現,它這樣配置:

<bean id="thymeleafViewResolver" class="org.thymeleaf.spring4.view.AjaxThymeleafViewResolver">
    <property name="viewClass" value="org.thymeleaf.spring4.view.FlowAjaxThymeleafView" />
    <property name="templateEngine" ref="templateEngine" />
</bean>

然后在ViewResolver中配置WebFlow的ViewFactoryCreator.

<bean id="mvcViewFactoryCreator" 
      class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator">
    <property name="viewResolvers" ref="thymeleafViewResolver"/>
</bean>

在這里可以指定模板的視圖狀態

<view-state id="detail" view="bookingDetail">
	 ...
</view-state>

在上邊的實例中,bookingDetail是Thymeleaf模板通常使用的一個方式,是模板引擎內任何模板解析器都可以懂的

Ajax片段

WebFlow的片段規范允許片段通過 標簽呈現,就像這樣:

<view-state id="detail" view="bookingDetail">
    <transition on="updateData">
        <render fragments="hoteldata"/>
    </transition>
</view-state>

這些片段(即hoteldata)可以是逗號分隔的列表標記在th:fragment標簽中。

<div id="data" th:fragment="hoteldata">
    這里內容替換
</div>

永遠記住,指定的片段必須有一個id屬性,這樣瀏覽器運行的Spring
JavaScript庫才能對標簽進行替換。

標簽,也可以通過DOM選擇器設定:

<view-state id="detail" view="bookingDetail">
    <transition on="updateData">
        <render fragments="[//div[@id='data']]"/>
    </transition>
</view-state>

這將意味着th:fragment不在需要:

<div id="data">
    This is a content to be changed
</div>

而出發updateData后轉換的代碼:

<script type="text/javascript" th:src="@{/resources/dojo/dojo.js}"></script>
<script type="text/javascript" th:src="@{/resources/spring/Spring.js}"></script>
<script type="text/javascript" th:src="@{/resources/spring/Spring-Dojo.js}"></script>

  ...

<form id="triggerform" method="post" action="">
    <input type="submit" id="doUpdate" name="_eventId_updateData" value="Update now!" />
</form>

<script type="text/javascript">
    Spring.addDecoration(
        new Spring.AjaxEventDecoration({formId:'triggerform',elementId:'doUpdate',event:'onclick'}));
</script>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM