整合完数据源MyBatis、SpringMVC之后,我们继续从后往前,到了页面展示的整合部分,之前我们的SSM框架使用的是JSP做为前端页面,在更早的Servlet时代用的也是JSP页面,但是时代变了,因为有了SpringBoot,JSP不再是前端页面的最佳选择了,本篇Blog就来介绍下一个SpringBoot官方推荐的默认模板引擎。
模板引擎概念模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档,其实JSP就是一个模板引擎。
通俗的讲,模板引擎的作用就是当我们创建一个前端页面时,有些值是动态的,需要写表达式从后端获取。模板引擎按照数据进行表达式解析、填充到页面指定的位置,然后把这个数据最终生成目标页面返回,不管是JSP还是我们本篇提到的Thymeleaf,抑或是其他模板引擎,功能作用都是一样的,不同模板引擎之间语法不同而已。
SpringBoot为什么不使用JSPJSP作为模板引擎的功能很强大,但是有两个原因导致SpringBoot不能使用JSP作为模板引擎
- SpringBoot用Jar打包而非War,springboot无法与jsp作为视图解析器一起正常使用的主要原因是因为Tomcat使用了硬编码的文件模式,当使用Jar打包时不会从WEB-INF文件夹中复制JSP文件,导致无法发现和解析JSP
- SpringBoot使用嵌入式Tomcat,默认情况下,嵌入式Tomcat软件包(在springboot中用于创建可执行jar)不包含JSP文件格式
所以可以理解为目前SpringBoot没有对JSP有很好的支持,而且更方便的选择还有很多,所以就抛弃了JSP转而使用Thymeleaf
ThymeleafThymeleaf 是一款用于渲染 XML/XHTML/HTML5 内容的模板引擎。它与 JSP,Velocity,FreeMaker 等模板引擎类似,也可以轻易地与 Spring MVC 、SpringBoot等 Web 框架集成。与其它模板引擎相比,Thymeleaf 最大的特点是,即使不启动 Web 应用,也可以直接在浏览器中打开并正确显示模板页面 。
1 Thymeleaf 基本特点与 Velocity、FreeMarker 等传统 Java 模板引擎不同,Thymeleaf 支持 HTML 原型,其文件后缀为.html:
- 当直接被浏览器打开时浏览器会忽略未定义的 Thymeleaf 标签属性,展示 thymeleaf 模板的静态页面效果;
- 当通过 Web 应用程序访问时,Thymeleaf 会动态地替换掉静态内容,使页面动态显示
Thymeleaf 通过在 html 标签中,增加额外属性来达到模板+数据的展示方式
Title 直接打开浏览器你是看不到我的动态数据的
例如我们直接从浏览器打开该文件:
通过Web程序启动的效果等我们通过SpringBoot整合Thymeleaf之后再查看。Thymeleaf整体具备如下特点:
- 动静结合:Thymeleaf 既可以直接使用浏览器打开,查看页面的静态效果,也可以通过 Web 应用程序进行访问,查看动态页面效果。
- 开箱即用:Thymeleaf 提供了 Spring 标准方言以及一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。
- 多方言支持:它提供了 Thymeleaf 标准和 Spring 标准两种方言,可以直接套用模板实现 JSTL、 OGNL 表达式;必要时,开发人员也可以扩展和创建自定义的方言。
- 与 SpringBoot 完美整合:SpringBoot 为 Thymeleaf 提供了的默认配置,并且还为 Thymeleaf 设置了视图解析器,因此 Thymeleaf 可以与 Spring Boot 完美整合。
在我看来其实最大的原因就是Thymeleaf能与SpringBoot完美结合,这让我们使用起来很方便。
2 Thymeleaf 语法规则在使用 Thymeleaf 之前,首先要在页面的 html 标签中声明名称空间
在 html 标签中声明此名称空间,可避免编辑器出现 html 验证错误,但这一步并非必须进行的,即使我们不声明该命名空间,也不影响 Thymeleaf 的使用,但我们最好配置上,防止不必要的错误。
Thymeleaf 语法分为:标准表达式语法,th 属性。
1 标准表达式语法Thymeleaf 模板引擎支持多种表达式,可类比JSP中的EL表达式。
- 变量表达式:${...}
- 选择变量表达式:*{...}
- 链接表达式:@{...}
- 国际化表达式:#{...}
- 片段引用表达式:~{...}
接下来我们分别试验下标准表达式语法的用法:
1 变量表达式变量表达式也依据获取内容的不同分为如下三类:
获取对象的属性和方法
使用变量表达式可以获取对象的属性和方法,例如,获取 person 对象的 lastName 属性,表达式形式如下:
${person.lastName}
使用内置的基本对象
使用变量表达式还可以使用内置基本对象,获取内置对象的属性,调用内置对象的方法。 Thymeleaf 中常用的内置基本对象如下:
#ctx :上下文对象; #vars :上下文变量; #locale:上下文的语言环境; #request:HttpServletRequest 对象(仅在 Web 应用中可用); #response:HttpServletResponse 对象(仅在 Web 应用中可用); #session:HttpSession 对象(仅在 Web 应用中可用); #servletContext:ServletContext 对象(仅在 Web 应用中可用)
例如,我们通过以下 2 种形式,都可以获取到 session 对象中的 map 属性
${#session.getAttribute('map')} ${session.map}
使用内置的工具对象
除了能使用内置的基本对象外,变量表达式还可以使用一些内置的工具对象
strings:字符串工具对象,常用方法有:equals、equalsIgnoreCase、length、trim、toUpperCase、toLowerCase、indexOf、substring、replace、startsWith、endsWith,contains 和 containsIgnoreCase 等; numbers:数字工具对象,常用的方法有:formatDecimal 等; bools:布尔工具对象,常用的方法有:isTrue 和 isFalse 等; arrays:数组工具对象,常用的方法有:toArray、length、isEmpty、contains 和 containsAll 等; lists/sets:List/Set 集合工具对象,常用的方法有:toList、size、isEmpty、contains、containsAll 和 sort 等; maps:Map 集合工具对象,常用的方法有:size、isEmpty、containsKey 和 containsValue 等; dates:日期工具对象,常用的方法有:format、year、month、hour 和 createNow 等
例如,我们可以使用内置工具对象 strings 的 equals 方法,来判断字符串与对象的某个属性是否相等,代码如下
${#strings.equals('tml',name)}2 选择变量表达式
选择变量表达式与变量表达式功能基本一致,只是在变量表达式的基础上增加了与 th:object 的配合使用。当使用 th:object 存储一个对象后,我们可以在其后代中使用选择变量表达式*{...}获取该对象中的属性,其中,*即代表该对象
3 链接表达式firstname
不管是静态资源的引用,还是 form 表单的请求,凡是链接都可以用链接表达式 @{...}。 链接表达式的形式结构如下: 无参请求:@{/xxx} ,有参请求:@{/xxx(k1=v1,k2=v2)} 例如使用链接表达式引入 css 样式表,代码如下
4 国际化表达式
国际化表达式一般用于国际化的场景
5 片段引用表达式片段引用表达式用于在模板页面中引用其他的模板片段,该表达式支持以下 2 中语法结构:
- 推荐:~{templatename::fragmentname}
- 支持:~{templatename::#id}
以上语法结构说明如下:
- templatename:模版名,Thymeleaf 会根据模版名解析完整径:/resources/templates/templatename.html,要注意文件的路径。
- fragmentname:片段名,Thymeleaf 通过 th:fragment 声明定义代码块,即:
- id:HTML 的 id 选择器,使用时要在前面加上 # 号,不支持 class 选择器
在公共页面抽取部分再详细叙述
2 th属性语法Thymeleaf 还提供了大量的 th 属性,有点类似于JSTL标签。这些属性可以直接在 HTML 标签中使用,其中常用 th 属性及其示例如下表
3 Thymeleaf 公共页面抽取
在 Web 项目中,通常会存在一些公共页面片段(重复代码),例如头部导航栏、侧边菜单栏和公共的 js css 等。我们一般会把这些公共页面片段抽取出来,存放在一个独立的页面中,然后再由其他页面根据需要进行引用,这样可以消除代码重复,使页面更加简洁
1 抽取公共页面Thymeleaf 作为一种优雅且高度可维护的模板引擎,同样支持公共页面的抽取和引用。我们可以将公共页面片段抽取出来,存放到一个独立的页面中,并使用 Thymeleaf 提供的 th:fragment 属性为这些抽取出来的公共页面片段命名,将公共页面片段抽取出来,存放在 commons.html 中,代码如下
2 引用公共页面公共页面片段在 Thymeleaf 中,我们可以使用以下 3 个属性,将公共页面片段引入到当前页面中。
- th:insert:将代码块片段整个插入到使用了 th:insert 属性的 HTML 标签中;
- th:replace:将代码块片段整个替换使用了 th:replace 属性的 HTML 标签中;
- th:include:将代码块片段包含的内容插入到使用了 th:include 属性的 HTML 标签中。
使用上 3 个属性引入页面片段,都可以通过以下 2 种方式实现。
- ~{templatename::#id}:模板名::选择器名
- ~{templatename::fragmentname}:模板名::片段名
也就是我们上边提到的片段引用表达式。在页面 fragment.html 中引入 commons.html 中声明的页面片段,可以通过以下方式实现
------------------------------------------------ ------------------------------------------------启动 Spring Boot,使用浏览器访问 fragment.html,查看源码,结果如下
3 传递参数公共页面片段------------------------------------------------公共页面片段公共页面片段公共页面片段------------------------------------------------公共页面片段公共页面片段Thymeleaf 在抽取和引入公共页面片段时,还可以进行参数传递,引用公共页面片段时,我们可以通过以下 2 种方式,将参数传入到被引用的页面片段中:
- 模板名::选择器名或片段名(参数1=参数值1,参数2=参数值2)
- 模板名::选择器名或片段名(参数值1,参数值2)
我们一般使用第一种,示例如下:
------------------------------------------------ ------------------------------------------------在commons.html公共页面我们可以使用这些参数:
...
启动 Spring Boot,使用浏览器访问
Spring Boot整合Thymeleaf
Spring Boot 推荐使用 Thymeleaf 作为其模板引擎。SpringBoot 为 Thymeleaf 提供了一系列默认配置,项目中一但导入了 Thymeleaf 的依赖,相对应的自动配置 ThymeleafAutoConfiguration 就会自动生效,因此 Thymeleaf 可以与 Spring Boot 完美整合
1 引入依赖Spring Boot 整合 Thymeleaf 的第一步,就是在项目的 pom.xml 中添加 Thymeleaf 的 Starter 依赖,代码如下
2 创建模板html文件org.springframework.boot spring-boot-starter-thymeleaf Spring Boot 通过 ThymeleafAutoConfiguration 自动配置类对 Thymeleaf 提供了一整套的自动化配置方案,该自动配置类的部分源码如下
@Configuration( proxyBeanMethods = false ) @EnableConfigurationProperties({ThymeleafProperties.class}) @ConditionalOnClass({TemplateMode.class, SpringTemplateEngine.class}) @AutoConfigureAfter({WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class}) public class ThymeleafAutoConfiguration { public ThymeleafAutoConfiguration() { }ThymeleafAutoConfiguration 使用 @EnableConfigurationProperties 注解导入了 ThymeleafProperties 类,该类包含了与 Thymeleaf 相关的自动配置属性,其部分源码如下
@ConfigurationProperties( prefix = "spring.thymeleaf" ) public class ThymeleafProperties { private static final Charset DEFAULT_ENCODING; //默认编码格式 public static final String DEFAULT_PREFIX = "classpath:/templates/"; //视图解析器的前缀 public static final String DEFAULT_SUFFIX = ".html"; //视图解析器的后缀 private boolean checkTemplate = true; private boolean checkTemplateLocation = true; private String prefix = "classpath:/templates/"; private String suffix = ".html"; private String mode = "HTML"; private Charset encoding; private boolean cache; private Integer templateResolverOrder; private String[] viewNames; private String[] excludedViewNames; private boolean enableSpringElCompiler; private boolean renderHiddenMarkersBeforeCheckboxes; private boolean enabled; private final ThymeleafProperties.Servlet servlet; private final ThymeleafProperties.Reactive reactive; ····· }ThymeleafProperties 通过 @ConfigurationProperties 注解将配置文件(application.properties/yml) 中前缀为 spring.thymeleaf 的配置和这个类中的属性绑定,通过配置默认值可知:Thymeleaf 模板的默认位置在 resources/templates 目录下,默认的后缀是 html,即只要将 HTML 页面放在classpath:/templates/下,Thymeleaf 就能自动进行渲染。我们创建一个html文件:
3 创建Controllertml第一个thymeleaf页面 测试页面
直接打开浏览器你是看不到我的动态数据的
然后我们请求Controller页面,用来路由结果到hello.html页面
package com.example.springboot.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import java.util.Arrays; import java.util.Map; @Controller public class HelloController { @GetMapping("/hello-thymeleaf") public String test2(Map4 测试实现map){ //存入数据 map.put("msg","Hello"); map.put("users", Arrays.asList("gcy","tml")); //classpath:/templates/hello.html return "hello"; } } 我们在页面请求测试下:http://localhost:8080/hello-thymeleaf
SpringBoot页面国际化
国际化(Internationalization 简称 I18n,其中“I”和“n”分别为首末字符,18 则为中间的字符数)是指软件开发时应该具备支持多种语言和地区的功能。换句话说就是,开发的软件需要能同时应对不同国家和地区的用户访问,并根据用户地区和语言习惯,提供相应的、符合用具阅读习惯的页面和数据,例如,为中国用户提供汉语界面显示,为美国用户提供提供英语界面显示
1 编写国际化资源文件在 Spring Boot 的类路径下创建国际化资源文件,文件名格式为:基本名_语言代码_国家或地区代码,例如 login_en_US.properties、login_zh_CN.properties。
以 spring-boot-springmvc-demo1为例,在 src/main/resources 下创建一个 i18n 的目录,并在该目录中按照国际化资源文件命名格式分别创建以下三个文件,
- login.properties:无语言设置时生效
- login_en_US.properties :英语时生效
- login_zh_CN.properties:中文时生效
以上国际化资源文件创建完成后,IDEA 会自动识别它们,编写并配置国际化资源文件如下:
login.properties
loginBtn=登录 password=密码 registerBtn=注册 username=用户名login_en_US.properties
loginBtn=Login password=PassWord registerBtn=Register username=UserNamelogin_zh_CN.properties
loginBtn=登录 password=密码 registerBtn=注册 username=用户名2 application.yml配置管理国际化资源文件Spring Boot 已经对 ResourceBundleMessageSource 提供了默认的自动配置。Spring Boot 通过 MessageSourceAutoConfiguration 对 ResourceBundleMessageSource 提供了默认配置,其部分源码如下
@Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @Conditional(org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration.ResourceBundleCondition.class) @EnableConfigurationProperties public class MessageSourceAutoConfiguration { private static final Resource[] NO_RESOURCES = {}; // 将 MessageSourceProperties 以组件的形式添加到容器中 // MessageSourceProperties 下的每个属性都与以 spring.messages 开头的属性对应 @Bean @ConfigurationProperties(prefix = "spring.messages") public MessageSourceProperties messageSourceProperties() { return new MessageSourceProperties(); } //Spring Boot 会从容器中获取 MessageSourceProperties // 读取国际化资源文件的 basename(基本名)、encoding(编码)等信息 // 并封装到 ResourceBundleMessageSource 中 @Bean public MessageSource messageSource(MessageSourceProperties properties) { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); //读取国际化资源文件的 basename (基本名),并封装到 ResourceBundleMessageSource 中 if (StringUtils.hasText(properties.getbasename())) { messageSource.setbasenames(StringUtils .commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getbasename()))); } //读取国际化资源文件的 encoding (编码),并封装到 ResourceBundleMessageSource 中 if (properties.getEncoding() != null) { messageSource.setDefaultEncoding(properties.getEncoding().name()); } messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale()); Duration cacheDuration = properties.getCacheDuration(); if (cacheDuration != null) { messageSource.setCacheMillis(cacheDuration.toMillis()); } messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat()); messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage()); return messageSource; } ... }从以上源码可知:
- Spring Boot 将 MessageSourceProperties 以组件的形式添加到容器中;
- MessageSourceProperties 的属性与配置文件中以“spring.messages”开头的配置进行了绑定;
- Spring Boot 从容器中获取 MessageSourceProperties 组件,并从中读取国际化资源文件的 basename(文件基本名)、encoding(编码)等信息,将它们封装到ResourceBundleMessageSource 中;
- Spring Boot 将 ResourceBundleMessageSource 以组件的形式添加到容器中,进而实现对国际化资源文件的管理。
查看 MessageSourceProperties 类,其代码如下
public class MessageSourceProperties { private String basename = "messages"; private Charset encoding; @DurationUnit(ChronoUnit.SECONDS) private Duration cacheDuration; private boolean fallbackToSystemLocale; private boolean alwaysUseMessageFormat; private boolean useCodeAsDefaultMessage; public MessageSourceProperties() { this.encoding = StandardCharsets.UTF_8; this.fallbackToSystemLocale = true; this.alwaysUseMessageFormat = false; this.useCodeAsDefaultMessage = false; } ... }通过以上代码,我们可以得到以下 3 点信息:
- MessageSourceProperties 为 basename、encoding 等属性提供了默认值;
- basename 表示国际化资源文件的基本名,其默认取值为“messages”,即 Spring Boot 默认会获取类路径下的 message.properties 以及 message_XXX.properties 作为国际化资源文件;
- 在 application.porperties/yml 等配置文件中,使用配置参数“spring.messages.basename”即可重新指定国际化资源文件的基本名。
通过以上源码分析可知,Spring Boot 已经对国际化资源文件的管理提供了默认自动配置,我们这里只需要在 Spring Boot 全局配置文件中,使用配置参数“spring.messages.basename”指定我们自定义的国际资源文件的基本名即可,代码如下(当指定多个资源文件时,用逗号分隔)
application.yml
spring: #页面国际化配置 messages: basename: i18n.login encoding: UTF-8 cache-duration: 13 区域信息解析器自动配置我们知道,Spring MVC 进行国际化时有 2 个十分重要的对象:
- Locale:区域信息对象
- LocaleResolver:区域信息解析器,容器中的组件,负责获取区域信息对象
我们可以通过以上两个对象对区域信息的切换,以达到切换语言的目的,Spring Boot 在 WebMvcAutoConfiguration 中为区域信息解析器(LocaleResolver)进行了自动配置,源码如下
@Bean @ConditionalOnMissingBean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME) @SuppressWarnings("deprecation") public LocaleResolver localeResolver() { if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) { return new FixedLocaleResolver(this.webProperties.getLocale()); } if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) { return new FixedLocaleResolver(this.mvcProperties.getLocale()); } AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); Locale locale = (this.webProperties.getLocale() != null) ? this.webProperties.getLocale() : this.mvcProperties.getLocale(); localeResolver.setDefaultLocale(locale); return localeResolver; }从以上源码可知:
- 该方法默认向容器中添加了一个区域信息解析器(LocaleResolver)组件,它会根据请求头中携带的“Accept-Language”参数,获取相应区域信息(Locale)对象。
- 该方法上使用了 @ConditionalOnMissingBean 注解,其参数 name 的取值为 localeResolver(与该方法注入到容器中的组件名称一致),该注解的含义为:当容器中不存在名称为 localResolver 组件时,该方法才会生效。
当我们手动向容器中添加一个名为“localeResolver”的组件时,Spring Boot 自动配置的区域信息解析器会失效,而我们定义的区域信息解析器则会生效。创建一个 component 包,并在该包中创建一个区域信息解析器 MyLocalResolver:
MyLocalResolver.java
package com.example.springboot.component; import org.springframework.util.StringUtils; import org.springframework.web.servlet.LocaleResolver; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Locale; public class MyLocalResolver implements LocaleResolver { @Override public Locale resolveLocale(HttpServletRequest request) { //获取请求中参数 String l = request.getParameter("l"); //获取默认的区域信息解析器 Locale locale = Locale.getDefault(); //根据请求中的参数重新构造区域信息对象 if (StringUtils.hasText(l)) { System.out.println(l); String[] s = l.split("_"); locale = new Locale(s[0], s[1]); } return locale; } @Override public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { } }4 SpringMVC定制化配置我们需要定制化配置SpringMVC,让我们的视图解析器生效:
MyMvcConfig
package com.example.springboot.config; import com.example.springboot.component.MyLocalResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.config.annotation.*; @Configuration public class MyMvcConfig implements WebMvcConfigurer { //添加视图控制器 @Override public void addViewControllers(ViewControllerRegistry registry) { //当访问/或者/index.html都会跳转到首页 registry.addViewController("/").setViewName("login"); registry.addViewController("/login.html").setViewName("login"); } //将自定义的区域信息解析器以组件的形式添加到容器中 @Bean public LocaleResolver localeResolver(){ return new MyLocalResolver(); } }5 获取国际化内容通过Controller路由到html,所以我们需要写一个Controller
LoginTestController
package com.example.springboot.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class LoginTestController { @GetMapping("/login-i18") public String loginTest(){ return "login"; } }通过如下语法获取国际化内容:
login.html
实现效果如下
当我们点击English时:
切换回中文时:
总结一下
本篇Blog详细了解了Thymeleaf的一些语法以及SpringBoot是如何定制整合Thymeleaf模板引擎,本质上Thymeleaf和JSP没有什么区别,只不过在SpringBoot时代,Thymeleaf能更好的和SpringBoot使用,有定义好的场景启动器starter供开发者方便的使用,而且还有方便使用的国际化配置。我们只需要简单的配置即可使用这个模板,当然和之前聊JSP时一样,在前后端分离时代其实我们更多的是通过RestController和前端交互,很少有后端自己定义前端页面,不过了解一下也好,对于小型项目,了解了Thymeleaf使用之后自己就可以独立搞定一个能完整运行的前后端都有的项目了,多学一点总归没有坏处。