Thymeleaf 与 Spring 框架的集成教程
本文基于官方文档翻译,如有错误欢迎指正。
Project version: 3.1.3.RELEASE
Project web site: https://www.thymeleaf.org/doc/tutorials/3.1/thymeleafspring.html (opens new window)
# 一、前言
本教程将解释如何将 Thymeleaf 与 Spring 框架集成,特别是(但不仅限于)Spring MVC。
请注意,Thymeleaf 为 Spring 框架的 5.x 和 6.x 版本提供了集成,分别由两个独立的库 thymeleaf-spring5
和 thymeleaf-spring6
提供。这些库被打包在不同的 .jar
文件中(thymeleaf-spring5-{版本}.jar
和 thymeleaf-spring6-{版本}.jar
),需要将它们添加到你的类路径中才能在应用程序中使用 Thymeleaf 的 Spring 集成。
本教程中的代码示例和示例应用程序使用了 Spring 6.x 及其相应的 Thymeleaf 集成,但文本内容也适用于 Spring 5.x。如果你的应用程序使用 Spring 5.x,你只需在代码示例中将 org.thymeleaf.spring6
包替换为 org.thymeleaf.spring5
。
# 二、将 Thymeleaf 与 Spring 集成
Thymeleaf 提供了一组 Spring 集成,允许你将其作为 Spring MVC 应用程序中 JSP 的功能齐全的替代品。
这些集成将允许你:
- 让 Spring MVC
@Controller
对象中的映射方法转发到由 Thymeleaf 管理的模板,就像使用 JSP 一样。 - 在你的模板中使用 Spring 表达式语言(Spring EL)而不是 OGNL。
- 在你的模板中创建与表单支持 bean 和结果绑定完全集成的表单,包括使用属性编辑器、转换服务和验证错误处理。
- 显示由 Spring 管理的消息文件中的国际化消息(通过常见的
MessageSource
对象)。 - 使用 Spring 自身的资源解析机制解析你的模板。
请注意,要完全理解本教程,你应该首先学习过《使用 Thymeleaf》教程,该教程深入解释了标准方言。
# 三、SpringStandard 方言
为了实现更简单和更好的集成,Thymeleaf 提供了一种方言,专门实现了所有与 Spring 正确工作所需的功能。
这种特定的方言基于 Thymeleaf 标准方言,并在名为 org.thymeleaf.spring6.dialect.SpringStandardDialect
的类中实现,实际上它扩展自 org.thymeleaf.standard.StandardDialect
。
除了标准方言中已经存在的所有功能(因此是继承的)之外,SpringStandard 方言引入了以下特定功能:
- 使用 Spring 表达式语言(Spring EL 或 SpEL)作为变量表达式语言,而不是 OGNL。因此,所有
${...}
和*{...}
表达式都将由 Spring 的表达式语言引擎评估。另请注意,支持 Spring EL 编译器。 - 使用 SpringEL 的语法访问应用程序上下文中的任何 bean:
${@myBean.doSomething()}
- 用于表单处理的新属性:
th:field
、th:errors
和th:errorclass
,除了th:object
的新实现,它允许用于表单命令选择。 - 一个表达式对象和方法,
#themes.code(...)
,等同于 JSP 自定义标签spring:theme
。 - 一个表达式对象和方法,
#mvc.uri(...)
,等同于 JSP 自定义函数spring:mvcUrl(...)
。
请注意,大多数情况下,你不应该在普通的 TemplateEngine
对象的配置中直接使用这种方言。除非你有非常特定的 Spring 集成需求,否则你应该创建 org.thymeleaf.spring6.SpringTemplateEngine
类的新实例,该实例会自动执行所有必需的配置步骤。
一个示例 bean 配置:
@Bean
public SpringResourceTemplateResolver templateResolver() {
// SpringResourceTemplateResolver 自动与 Spring 自身的资源解析基础结构集成,这是非常推荐的。
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(this.applicationContext);
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
// HTML 是默认值,此处添加是为了清晰起见。
templateResolver.setTemplateMode(TemplateMode.HTML);
// 模板缓存默认为 true。如果希望模板在修改时自动更新,请设置为 false。
templateResolver.setCacheable(true);
return templateResolver;
}
@Bean
public SpringTemplateEngine templateEngine() {
// SpringTemplateEngine 自动应用 SpringStandardDialect 并启用 Spring 自身的 MessageSource 消息解析机制。
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
// 在 Spring 4.2.4 或更高版本中启用 SpringEL 编译器可以加快大多数情况下的执行速度,但可能与特定情况不兼容
// 当一个模板中的表达式在不同数据类型之间重用时,因此默认情况下此标志为 "false" 以实现更安全的向后兼容性。
templateEngine.setEnableSpringELCompiler(true);
return templateEngine;
}
或者,使用 Spring 的基于 XML 的配置:
<!-- SpringResourceTemplateResolver 自动与 Spring 自身的 -->
<!-- 资源解析基础结构集成,这是非常推荐的。 -->
<bean id="templateResolver" class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
<property name="prefix" value="/WEB-INF/templates/" />
<property name="suffix" value=".html" />
<!-- HTML 是默认值,此处添加是为了清晰起见。 -->
<property name="templateMode" value="HTML" />
<!-- 模板缓存默认为 true。如果希望模板在修改时自动更新,请设置为 false。 -->
<property name="cacheable" value="true" />
</bean>
<!-- SpringTemplateEngine 自动应用 SpringStandardDialect 并 -->
<!-- 启用 Spring 自身的 MessageSource 消息解析机制。 -->
<bean id="templateEngine" class="org.thymeleaf.spring6.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver" />
<!-- 启用 SpringEL 编译器可以加快大多数情况下的执行速度,但可能与特定情况不兼容 -->
<!-- 当一个模板中的表达式在不同数据类型之间重用时,所以默认情况下此标志为 "false" 以实现更安全的向后兼容性。 -->
<property name="enableSpringELCompiler" value="true" />
</bean>
# 四、视图和视图解析器
# 1、Spring MVC 中的视图和视图解析器
Spring MVC 中有两个接口构成了其模板系统的核心:
org.springframework.web.servlet.View
org.springframework.web.servlet.ViewResolver
视图在我们的应用程序中建模页面,并允许我们通过将它们定义为 bean 来修改和预定义它们的行为。视图负责渲染实际的 HTML 界面,通常是通过执行一些模板引擎(如 Thymeleaf)来实现的。
视图解析器负责为特定的操作和区域设置获取视图对象。通常,控制器会要求视图解析器转发到具有特定名称的视图(控制器方法返回的字符串),然后应用程序中的所有视图解析器按顺序链式执行,直到其中一个能够解析该视图,在这种情况下,返回视图对象并将控制权传递给它以渲染 HTML。
请注意,并非我们应用程序中的所有页面都必须定义为视图,而只是那些我们希望其行为非标准或以特定方式配置(例如,通过向其连接某些特殊 bean)的页面。如果视图解析器被要求提供一个没有对应 bean 的视图(这是常见的情况),则会临时创建一个新的视图对象并返回。
过去,Spring MVC 应用程序中 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
允许定义(使用通配符)将由该视图解析器解析的视图名称。
# 2、Thymeleaf 中的视图和视图解析器
Thymeleaf 为上述两个接口提供了实现:
org.thymeleaf.spring6.view.ThymeleafView
org.thymeleaf.spring6.view.ThymeleafViewResolver
这两个类将负责处理控制器执行后生成的 Thymeleaf 模板。
Thymeleaf 视图解析器的配置与 JSP 非常相似:
@Bean
public ThymeleafViewResolver viewResolver() {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
// 注意 'order' 和 'viewNames' 是可选的
viewResolver.setOrder(1);
viewResolver.setViewNames(new String[] {".html", ".xhtml"});
return viewResolver;
}
…或者在 XML 中:
<bean class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<property name="templateEngine" ref="templateEngine" />
<!-- 注意 'order' 和 'viewNames' 是可选的 -->
<property name="order" value="1" />
<property name="viewNames" value="*.html,*.xhtml" />
</bean>
templateEngine
参数,当然是我们上一章中定义的 SpringTemplateEngine
对象。另外两个(order
和 viewNames
)都是可选的,与我们之前看到的 JSP ViewResolver 中的含义相同。
请注意,我们不需要 prefix
或 suffix
参数,因为这些已经在模板解析器中指定(模板解析器又传递给模板引擎)。
如果我们想定义一个 View
bean 并向其中添加一些静态变量怎么办?简单,只需为它定义一个 原型 bean:
@Bean
@Scope("prototype")
public ThymeleafView mainView() {
ThymeleafView view = new ThymeleafView("main"); // templateName = 'main'
view.setStaticVariables(
Collections.singletonMap("footer", "The ACME Fruit Company")
);
return view;
}
通过这样做,你将能够通过 bean 名称(在本例中为 mainView
)选择性地执行这个视图 bean。
# 五、Spring Thyme SeedStarter Manager
本指南中所示示例的源代码可以在 Spring Thyme SeedStarter Manager (STSM) 示例应用程序中找到:
# 1、概念
在 Thymeleaf,我们非常喜欢百里香,每到春天,我们都会准备好土壤和我们最喜欢的种子,放置在西班牙阳光下,耐心地等待新植物的生长。
但今年我们厌倦了给种子启动容器贴标签,以便知道每个单元格中是什么种子,所以我们决定使用 Spring MVC 和 Thymeleaf 制作一个应用程序来帮助我们编录我们的启动器:《Spring Thyme SeedStarter Manager》。
与我们在《使用 Thymeleaf》教程中开发的 Good Thymes 虚拟杂货店应用程序类似,STSM 将允许我们例证将 Thymeleaf 作为 Spring MVC 的模板引擎集成的最重要方面。
# 2、业务层
我们的应用程序需要一个非常简单的业务层。首先,让我们看看我们的模型实体:
一些非常简单的服务类将提供所需的事务方法。比如:
@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 配置
接下来我们需要为应用程序设置 Spring MVC 配置,这不仅包括标准的 Spring MVC 构件,如资源处理或注解扫描,还包括创建模板引擎和视图解析器实例。
@Configuration
@EnableWebMvc
@ComponentScan
public class SpringWebConfig extends WebMvcConfigurerAdapter implements ApplicationContextAware {
private ApplicationContext applicationContext;
public SpringWebConfig() {
super();
}
public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/* ******************************************************************* */
/* GENERAL CONFIGURATION ARTIFACTS */
/* Static Resources, i18n Messages, Formatters (Conversion Service) */
/* ******************************************************************* */
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
registry.addResourceHandler("/images/**").addResourceLocations("/images/");
registry.addResourceHandler("/css/**").addResourceLocations("/css/");
registry.addResourceHandler("/js/**").addResourceLocations("/js/");
}
@Bean
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("Messages");
return messageSource;
}
@Override
public void addFormatters(final FormatterRegistry registry) {
super.addFormatters(registry);
registry.addFormatter(varietyFormatter());
registry.addFormatter(dateFormatter());
}
@Bean
public VarietyFormatter varietyFormatter() {
return new VarietyFormatter();
}
@Bean
public DateFormatter dateFormatter() {
return new DateFormatter();
}
/* THYMELEAF-SPECIFIC ARTIFACTS */
/* TemplateResolver <- TemplateEngine <- ViewResolver */
/* **************************************************************** */
@Bean
public SpringResourceTemplateResolver templateResolver() {
// SpringResourceTemplateResolver 自动与 Spring 自身的资源解析基础结构集成,这是非常推荐的。
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(this.applicationContext);
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
// HTML 是默认值,此处添加是为了清晰起见。
templateResolver.setTemplateMode(TemplateMode.HTML);
// 模板缓存默认为 true。如果希望模板在修改时自动更新,请设置为 false。
templateResolver.setCacheable(true);
return templateResolver;
}
@Bean
public SpringTemplateEngine templateEngine() {
// SpringTemplateEngine 自动应用 SpringStandardDialect 并启用 Spring 自身的 MessageSource 消息解析机制。
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
// 启用 SpringEL 编译器可以加快大多数情况下的执行速度,但可能与特定情况不兼容
// 当一个模板中的表达式在不同数据类型之间重用时,所以默认情况下此标志为 "false" 以实现更安全的向后兼容性。
templateEngine.setEnableSpringELCompiler(true);
return templateEngine;
}
@Bean
public ThymeleafViewResolver viewResolver() {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
return viewResolver;
}
}
# 七、控制器
当然,我们的应用程序也需要一个控制器。由于 STSM 只包含一个包含种子启动器列表和添加新启动器的表单的网页,我们将只编写一个控制器类来处理所有服务器交互:
@Controller
public class SeedStarterMngController {
@Autowired
private VarietyService varietyService;
@Autowired
private SeedStarterService seedStarterService;
...
}
现在让我们看看我们可以在这个控制器类中添加什么。
# 1、模型属性
首先,我们将添加一些我们将在页面中需要的模型属性:
@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();
}
# 2、映射方法
现在,控制器最重要的部分,映射方法:一个用于显示表单页面,另一个用于处理添加新的 SeedStarter
对象。
@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";
}
# 八、配置转换服务
为了允许在视图层轻松格式化 Date
和 Variety
对象,我们配置了应用程序,以便创建和初始化一个 Spring ConversionService
对象(通过我们扩展的 WebMvcConfigurerAdapter
)与我们需要的一些 格式化器 对象。再次看看:
@Override
public void addFormatters(final FormatterRegistry registry) {
super.addFormatters(registry);
registry.addFormatter(varietyFormatter());
registry.addFormatter(dateFormatter());
}
@Bean
public VarietyFormatter varietyFormatter() {
return new VarietyFormatter();
}
@Bean
public DateFormatter dateFormatter() {
return new DateFormatter();
}
Spring 格式化器 是 org.springframework.format.Formatter
接口的实现。有关 Spring 转换基础结构如何工作的更多信息,请参阅 spring.io (opens new window) 上的文档。
让我们来看看 DateFormatter
,它根据 Messages.properties
中 date.format
消息键的格式字符串格式化日期:
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
自动在我们的 Variety
实体和我们希望在表单中使用它们的方式(基本上,通过它们的 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() : "");
}
}
我们将了解更多关于这些格式化器如何影响我们的数据被显示的方式。
# 九、列出种子启动器数据
我们的 /WEB-INF/templates/seedstartermng.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="#{|bool.${sb.covered}|}">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.isEmpty(...
函数来实现。
<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}">
请注意,所有像 #lists
这样的实用程序对象在 Spring EL 表达式中就像它们在标准方言中的 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>
...
</tr>
</thead>
</table>
作为一个 Spring MVC 应用程序,我们已经在 Spring 配置中定义了一个 MessageSource
bean(MessageSource
对象是管理 Spring MVC 中外部化文本的标准方式):
@Bean
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("Messages");
return messageSource;
}
…而 basename
属性表明我们将在类路径中拥有 Messages_es.properties
或 Messages_en.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 转换服务,包括我们在配置中注册的 DateFormatter
。
<td th:text="${{ sb.datePlanted }}">13/01/2011</td>
接下来是显示种子启动器容器是否被覆盖,通过使用文字替换表达式将布尔覆盖 bean 属性的值转换为国际化的 “yes” 或 “no”:
<td th:text="#{|bool.${sb.covered}|}">yes</td>
现在我们必须显示种子启动器容器的类型。类型是一个 Java 枚举,有两个值 (WOOD
和 PLASTIC
),这就是为什么我们在 Messages
文件中定义了两个属性 seedstarter.type.WOOD
和 seedstarter.type.PLASTIC
。
但为了获得类型的国际化名称,我们需要通过表达式将 seedstarter.type.
前缀添加到枚举值,我们将结果用作消息键:
<td th:text="#{|seedstarter.type.${sb.type}|}">Wireframe</td>
这个列表中最困难的部分是 features 列。在其中我们想要显示我们容器的所有功能——它们以 Feature
枚举数组的形式出现——并用逗号分隔。比如 “Electric Heating, Turf”。
请注意,这特别困难,因为这些枚举值也需要像我们对类型所做的那样外部化。因此,流程是:
- 为
features
数组的所有元素添加相应的前缀。 - 获得步骤 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>
# 十、创建表单
# 1、处理命令对象
命令对象 是 Spring MVC 对表单支持 bean 的称呼,即,对象对表单的字段进行建模,并提供 getter 和 setter 方法,框架将使用这些方法建立和获取用户在浏览器端输入的值。
Thymeleaf 要求你在 <form>
标签中使用 th:object
属性指定命令对象:
<form action="#" th:action="@{/seedstartermng}" th:object="${seedStarter}" method="post">
...
</form>
这与其他使用 th:object
的情况是一致的,但实际上这个特定场景在为了与 Spring MVC 的基础设施正确集成时增加了一些限制:
- 表单标签中的
th:object
属性的值必须是变量表达式 (${...}
) 仅指定模型属性的名称,而不进行属性导航。这意味着像${seedStarter}
这样的表达式是有效的,但${seedStarter.data}
则不是。 - 一旦进入
<form>
标签,就不能再指定其他th:object
属性。这与 HTML 表单不能嵌套的事实是一致的。
# 2、输入
现在让我们看看如何向我们的表单添加一个输入:
<input type="text" th:field="*{datePlanted}" />
如您所见,我们在这里引入了一个新属性:th:field
。这是 Spring MVC 集成的一个非常重要的特性,因为它完成了将输入与表单支持 bean 中的属性绑定的繁重工作。您可以将其视为 Spring MVC 的 JSP 标签库中 <form:input>
标签的 path 属性的等效物。
th:field
属性根据它是附加到 <input>
, <select>
或 <textarea>
标签(以及 <input>
标签的具体类型)而表现不同。在这种情况下 (input[type=text]
),上述代码行类似于:
<input type="text" id="datePlanted" name="datePlanted" th:value="*{datePlanted}" />
...但实际上它比这要多一些,因为 th:field
还将应用已注册的 Spring 转换服务,包括我们之前看到的 DateFormatter
(即使字段表达式不是双括号)。多亏了这一点,日期将被正确格式化。
th:field
属性的值必须是选择表达式 (*{...}
),这很有意义,因为它们将在表单支持 bean 上进行评估,而不是在上下文变量(或 Spring MVC 行话中的模型属性)上。
与 th:object
中的那些不同,这些表达式可以包括属性导航(实际上是允许 <form:input>
JSP 标签的 path 属性允许的任何表达式都可以在这里使用)。
请注意,th:field
还理解 HTML5 引入的新的 <input>
元素类型,如 <input type="datetime" ... />
, <input type="color" ... />
等,有效地为 Spring MVC 增加了完整的 HTML5 支持。
# 3、复选框字段
th:field
还允许我们定义复选框输入。让我们看看来自我们 HTML 页面的一个示例:
<div>
<label th:for="${#ids.next('covered')}" th:text="#{seedstarter.covered}">Covered</label>
<input type="checkbox" th:field="*{covered}" />
</div>
请注意,除了复选框本身之外,这里还有一些精细的东西,比如一个外部化的标签,以及使用 #ids.next('covered')
函数来获取将应用于复选框输入的 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
属性,因为 features
字段不是一个布尔值(像 covered
那样),而是一个值的数组。
让我们看看这段代码生成的 HTML 输出:
<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
生成的最后一个序列值。
注意: 不要担心那些带有 name="_features"
的隐藏输入:它们是自动添加的,以避免浏览器在表单提交时未将未选中的复选框值发送到服务器的问题。
另请注意,如果我们的 features
属性在我们的表单支持 bean 中包含一些选定的值,th:field
将负责这一点,并将添加一个 checked="checked"
属性到相应的输入标签。
# 4、单选按钮字段
单选按钮字段的指定方式与非布尔值(多值)复选框类似——当然,除了它们不是多值的:
<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>
# 5、下拉列表/列表选择器
选择字段有两个部分:<select>
标签及其嵌套的 <option>
标签。在创建这种类型的字段时,只有 <select>
标签必须包含 th:field
属性,但嵌套的 <option>
标签中的 th:value
属性非常重要,因为它们将提供知道当前选定的选项的方式(类似于非布尔复选框和单选按钮)。
让我们将 type
字段重新构建为一个下拉选择列表:
<select th:field="*{type}">
<option th:each="type : ${allTypes}" th:value="${type}" th:text="#{${'seedstarter.type.' + type}}">Wireframe</option>
</select>
此时,理解这段代码非常容易。只要注意属性优先级允许我们在 <option>
标签本身中设置 th:each
属性。
# 6、动态字段
多亏了 Spring MVC 中高级的表单字段绑定功能,我们可以使用复杂的 Spring EL 表达式将动态表单字段绑定到我们的表单支持 bean。这将允许我们在 SeedStarter
bean 中创建新的 Row
对象,并在用户请求时将这些行的字段添加到我们的表单中。
为了做到这一点,我们需要在我们的控制器中添加一些新的映射方法,这些方法将根据特定请求参数的存在添加或删除 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";
}
现在我们可以向我们的表单添加一个动态表格:
<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 name="removeRow" type="submit" th:value="${rowStat.index}" th:text="#{seedstarter.row.remove}">Remove row</button>
</td>
</tr>
</tbody>
</table>
有很多东西需要看,但没有什么是我们现在不应该理解的...除了一个 strange
的事情:
<select th:field="*{rows[__${rowStat.index}__].variety}">
...
</select>
如果你回想一下《使用 Thymeleaf》教程中的内容,__${...}__
语法是一个预处理表达式,这是一个在评估整个表达式之前先评估的内部表达式。但为什么要以这种方式指定行索引呢?使用以下方式不够吗?
<select th:field="*{rows[rowStat.index].variety}">
...
</select>
...嗯,实际上,不够。问题是 Spring EL 不会在数组索引括号内评估变量,因此执行上述表达式时,我们会得到一个错误,告诉我们 rows[rowStat.index]
(而不是 rows[0]
, rows[1]
等)是行集合中的无效位置。这就是为什么这里需要预处理。
让我们来看看按下 “Add Row” 按钮几次后生成的 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>
# 十一、验证和错误消息
我们的大多数表单都需要显示验证消息,以通知用户他/她所犯的错误。
Thymeleaf 为此提供了一些工具:#fields
对象中的一对函数、th:errors
和 th:errorclass
属性。
# 1、字段错误
让我们看看我们如何在字段有错误时为字段设置特定的 CSS 类:
<input type="text" th:field="*{datePlanted}" th:class="${#fields.hasErrors('datePlanted')}? 'fieldError'" />
如您所见,#fields.hasErrors(...)
函数接收字段表达式作为参数 (datePlanted
),并返回一个布尔值,指示该字段是否存在任何验证错误。
我们还可以获取该字段的所有错误并对其进行迭代:
<ul>
<li th:each="err : ${#fields.errors('datePlanted')}">${err}</li>
</ul>
我们也可以使用 th:errors
,这是一个专门的属性,它构建了一个包含指定选择器所有错误的列表,用 <br />
分隔:
<input type="text" th:field="*{datePlanted}" />
<p th:if="${#fields.hasErrors('datePlanted')}" th:errors="*{datePlanted}">Incorrect date</p>
# 1.1、简化基于错误的 CSS 样式:th:errorclass
我们在上面看到的示例,如果字段有错误,则设置表单输入的 CSS 类,非常常见,因此 Thymeleaf 提供了一个特定的属性来做到这一点:th:errorclass
。
应用于表单字段标签(输入、选择、文本区域...),它将从同一标签中的任何现有 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" />
# 2、所有错误
如果我们想显示表单中的所有错误怎么办?我们只需使用 *
或 all
常量(它们是等效的)查询 #fields.hasErrors(...)
和 #fields.errors(...)
方法:
<ul th:if="${#fields.hasErrors('*')}">
<li th:each="err : ${#fields.errors('*')}">Input is incorrect</li>
</ul>
如上面的示例所示,我们可以获取所有错误并进行迭代...
<ul>
<li th:each="err : ${#fields.errors('*')}">${err}</li>
</ul>
...以及构建一个用 <br />
分隔的列表:
<p th:if="${#fields.hasErrors('all')}">
Incorrect date
</p>
最后注意,#fields.hasErrors('*')
等价于 #fields.hasAnyErrors()
,#fields.errors('*')
等价于 #fields.allErrors()
。使用你喜欢的任何语法:
<div th:if="${#fields.hasAnyErrors()}">
<p th:each="err : ${#fields.allErrors()}">...</p>
</div>
# 3、全局错误
在 Spring 表单中还有第三种类型的错误:全局 错误。这些错误与表单中的任何特定字段无关,但仍然存在。
Thymeleaf 提供了 global
常量来访问这些错误:
<ul th:if="${#fields.hasErrors('global')}">
<li th:each="err : ${#fields.errors('global')}">Input is incorrect</li>
</ul>
<p th:if="${#fields.hasErrors('global')}">
Incorrect date
</p>
...以及等效的 #fields.hasGlobalErrors()
和 #fields.globalErrors()
便捷方法:
<div th:if="${#fields.hasGlobalErrors()}">
<p th:each="err : ${#fields.globalErrors()}">...</p>
</div>
# 4、在表单外部显示错误
表单验证错误也可以通过使用变量 (${...}
) 而不是选择 (*{...}
) 表达式并为表单支持 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(而不是简单的 字符串)的形式获取表单错误信息的能力,具有 fieldName
(字符串)、message
(字符串)和 global
(布尔值)属性。
这些错误可以通过 #fields.detailedErrors()
实用方法获得:
<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 最美好的结果之一是,在我们向我们的 HTML 添加了所有这些功能之后,我们仍然可以将其用作原型(我们说它是一个 自然模板)。让我们在浏览器中直接打开 seedstartermng.html
而不执行我们的应用程序:
看!它不是一个正在运行的应用程序,它不是真实的数据...但它是一个完全有效的原型,由完全可显示的 HTML 代码组成。
# 十四、转换服务
# 1、配置
如前所述,Thymeleaf 可以使用在应用程序上下文中注册的转换服务。我们的应用程序配置类,通过扩展 Spring 自己的 WebMvcConfigurerAdapter
辅助类,将自动注册这样的转换服务,我们可以通过添加我们需要 格式化器 来进行配置。让我们再次看看它是什么样子的:
@Override
public void addFormatters(final FormatterRegistry registry) {
super.addFormatters(registry);
registry.addFormatter(varietyFormatter());
registry.addFormatter(dateFormatter());
}
@Bean
public VarietyFormatter varietyFormatter() {
return new VarietyFormatter();
}
@Bean
public DateFormatter dateFormatter() {
return new DateFormatter();
}
# 2、双括号语法
转换服务可以很容易地应用于将任何对象转换为字符串。这是通过双括号表达式语法完成的:
- 对于变量表达式:
${{ ... }}
- 对于选择表达式:
*{{ ... }}
因此,例如,给定一个将逗号作为千位分隔符的整数到字符串转换器,这:
<p th:text="${val}">...</p>
<p th:text="${{ val }}">...</p>
...应该导致:
<p>1234567890</p>
<p>1,234,567,890</p>
# 3、在表单中的使用
我们之前看到,每个 th:field
属性总是会应用转换服务,所以这:
<input type="text" th:field="*{datePlanted}" />
...实际上等同于:
<input type="text" th:field="*{{ datePlanted }}" />
请注意,根据 Spring 的要求,这是唯一一种在表达式中使用单括号语法应用转换服务的情况。
# 4、#conversions
实用对象
#conversions
表达式实用对象允许在需要时手动执行转换服务:
<p th:text="${'Val: ' + #conversions.convert(val,'String')}">...</p>
这个实用对象的语法:
#conversions.convert(Object,Class)
: 将对象转换为指定的类。#conversions.convert(Object,String)
: 与上面相同,但指定目标类作为字符串(注意可以省略java.lang.
包)。
# 十五、渲染模板片段
Thymeleaf 提供了只渲染模板的一部分作为其执行结果的可能性:一个 片段。
这可以是一个有用的组件化工具。例如,它可以用于在 AJAX 调用上执行的控制器,这些控制器可能返回页面中已加载的标记片段(用于更新选择、启用/禁用按钮等)。
可以通过使用 Thymeleaf 的 片段规范 来实现片段化渲染:实现 org.thymeleaf.fragment.IFragmentSpec
接口的对象。
这些实现中最常见的是 org.thymeleaf.standard.fragment.StandardDOMSelectorFragmentSpec
,它允许使用与 th:include
或 th:replace
中使用的 DOM 选择器完全相同的片段。
# 1、在视图 bean 中指定片段
视图 bean 是声明在应用程序上下文中的 org.thymeleaf.spring6.view.ThymeleafView
类的 bean(如果您使用的是 Java 配置,则为 @Bean
声明)。它们允许指定如下所示的片段:
@Bean(name="content-part")
@Scope("prototype")
public ThymeleafView someViewBean() {
ThymeleafView view = new ThymeleafView("index"); // templateName = 'index'
view.setMarkupSelector("content");
return view;
}
给定上述 bean 定义,如果我们的控制器返回 content-part
(上述 bean 的名称)...
@RequestMapping("/showContentPart")
public String showContentPart() {
...
return "content-part";
}
...thymeleaf 将只返回 index
模板的 content
片段——其位置可能是 index.html
,一旦应用了前缀和后缀。因此,结果将与指定 index :: content
完全等效:
<!DOCTYPE html>
<html>
...
<body>
...
<div th:fragment="content">
Only this div will be rendered!
</div>
...
</body>
</html>
请注意,多亏了 Thymeleaf 标记选择器的强大功能,我们可以在模板中选择一个片段,而不需要任何 th:fragment
属性。让我们使用 id
属性,例如:
@Bean(name="content-part")
@Scope("prototype")
public ThymeleafView someViewBean() {
ThymeleafView view = new ThymeleafView("index"); // templateName = 'index'
view.setMarkupSelector("#content");
return view;
}
...这将完美地选择:
<!DOCTYPE html>
<html>
...
<body>
...
<div id="content">
Only this div will be rendered!
</div>
...
</body>
</html>
# 十六、在控制器返回值中指定片段
除了声明 视图 bean,还可以通过使用 片段表达式 的语法从控制器本身指定片段。就像在 th:insert
或 th:replace
属性中一样:
@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')";
}
# 十七、高级集成功能
# 1、与 RequestDataValueProcessor
的集成
Thymeleaf 与 Spring 的 RequestDataValueProcessor
接口无缝集成。这个接口允许拦截链接 URL、表单 URL 和表单字段值,然后才将它们写入标记结果,以及透明地添加隐藏表单字段,以启用安全功能,例如,例如。保护免受 CSRF(跨站点请求伪造)。
RequestDataValueProcessor
的实现可以很容易地在应用程序上下文中配置。它需要实现 org.springframework.web.servlet.support.RequestDataValueProcessor
接口,并具有 requestDataValueProcessor
作为 bean 名称:
@Bean
public RequestDataValueProcessor requestDataValueProcessor() {
return new MyRequestDataValueProcessor();
}
...而 Thymeleaf 将以这种方式使用它:
th:href
和th:src
在渲染 URL 之前调用RequestDataValueProcessor.processUrl(...)
。th:action
在渲染表单的action
属性之前调用RequestDataValueProcessor.processAction(...)
,并且它还会检测何时将此属性应用于<form>
标签——无论如何,这应该是唯一的地方——在这种情况下,它调用RequestDataValueProcessor.getExtraHiddenFields(...)
并在关闭</form>
标签之前添加返回的隐藏字段。th:value
调用RequestDataValueProcessor.processFormFieldValue(...)
以渲染它所指的值的,除非在同一标签中存在th:field
(在这种情况下,th:field
将负责)。th:field
调用RequestDataValueProcessor.processFormFieldValue(...)
以渲染它所应用的字段的值(或如果是<textarea>
,则为标签正文)。
注意: 在您的应用程序中很少有场景需要您显式实现 RequestDataValueProcessor
。在大多数情况下,这将由您透明使用的安全库自动使用,例如,例如。Spring Security 的 CSRF 支持。
# 2、构建指向控制器的 URI
自版本 4.1 以来,Spring 允许直接从视图构建指向注解控制器的链接,无需知道这些控制器映射到的 URI。
在 Thymeleaf 中,这可以通过 #mvc.url(...)
表达式对象方法实现,该方法允许通过控制器类的首字母后跟方法本身的名称来指定控制器方法。这等同于 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()}">Get Data Param</a>
<a th:href="${(#mvc.url('EC#getDataParam').arg(0,'internal')).build()}">Get Data Param</a>
您可以在以下网址阅读有关此机制的更多信息: http://docs.spring.io/spring-framework/docs/4.1.2.RELEASE/spring-framework-reference/html/mvc.html#mvc-links-to-controllers-from-views
# 十八、Spring WebFlow 集成
# 1、基本配置
Thymeleaf + Spring 集成包包括与 Spring WebFlow 的集成。
注意: 当 Thymeleaf 与 Spring 6 一起使用时,需要 Spring WebFlow 3.0+,而与 Spring 5 一起使用时,需要 Spring WebFlow 2.5。
WebFlow 包括一些 AJAX 功能,用于在触发特定事件(转换)时渲染显示页面的片段,为了使 Thymeleaf 能够处理这些 AJAX 请求,我们将不得不使用不同的 ViewResolver
实现,配置如下:
@Bean
public FlowDefinitionRegistry flowRegistry() {
// 注意:您的应用程序中可能需要额外配置
return getFlowDefinitionRegistryBuilder()
.addFlowLocation("...")
.setFlowBuilderServices(flowBuilderServices())
.build();
}
@Bean
public FlowExecutor flowExecutor() {
// 注意:您的应用程序中可能需要额外配置
return getFlowExecutorBuilder(flowRegistry())
.build();
}
@Bean
public FlowBuilderServices flowBuilderServices() {
// 注意:您的应用程序中可能需要额外配置
return getFlowBuilderServicesBuilder()
.setViewFactoryCreator(viewFactoryCreator())
.build();
}
@Bean
public ViewFactoryCreator viewFactoryCreator() {
MvcViewFactoryCreator factoryCreator = new MvcViewFactoryCreator();
factoryCreator.setViewResolvers(
Collections.singletonList(thymeleafViewResolver()));
factoryCreator.setUseSpringBeanBinding(true);
return factoryCreator;
}
@Bean
public ViewResolver thymeleafViewResolver() {
AjaxThymeleafViewResolver viewResolver = new AjaxThymeleafViewResolver();
// 我们需要设置一个特殊的 ThymeleafView 实现:FlowAjaxThymeleafView
viewResolver.setViewClass(FlowAjaxThymeleafView.class);
viewResolver.setTemplateEngine(templateEngine());
return viewResolver;
}
注意,以上并不是一个完整的配置:你仍然需要配置你的处理程序等。请参阅 Spring WebFlow 文档以获取更多信息。
从这里开始,您可以在视图状态的指定 Thymeleaf 模板:
<view-state id="detail" view="bookingDetail">
...
</view-state>
在上面的示例中,bookingDetail
是一个以通常方式指定的 Thymeleaf 模板,任何配置在 TemplateEngine
中的 模板解析器 都能理解。
# 2、Spring WebFlow 中的 AJAX 片段
注意: 这里解释的是创建用于与 Spring WebFlow 一起使用的 AJAX 片段的方法。如果您不使用 WebFlow,创建一个响应 AJAX 请求并返回 HTML 块的 Spring MVC 控制器就像创建任何其他返回模板的控制器一样简单,唯一的例外是你可能会从控制器方法返回像 main :: admin
这样的片段。
WebFlow 允许使用 <render>
标签指定要通过 AJAX 渲染的片段,如下所示:
<view-state id="detail" view="bookingDetail">
<transition on="updateData">
<render fragments="hoteldata" />
</transition>
</view-state>
这些片段(在本例中为 hoteldata
)可以是标记中用 th:fragment
指定的片段的逗号分隔列表:
<div id="data" th:fragment="hoteldata">
This is a content to be changed
</div>
永远记住,指定的片段必须有一个 id
属性,以便在浏览器上运行的 Spring JavaScript 库能够替换标记。
<render>
标签也可以使用 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>