《Spring实战》(第6版)第2章开发Web应用
- 互联网
- 2025-09-09 23:18:02

第2章 开发Web应用
第一印象非常重要,外观足够吸引人的房子更有可能卖掉,即使购房者甚至没有进门。
应用第一印象来源于,用户界面(User Interface,UI)是以浏览器Web应用的形式来展现的。
2.1 展现信息SpringWeb应用中,获取和处理数据是控制器的任务,将数据渲染到HTML中并在浏览器中展现是视图的任务。为支撑taco的创建页面,我们构建如下组件:
定义taco配料属性的领域类获取配料信息并将其传递给视图的SpringMVC控制器类。在用户的浏览器中渲染配料列表的视图模版。 2.1.1 构建领域类应用的领域指的是它所要解决的主题范围,影响应用理解的理念和概念。
Taco Cloud应用中,领域对象包括taco设计、组成这些设计的配料、顾客以及顾客所下的taco订单。
定义taco配料,Ingredient,美/ɪnˈɡriːdiənt/,原料,配料 package tacos; import lombok.Data; @Data public class Ingredient { private String id; private String name; private Type type; public enum Type { WRAP,//包,裹,缠绕 PROTEIN,//蛋白质 VEGGIES,//蔬菜 CHEESE,//干酪,奶酪 SAUCE//酱 } }安装Lombok的库。
右击pom.xml —> Spring —> Add Starters —> Lombok —> 添加完成。
可以手动添加:
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>Maven插件排除
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </path> </annotationProcessorPaths> </configuration> </plugin>Eclipse也要安装插件Lombok,否则出错。
先下载:
http:// .onlinedown.net/soft/10002347.htm
再安装:
blogs /money131/p/11204780.html
通过网盘分享的文件:LombokJar.rar
链接: pan.baidu /s/1NAhH3ya6OvZCTU8ArMLYfQ?pwd=g33s 提取码: g33s
–来自百度网盘超级会员v6的分享
如果插件不生效,生成普通方法吧。
定义taco设计的领域对象 package tacos; import java.util.List; import lombok.Data; @Data public class Taco { private String name; private List<Ingredient> ingredients; }需要类定义订购taco并明确支付信息和投递信息(配送地址)。
taco订单的领域对象 package tacos; import java.util.ArrayList; import java.util.List; import lombok.Data; @Data public class TacoOrder { private String deliveryName; private String deliveryStreet; private String deliveryCity; private String deliveryState; private String deliveryZip; private String ccNumber; private String ccExpiration; private String ccCVV; private List<Taco> tacos = new ArrayList<>(); public void addTaco(Taco taco) { this.tacos.add(taco); } } 2.1.2 创建控制器类创建简单控制器,完成如下功能:
处理路径为"/design"的HTTP GET请求。构建配料的列表。处理请求,并将配料数据传递给要渲染的HTML的视图模版,发给请求的Web浏览器。 初始的Spring控制器类 package tacos.web; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.SessionAttributes; import lombok.extern.slf4j.Slf4j; import tacos.Ingredient; import tacos.Ingredient.Type; import tacos.Taco; import tacos.TacoOrder; @Slf4j @Controller @RequestMapping("/design") @SessionAttributes("tacoOrder") public class DesignTacoController { @ModelAttribute public void addIngredientsToModel(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)); } } @ModelAttribute(name = "tacoOrder") public TacoOrder order() { return new TacoOrder(); } @ModelAttribute(name = "taco") public Taco taco() { return new Taco(); } @GetMapping public String showDesignForm() { return "design"; } private Iterable<Ingredient> filterByType(List<Ingredient> ingredients,Type type){ return ingredients .stream() .filter(x -> x.getType().equals(type)) .collect(Collectors.toList()); } }@SessionAttributes(“tacoOrder”)表明这个类中稍后放到模型里面的TacoOrder对象应该在会话中一直保持,这很重要,创建taco也是创建订单的第一步,我们创建的订单需要在会话中保存,这样能够使其跨多个请求。
处理GET请求处理路径,类上路径+方法路径,类中是/design进行处理。
SpringMVC的请求映射注解showDesignForm()返回design的视图逻辑名称。
@ModelAttribute,拥有这个注解的方法会在请求处理的时候调用,构建一个包含Ingredient的配料列表并将其放到模型中。
过滤后,配料类型的列表会以属性的形式添加到Model对象上,并传给showDesignForm()方法。Model对象负责在控制器和展现数据的视图之间传递数据。
实际上,Model属性中的数据将会复制到Servlet Request的属性中,视图会找到它们并使用它们渲染页面。(这里最好学习Servlet相关知识,才更好理解,多从request中取取属性)
2.1.3 设计视图Spring提供了多种定义试图的方式,JSP,Thymeleaf、FreeMarker、Mustache等。
当前使用Thymeleaf,与Web框架解耦无法感知Spring抽象模型,无法与控制器放到Model中的数据协同工作,但可以与Servlet的request属性协作,Spring将请求转移到视图之前,会将模型数据复制到request属性中,Thymeleaf就可以访问它们了。
Thymeleaf语法略,自查吧。
设计taco的完整页面 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Taco Cloud</title> <link rel="stylesheet" th:href="@{/styles.css}" > </head> <body> <h1>Design your taco!</h1> <img th:src="@{/images/TacoCloud.png}" height="200px"> <form method="post" th:object = "${taco}"> <div class="grid"> <div class="ingredient-group" id="wraps"> <h3>Designate your wrap:</h3> <div th:each="ingredient: ${wrap}"> <input th:field="*{ingredients}" type="checkbox" th:value="${ingredient.id}"> <span th:text="${ingredient.name}">INGREDIENT</span> </div> </div> <div class="ingredient-group" id="proteins"> <h3>Pick your protein:</h3> <div th:each="ingredient: ${protein}"> <input th:field="*{ingredients}" type="checkbox" th:value="${ingredient.id}"> <span th:text="${ingredient.name}">INGREDIENT</span> </div> </div> <div class="ingredient-group" id="cheeses"> <h3>Choose your cheese:</h3> <div th:each="ingredient: ${cheese}"> <input th:field="*{ingredients}" type="checkbox" th:value="${ingredient.id}"> <span th:text="${ingredient.name}">INGREDIENT</span> </div> </div> <div class="ingredient-group" id="veggies"> <h3>Determine your veggies:</h3> <div th:each="ingredient: ${veggies}"> <input th:field="*{ingredients}" type="checkbox" th:value="${ingredient.id}"> <span th:text="${ingredient.name}">INGREDIENT</span> </div> </div> <div class="ingredient-group" id="sauces"> <h3>Select your sauce:</h3> <div th:each="ingredient: ${sauce}"> <input th:field="*{ingredients}" type="checkbox" th:value="${ingredient.id}"> <span th:text="${ingredient.name}">INGREDIENT</span> </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> 2.2 处理表单提交 标签中,会发现method属性设置为post,并且没有声明action属性,提交的时候,浏览器会收集所有的数据,以POST请求的形式将其发送到服务端,路径与get请求相同,也就是"/design"。控制器类中需要增加一个新的方法处理"/design"的post请求。
使用@PostMapping来处理请求 package tacos.web; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.SessionAttributes; import tacos.Ingredient; import tacos.Ingredient.Type; import tacos.Taco; import tacos.TacoOrder; @Controller @RequestMapping("/design") @SessionAttributes("tacoOrder") public class DesignTacoController { private static final Logger log = LoggerFactory.getLogger(DesignTacoController.class); @ModelAttribute public void addIngredientsToModel(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)); } } @ModelAttribute(name = "tacoOrder") public TacoOrder order() { return new TacoOrder(); } @ModelAttribute(name = "taco") public Taco taco() { return new Taco(); } @GetMapping public String showDesignForm() { return "design"; } @PostMapping public String processTaco(Taco taco,@ModelAttribute TacoOrder tacoOrder) { tacoOrder.addTaco(taco); log.info("处理 taco:{}",taco); return "redirect:/orders/current"; } private Iterable<Ingredient> filterByType(List<Ingredient> ingredients,Type type){ return ingredients .stream() .filter(x -> x.getType().equals(type)) .collect(Collectors.toList()); } }表单提交时,表单输入的字段会绑定到Taco对象上,该对象以参数的形式传递给processTaco()拿到Taco对象,可以任意操作了,tacoOrder对象则是从模型中拿出来,在整个会话中都会保存。
表单中包含多个checkbox元素,名字都是ingredients,名为name的文本输入元素,表单中的这些Input框对应Taco类的ingredients和name属性。
表单中name输入只需要一个简单文本值,多个也是多个String,但taco对象中是List,文本值列表如何绑到一个Ingredient列表中呢?
转换器(converter)转换器实现了Spring的Converter接口并实现convert()方法,该方法将接收的一个值转换成另一个值。也就是将String转换为Ingredient。
将String转换为Ingredient package tacos; import java.util.HashMap; import java.util.Map; import org.springframework.core.convert.converter.Converter; import org.springframework.stereotype.Component; import tacos.Ingredient.Type; @Component public class IngredientByIdConverter implements Converter<String, Ingredient> { private Map<String, Ingredient> map = new HashMap(); public IngredientByIdConverter() { // 实际是从数据库中查出来的 map.put("FLTO", new Ingredient("FLTO", "Flour Tortilla", Type.WRAP)); map.put("COTO", new Ingredient("COTO", "Corn Tortilla", Type.WRAP)); map.put("GRBF", new Ingredient("GRBF", "Ground Beef", Type.PROTEIN)); map.put("CARN", new Ingredient("CARN", "Carnitas", Type.PROTEIN)); map.put("TMTO", new Ingredient("TMTO", "Diced Tomatoes", Type.VEGGIES)); map.put("LETC", new Ingredient("LETC", "Lettuce", Type.VEGGIES)); map.put("CHED", new Ingredient("CHED", "Cheddar", Type.CHEESE)); map.put("JACK", new Ingredient("JACK", "Monterrey Jack", Type.CHEESE)); map.put("SLSA", new Ingredient("SLSA", "Salsa", Type.SAUCE)); map.put("SRCR", new Ingredient("SRCR", "Sour Cream", Type.SAUCE)); } @Override public Ingredient convert(String id) { return map.get(id); } }这里map是自己造的,实际从数据库中查出来。
@component将当前类注册为一个Bean,请求参数与绑定属性需要转换时会用到。
有redirect:表示这是一个重定向视图,处理完逻辑后重定向到"/order/current"。
编写展现taco订单表单的控制器 package tacos; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.SessionAttributes; @Controller @RequestMapping("/orders") @SessionAttributes("tacoOrder") public class OrderController { private static final Logger log = LoggerFactory.getLogger(OrderController.class); @GetMapping("/current") public String orderForm() { return "orderForm"; } }类注解上的路径+方法上注解路径结合之后,指定orderForm方法处理"/orders/current"的http get请求。
创建orderForm.html
taco订单的表单视图 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Taco Cloud</title> <link rel="stylesheet" th:href="@{/styles.css}"> </head> <body> <form method="post" th:action="@{/orders}" th:object="${tacoOrder}"> <h1>Order your taco creations!</h1> <img alt="" th:src="@{/images/TacoCloud.png}" height="30px"> <h3>Your tacos in this order:</h3> <a th:href="@{/design}" id="another">Design another taco</a><br> <ul> <li th:each="taco:${tacoOrder.tacos}"><span th:text="${taco.name}">taco name</span></li> </ul> <h3>Deliver my taco masterpieces to...</h3> <label for="deliveryName">Name:</label> <input type="text" th:field="*{deliveryName}"> <br> <label for="deliveryStreet">Street address:</label> <input type="text" th:field="*{deliveryStreet}"> <br> <label for="deliveryCity">City:</label> <input type="text" th:field="*{deliveryCity}"> <br> <label for="deliveryState">State:</label> <input type="text" th:field="*{deliveryState}"> <br> <label for="deliveryZip">Zip code:</label> <input type="text" th:field="*{deliveryZip}"> <br> <h3>Here's how I'll pay...</h3> <label for="ccNumber">Credit Card #:</label> <input type="text" th:field="*{ccNumber}"> <br> <label for="ccExpiration">Expiration:</label> <input type="text" th:field="*{ccExpiration}"> <br> <label for="ccCVV">CVV:</label> <input type="text" th:field="*{ccCVV}"> <br> <input type="submit" value="Submit Order"> </form> </body> </html> 处理taco订单的提交,增加方法 @PostMapping public String processOrder(TacoOrder order, SessionStatus sessionStatus) { log.info("Order submitted:{}",order); sessionStatus.setComplete(); return "redirect:/"; } 2.3 校验表单输入字段合法性无法保证,可以添加一堆乱七八糟的if/then代码块逐个检查输入,但这操作会很繁琐。
幸运的是,Spring支持JavaBean检验API(JavaBean Validation API,也成JSR-303)
要在SpringMVC中应用校验,需要:
POM中添加Spring Validation starter;在要被校验的类上声明校验规则,被校验的类就是Taco类;在控制器方法中声明要进行校验;修改表单视图以展现校验错误;还记的怎么添加依赖吗,参考之前,搜Validation。
或者手动添加:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> 2.3.1 声明校验规则对于Taco类来说,想要确保name属性不能为空或null,同时希望至少一项配料被选中。
使用@NotNull和@Size注解来声明规则。
为Taco领域类添加校验,部分代码 @NotNull @Size(min = 5,message = "名称至少5个字符长") private String name; @NotNull @Size(min = 1,message = "至少选择一种配料") private List<Ingredient> ingredients;校验订单就稍微复杂点了,地址不能为空,用@NotBlank注解。
信用卡号保证值是合法的,ccExpiration必须符合MM/YY格式,ccCVV需要3位数字。
JavaBean Validation API注解部分来自Hibernate Validator的注解。
校验订单的字段 public class TacoOrder { @NotBlank(message = "地址名称不能为空") private String deliveryName; @NotBlank(message = "街道不能为空") private String deliveryStreet; @NotBlank(message = "城市不能为空") private String deliveryCity; @NotBlank(message = "状态不能为空") private String deliveryState; @NotBlank(message = "邮编不能为空") private String deliveryZip; @CreditCardNumber(message = "不是一个可用的信用卡号") private String ccNumber; @Pattern(regexp = "^(0[1-9]|1[0-2])([\\\\/])([2-9][0-9])$",message = "格式必须是MM/YY") private String ccExpiration; @Digits(integer = 3,fraction = 0,message = "不可用的CVV") private String ccCVV; } 2.3.2 在表单绑定的时候执行校验已声明Taco和TacoOrder,接下来重新修改每个控制器,让表单在POST提交至控制器方法时执行校验。
两个控制器中对应参数前增加@Validated以及增加Errors参数。
我用的是JDK17,这里并不是原书的@Valid。
2.3.3 展现校验错误Thymeleaf提供了便捷访问Errors对象的方法,借助fields以及th:errors属性。
订单字段都添加错误时的替换,暂时不想弄,略。
2.4 使用视图控制器如果一个控制器非常简单,不需要填充模型或处理输入,也就是HomeController,那么还有另外一种方式定义控制器,视图控制器,只请求转发到视图而不作其他事情的控制器。
声明视图控制器 package tacos.web; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("home"); } }关于WebConfig,它实现了WebMvcConfigurer接口,接口中定义多个方法来配置Spring MVC,我们只需覆盖所需的方法,本例中覆盖了addViewControllers方法。
此方法接收一个ViewControllerRegistry对象,我们可以使用它注册一个或多个视图控制器。指明当前请求"/"的时候要转发到home视图上。那现在可以删除HomeController了。找到第1章的HomeControllerTest类,从@WebMvcTest注解中移除对HomeController的引用,保证编译不出错。
这里我们是创建了一个WebConfig配置类存放视图控制器的声明,所有配置类都可以实现WebMvcConfigurer接口并覆盖相应方法,所以上面的方法可以在TacoCloudApplication引导类中添加,也是实现接口,重写方法,效果一样,不演示了。
每种配置(Web、数据、安全等)都可以在引导类中配置,但本人为了整洁和简单,习惯为每种配置单独创建新的配置类。
2.5 选择视图模板库视图模板库选择取决于个人喜好,Spring很灵活,能支持很多常见的模版方案。
若想使用不同的模板库,只需要在项目初始化的时候选择它,或者编辑已有的POM文件。
JSP特殊一些,需要嵌入式的Tomcat和Jetty容器,通常在"/WEB-INF"目录下找JSP文件,现在很少用了,了解即可。
缓存模版开发期间,SpringBoot的DevTools默认禁用,也就是cache=false,改动都可以刷新浏览器。
若要启用,则在application.properties添加代码,设为true或删除。
实际开发中用profile控制,后面会学。
《Spring实战》(第6版)第2章开发Web应用由讯客互联互联网栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“《Spring实战》(第6版)第2章开发Web应用”