开发Web应用
建立Domain
Domain:一个应用的Domain就是这个应用处理的主题领域,Domain类就是对这个主题领域的抽象。在TacoCloud应用中,我们希望提供一个用户自己设计Taco的功能,那么Taco的成分类就属于该应用的Domain类。
import lombok.Data;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor
public class Ingredient {
private final String id;
private final String name;
private final Type type;
public static enum Type{
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
上面定义了一个简单的Ingredient
类,使用了@Data
注解,这个注解由Lombok
提供,我们需要在pom中添加lombok的依赖,并在IED中添加lombok插件。使用lombok,我们就不必再为普通的pojo手动的添加getter和setter方法以及一些必要的构造器了。在程序运行时,Lombok会自动的为我们加上这些方法。Lombok文档
创建一个Controller
@Controller
@Slf4j
@RequestMapping("/design")
public String showDesignForm(Model model){
List<Ingredient> ingredients = Arrays.asList(
new Ingredient("FLTO", "Flour Tortilla", Type.WRAP),
new Ingredient("COTO", "Corn Tortilla", Type.WRAP),
new Ingredient("GRBF", "Ground Beef", Type.PROTEIN),
new Ingredient("CARN", "Carnitas", Type.PROTEIN),
new Ingredient("TMTO", "Diced Tomatoes", Type.VEGGIES),
new Ingredient("LETC", "Lettuce", Type.VEGGIES),
new Ingredient("CHED", "Cheddar", Type.CHEESE),
new Ingredient("JACK", "Monterrey Jack", Type.CHEESE),
new Ingredient("SLSA", "Salsa", Type.SAUCE),
new Ingredient("SRCR", "Sour Cream", Type.SAUCE)
);
Type[] types = Ingredient.Type.values();
for(Type type : types){
model.addAttribute(type.toString().toLowerCase(),filterByType(ingredients,type));
}
model.addAttribute("design",new Taco());
return "design";
}
List<Ingredient> filterByType(List<Ingredient> ingredients, Type type){
List<Ingredient> result = new ArrayList<>();
for (int i = 0; i < ingredients.size(); i++) {
Ingredient ingredient = ingredients.get(i);
if (type == ingredient.getType()) {
result.add(ingredient);
}
}
return result;
}
上面的代码中,@Slf4j
注解是由Lombok提供的,在运行时自动生成一个SLF4J
。@RequestMapping("/design")
注解在class层,这个类中的方法将会处理以/design
开头的请求。Model
用于在Controller和view之间传递数据。放入Model的数据会被复制到Servlet响应参数中,视图使用的是Servlet响应参数的数据。
设计视图
<!DOCTYPE html>
<!-- 将表单数据存入对象中 -->
<form method="post" th:object="${design}">
<div class="grid">
<div class="ingredient-group" id="wraps">
<h3>Design your wrap:</h3>
<!-- 遍历wrap集合 -->
<div th:each="ingredient : ${wrap}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}">
<span th:text="${ingredient.name}">INGREDIENT</span>
<br>
</div>
</div>
</div>
<div>
<h3>Name your taco creation:</h3>
<input type="text" th:field="*{name}">
<br>
<button>Submit your taco</button>
</div>
</form>
</body>
</html>
视图使用Thymeleaf模板。我们在html文件中加入th
命名空间:<html xmlns:th="http://www.thymeleaf.org">
,然后开始使用Thymeleaf模板。
Thymeleaf的标准表达语法
- 基本语法
-
变量表达式:
${...}
:变量表达式实际上是
OGNL
,在Spring MVC 应用中,OGNL被替换为SpringEL(语法与OGNL十分相似)。 -
选择表达式:
*{...}
:选择表达式与
th:object=${objectName}
一起使用,在上面的html中,<input type="text" th:field="*{name}">
等价于<input type="text" th:field="${design.name}">
。 -
消息表达式:
#{...}
:使用消息表达式让国际化变得简单,我们可以引用外部的
.properties
文件中定义的值来替换html页面内容。在Spring中使用消息表达式,需要注册org.springframework.context.support.ResourceBundleMessageSource
bean:@Bean public ResourceBundleMessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("message");//这里设置properties文件名的前缀 return messageSource; }
我们在classpath下创建名为
message_en.properties
,message_cn_ZH.properties
,message.properties
的资源文件,并在其中定义消息。在SpringBoot中,我们只需要直接创建前缀为
messages
的properties文件即可(spring.messages.basename值默认为messages),也可以在application.properties中配置消息资源文件的前缀:spring.messages.basename=application
-
URL表达式:
@{...}
使用URL表达式来表达一个URL,那么在URL中我们就可以嵌入其它的表达式
-
片段表达式:
~{...}
:片段表达式让我们加强对HTML块的重用,我们在templates目录下创建一个fragment.html,并在其中定义一个名为header的fragment:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div th:fragment="header"> <h1 th:text="#{header.msg}"></h1> <img th:src="@{/images/TacosCloud.jpg}"> </div> </body> </html>
然后我们以前页面中的标题和图片就可以使用片段表达式来替换:
<div th:insert="~{fragment::header}"></div> <!-- 第一个值为html文件名,第二个值为文件中fragment的名字 -->
(未完待续。。。)
处理表单提交
前面的html表单中,method
属性为post,没有设置action
属性,那么我们提交表单时,浏览器将会收集表单数据并将其提交给一个HTTP POST 请求,这个请求的路径是/design
。我们需要在Controller中添加一个处理Post请求的方法。
@PostMapping
public String processDesign(Taco taco){
//do something with taco
return "redirect:/orders/current";
}
processDesign
方法也返回一个字符串,但是这个字符串有一个前缀redirect:
,这表明这是一个重定向视图。当用户设计了自己的Taco并提交后,浏览器会重定向到/orders/current
。
表单输入验证
Spring支持Java’s Bean Validation API(JSR-303) ,这个API使得定义校验规则变得简单,我们不需要通过编码来实现校验。
在Spring Boot应用中,不需要添加任何的依赖包。因为JSR-303 API和Hibernate实现的校验API在应用启动时被自动加入到项目中。
在Spring MVC中使用这个API,我们需要:
- 在需要被校验的类中声明校验规则
- 指定控制器方法来执行验证
- 修改视图的表单来展示验证的错误结果
JSR-303 API提供了几个注解,用来声明校验规则,Hibernate实现的API提供了更多的接口。
添加校验规则
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;
@Data
public class Taco {
//不为空,长度不少于5个字符
@NotNull
@Size(min=5,message = "name must be at least 5 characters long")
private String name;
//至少有一个元素
@Size(min=1,message="you must choose at least 1 ingredient")
private List<String> ingredients;
}
import lombok.Data;
import org.hibernate.validator.constraints.CreditCardNumber;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@Data
public class Order {
//不能为空,也不能只含空格
@NotBlank(message = "Name is required")
private String name;
@NotBlank(message = "Street is required")
private String street;
@NotBlank(message = "City is required")
private String city;
@NotBlank(message = "State is required")
private String state;
@NotBlank(message = "Zip is required")
private String zip;
//这是一个有Hibernate提供的注解,将检验ccNumber是否是合法的信用卡号码
@CreditCardNumber(message = "Not a valid credit card number")
private String ccNumber;
//提供一个正则表达式,校验值是否与表达式匹配
@Pattern(regexp = "(0[1-9]|1[0-2])[\\\\/]([1-9][0-9])",message = "Must be formatted MM/YY")
private String ccExpiration;
//值必须是数字
@Digits(integer = 3,fraction = 0,message = "Invalid CVV")
private String ccCVV;
}
表单绑定时完成校验
除了对domain class 添加注解集验证规则外,Controller中也需要一点修改:
@PostMapping
public String processDesing(@Valid Taco design, Errors errors){
if (errors.hasErrors()){
return "/design";
}
return "redirect:/orders/current";
}
@PostMapping
public String processOrder(@Valid Order order, Errors errors){
if (errors.hasErrors()){
return "orderForm";
}
log.info("Order submit:" + order);
return "redirect:/";
}
在控制器方法中,我们对表单数据绑定对象添加了@Valid
注解,并新增了一个org.springframework.validation.Errors
实例的新入参。@Valid
注解告诉Spring MVC在taco
、order
对象绑定了表单提交的数据之后,控制器方法被调用之前对表单数据进行验证。errors
可得到校验结果。
展示校验结果
在Thymeleaf模板中,使用field
和th:errors
可以轻松访问Error
对象。
<form method="post" th:object="${taco}">
<label for="name">Name: </label>
<input type="text" th:field="*{name}"/>
<span class="validationError" th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Name Num Error</span>
<br/>
</form>
${#fields}
就是对控制器中errors对象的引用。 th:if
控制是否显示这个span
,th:errors
用于指定使用哪个字段上的message来展示错误信息。
值得注意的是:在页面上th:object
对应的对象变量名应该是类名,或者是类名的驼峰命名,这样验证结果才会在页面显示。如上面processDesing方法我们是这样注解的:@Valid Taco design
,spring运行时的入参名称将不再是design,而是与类名相关的一个名称。所以在页面上绑定对象时,应该使用如"taco"这样的对象变量名称。
使用视图控制器
到目前为止,我们都是在Controller中定义一个方法来实现对HTTP GET请求的处理。我们可以使用视图控制器来达到同样的效果。
@Configuration
public class Config implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");//请求路径为/,跳转页面的视图逻辑名为home
}
}
WebMvcConfigurer
虽然是一个接口,但是它所定义的方法都有默认的实现,我们只需要覆盖addViewController
方法即可。该方法提供了一个org.springframework.web.servlet.config.annotation.ViewControllerRegistry
对象,我们可以通过它注册多个视图控制器。
选择视图模板
我们可以选择上面的视图模板,只需要加入其依赖即可。当应用启动之后,Spring Boot自动配置将探测到我们所选择的模板,并自动的配置模板bean。我们只需要在src/main/resources/templates
目录下编写模板即可。
Jsp不需要任何依赖,因为Web容器(默认为Tomcat)自身已经实现了对JSP的支持。但是如果我们的项目被打包成jar包,那么Jsp将不适合,因为容器总是在/WEB-INF目录下寻找jsp页面,然而spring boot项目并没有这个目录。所以Jsp模板只在当我们将项目打包成war包,并部署在传统web容器上时才能使用。
模板缓存设置
模板缓存默认开启,在application.properties
文件中,加上spring.thymeleaf.cache=false
,将关闭模板缓存。