轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

  • Spring

    • 基础

    • 框架

    • Spring Boot

    • 集成

      • Thymeleaf 与 Spring 框架的集成教程
        • 一、前言
        • 二、将 Thymeleaf 与 Spring 集成
        • 三、SpringStandard 方言
        • 四、视图和视图解析器
          • 1、Spring MVC 中的视图和视图解析器
          • 2、Thymeleaf 中的视图和视图解析器
        • 五、Spring Thyme SeedStarter Manager
          • 1、概念
          • 2、业务层
        • 六、Spring MVC 配置
        • 七、控制器
          • 1、模型属性
          • 2、映射方法
        • 八、配置转换服务
        • 九、列出种子启动器数据
        • 十、创建表单
          • 1、处理命令对象
          • 2、输入
          • 3、复选框字段
          • 4、单选按钮字段
          • 5、下拉列表/列表选择器
          • 6、动态字段
          • 十一、验证和错误消息
          • 1、字段错误
          • 1.1、简化基于错误的 CSS 样式:th:errorclass
          • 2、所有错误
          • 3、全局错误
          • 4、在表单外部显示错误
          • 十二、富错误对象
          • 十三、它仍然是一个原型!
          • 十四、转换服务
          • 1、配置
          • 2、双括号语法
          • 3、在表单中的使用
          • 4、#conversions 实用对象
          • 十五、渲染模板片段
          • 1、在视图 bean 中指定片段
          • 十六、在控制器返回值中指定片段
          • 十七、高级集成功能
          • 1、与 RequestDataValueProcessor 的集成
          • 2、构建指向控制器的 URI
          • 十八、Spring WebFlow 集成
          • 1、基本配置
          • 2、Spring WebFlow 中的 AJAX 片段
  • 其他语言

  • 工具

  • 后端
  • Spring
  • 集成
轩辕李
2024-03-20
目录

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(也适用于其他 Spring 组件)。

Thymeleaf 为 Spring 5.x 和 6.x 版本分别提供了专用集成库:thymeleaf-spring5 和 thymeleaf-spring6。两个库的 jar 包分别为 thymeleaf-spring5-{版本}.jar 和 thymeleaf-spring6-{版本}.jar,请根据项目所用的 Spring 版本选择对应的 jar 包并添加到类路径中。

本教程的代码示例基于 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 编译器
  • 使用 Spring EL 语法访问应用上下文中的任意 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) 示例应用程序中找到:

  • Spring 5 STSM (opens new window)
  • Spring 6 STSM (opens new window)

# 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 &lt;- TemplateEngine &lt;- 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”。

请注意,这特别困难,因为这些枚举值也需要像我们对类型所做的那样外部化。因此,流程是:

  1. 为 features 数组的所有元素添加相应的前缀。
  2. 获得步骤 1 中所有键对应的外部化消息。
  3. 使用逗号作为分隔符将步骤 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>
编辑 (opens new window)
#Thymeleaf#Spring集成
上次更新: 2025/08/16
Spring Boot版本新特性
C语言指针二三事

← Spring Boot版本新特性 C语言指针二三事→

最近更新
01
AI时代的编程心得
09-11
02
Claude Code与Codex的协同工作
09-01
03
Claude Code实战之供应商切换工具
08-18
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式