Thymeleaf + Spring中的验证
总览
我们将要讨论的重要主题涉及空值,空字符串和输入验证,因此我们不会在数据库中输入无效数据。
在处理空值时,我们使用了Java 1.8中引入的java.util.Optional 。
0 – Spring Boot + Thymeleaf示例表单验证应用程序
我们正在为一所大学构建一个Web应用程序,使潜在的学生可以请求有关其课程的信息。
查看并从 Github 下载代码
1 –项目结构
2 –项目依赖性
除了典型的Spring Boot依赖关系之外,我们还在 LEGACYHTML5模式下使用嵌入式HSQLDB数据库和nekohtml 。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.michaelcgood</groupId> <artifactId>michaelcgood-validation-thymeleaf</artifactId> <version>0.0.1</version> <packaging>jar</packaging> <name>michaelcgood-validation-thymeleaf</name> <description>Michael C Good - Validation in Thymeleaf Example Application</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.7.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- legacy html allow --> <dependency> <groupId>net.sourceforge.nekohtml</groupId> <artifactId>nekohtml</artifactId> <version>1.9.21</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
3 –模型
在我们的模型中,我们定义:
- 自动生成的ID字段
- 名称字段不能为空
- 名称必须在2到40个字符之间
- 由@Email批注验证的电子邮件字段
- 布尔字段“开放日”,允许潜在学生指出她是否想参加开放日
- 布尔字段“订阅”,用于订阅电子邮件更新
- 注释字段是可选的,因此没有最低字符要求,但有最高字符要求
package com.michaelcgood.model; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import org.hibernate.validator.constraints.Email; @Entity public class Student { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @NotNull @Size(min=2, max=40) private String name; @NotNull @Email private String email; private Boolean openhouse; private Boolean subscribe; @Size(min=0, max=300) private String comments; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public Boolean getOpenhouse() { return openhouse; } public void setOpenhouse(Boolean openhouse) { this.openhouse = openhouse; } public Boolean getSubscribe() { return subscribe; } public void setSubscribe(Boolean subscribe) { this.subscribe = subscribe; } public String getComments() { return comments; } public void setComments(String comments) { this.comments = comments; } }
4 –储存库
我们定义一个存储库。
package com.michaelcgood.dao; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.michaelcgood.model.Student; @Repository public interface StudentRepository extends JpaRepository<Student,Long> { }
5 –控制器
我们注册StringTrimmerEditor将空字符串自动转换为空值。
当用户发送POST请求时,我们希望接收该Student对象的值,因此我们使用@ModelAttribute来做到这一点。
为确保用户发送的值有效,我们接下来使用适当命名的@Valid批注。
BindingResult必须紧随其后,否则在提交无效数据而不是保留在表单页面时, 将为用户提供错误页面。
我们使用if ... else来控制用户提交表单时发生的情况。 如果用户提交了无效数据,则该用户将保留在当前页面上,而在服务器端将不再发生任何事情。 否则,应用程序将使用用户的数据,并且用户可以继续。
在这一点上,检查学生的姓名是否为空是多余的,但是我们这样做。 然后,我们调用下面定义的方法checkNullString ,以查看注释字段是空的String还是null。
package com.michaelcgood.controller; import java.util.Optional; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.propertyeditors.StringTrimmerEditor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import com.michaelcgood.dao.StudentRepository; import com.michaelcgood.model.Student; @Controller public class StudentController { @InitBinder public void initBinder(WebDataBinder binder) { binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); } public String finalString = null; @Autowired private StudentRepository studentRepository; @PostMapping(value="/") public String addAStudent(@ModelAttribute @Valid Student newStudent, BindingResult bindingResult, Model model){ if (bindingResult.hasErrors()) { System.out.println("BINDING RESULT ERROR"); return "index"; } else { model.addAttribute("student", newStudent); if (newStudent.getName() != null) { try { // check for comments and if not present set to 'none' String comments = checkNullString(newStudent.getComments()); if (comments != "None") { System.out.println("nothing changes"); } else { newStudent.setComments(comments); } } catch (Exception e) { System.out.println(e); } studentRepository.save(newStudent); System.out.println("new student added: " + newStudent); } return "thanks"; } } @GetMapping(value="thanks") public String thankYou(@ModelAttribute Student newStudent, Model model){ model.addAttribute("student",newStudent); return "thanks"; } @GetMapping(value="/") public String viewTheForm(Model model){ Student newStudent = new Student(); model.addAttribute("student",newStudent); return "index"; } public String checkNullString(String str){ String endString = null; if(str == null || str.isEmpty()){ System.out.println("yes it is empty"); str = null; Optional<String> opt = Optional.ofNullable(str); endString = opt.orElse("None"); System.out.println("endString : " + endString); } else{ ; //do nothing } return endString; } }
Optional.ofNullable(str); 表示字符串将成为数据类型Optional,但是字符串可以为空值。
endString = opt.orElse(“ None”); 如果变量opt为null,则将String值设置为“ None”。
6 – Thymeleaf模板
正如您在上面的Controller映射中所看到的,有两个页面。 index.html是我们的主页,具有针对潜在大学生的表格。
我们的主要对象是学生,因此我们的th:object当然是指该对象 。 我们模型的字段分别进入th:field 。
我们将表单的输入包装在表格中以进行格式化。
在每个表单元格(td)下,我们都有一个条件语句,如下所示: […] th:if =” $ {#fields.hasErrors('name')}“ th:errors =” * {name}”
[…]
上面的条件语句意味着,如果用户在该字段中输入的数据与我们在Student模型中对该字段的要求不符,然后提交表单,则在用户返回此页面时显示输入要求。
index.html
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <!-- CSS INCLUDE --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"></link> <!-- EOF CSS INCLUDE --> </head> <body> <!-- START PAGE CONTAINER --> <div class="container-fluid"> <!-- PAGE TITLE --> <div class="page-title"> <h2> <span class="fa fa-arrow-circle-o-left"></span> Request University Info </h2> </div> <!-- END PAGE TITLE --> <div class="column"> <form action="#" th:action="@{/}" th:object="${student}" method="post"> <table> <tr> <td>Name:</td> <td><input type="text" th:field="*{name}"></input></td> <td th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Name Error</td> </tr> <tr> <td>Email:</td> <td><input type="text" th:field="*{email}"></input></td> <td th:if="${#fields.hasErrors('email')}" th:errors="*{email}">Email Error</td> </tr> <tr> <td>Comments:</td> <td><input type="text" th:field="*{comments}"></input></td> </tr> <tr> <td>Open House:</td> <td><input type="checkbox" th:field="*{openhouse}"></input></td> </tr> <tr> <td>Subscribe to updates:</td> <td><input type="checkbox" th:field="*{subscribe}"></input></td> </tr> <tr> <td> <button type="submit" class="btn btn-primary">Submit</button> </td> </tr> </table> </form> </div> <!-- END PAGE CONTENT --> <!-- END PAGE CONTAINER --> </div> <script src="https://code.jquery.com/jquery-1.11.1.min.js" integrity="sha256-VAvG3sHdS5LqTT+5A/aeq/bZGa/Uj04xKxY8KM/w9EE=" crossorigin="anonymous"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> </body> </html>
这是用户成功完成表单后看到的页面。 我们使用th:text向用户显示他或她在该字段中输入的文本。
Thanks.html
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <!-- CSS INCLUDE --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"></link> <!-- EOF CSS INCLUDE --> </head> <body> <!-- START PAGE CONTAINER --> <div class="container-fluid"> <!-- PAGE TITLE --> <div class="page-title"> <h2> <span class="fa fa-arrow-circle-o-left"></span> Thank you </h2> </div> <!-- END PAGE TITLE --> <div class="column"> <table class="table datatable"> <thead> <tr> <th>Name</th> <th>Email</th> <th>Open House</th> <th>Subscribe</th> <th>Comments</th> </tr> </thead> <tbody> <tr th:each="student : ${student}"> <td th:text="${student.name}">Text ...</td> <td th:text="${student.email}">Text ...</td> <td th:text="${student.openhouse}">Text ...</td> <td th:text="${student.subscribe}">Text ...</td> <td th:text="${student.comments}">Text ...</td> </tr> </tbody> </table> </div> </div> <!-- END PAGE CONTAINER --> </div> <script src="https://code.jquery.com/jquery-1.11.1.min.js" integrity="sha256-VAvG3sHdS5LqTT+5A/aeq/bZGa/Uj04xKxY8KM/w9EE=" crossorigin="anonymous"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> </body> </html>
7 –配置
使用Spring Boot Starter并包括Thymeleaf依赖项,您将自动拥有/ templates /的模板位置,并且Thymeleaf可以直接使用。 因此,不需要大多数这些设置。
要注意的一个设置是nekohtml提供的LEGACYHTM5 。 如果需要,这使我们可以使用更多随意HTML5标签。 否则,Thymeleaf将非常严格,并且可能无法解析您HTML。 例如,如果您不关闭输入标签,Thymeleaf将不会解析您HTML。
application.properties
#================================== # = Thymeleaf configurations #================================== spring.thymeleaf.check-template-location=true spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html spring.thymeleaf.content-type=text/html spring.thymeleaf.cache=false spring.thymeleaf.mode=LEGACYHTML5 server.contextPath=/
8 –演示
主页
无效数据
我在名称字段和电子邮件字段中输入了无效数据。
有效数据,无评论
现在,我在所有字段中输入了有效数据,但未提供注释。 不需要提供评论。 在我们的控制器中,我们使所有空字符串都为空值。 如果用户未提供注释,则将String值设置为“ None”。
9 –结论
包起来
该演示应用程序演示了如何以Thymeleaf形式验证用户输入。
我认为,Spring和Thymeleaf与javax.validation.constraints一起可以很好地验证用户输入。
源代码在 Github上
笔记
Java 8的Optional出于某种演示目的而被强加到该应用程序中,我想指出的是,当使用@RequestParam时 ,如我的PagingAndSortingRepository教程中所示,它可以更有机地工作。
但是,如果您不使用Thymeleaf,则可以将我们不需要的字段设置为Optional 。 Vlad Mihalcea在这里讨论了使用JPA和Hibernate映射Optional实体属性的最佳方法 。
翻译自: https://www.javacodegeeks.com/2017/10/validation-thymeleaf-spring.html