Skip to main content

Spring in action

v6

SpringBoot版本汇总

SpringBoot3.x - Spring 6.x - Java17+

SpringBoot2.x - Spring 5.x - Java8+

  • 构建应用的基础知识
  • 与其他应用的集成
  • 反应式编程
  • 部署生产环境

SpringBoot在不断优化,越来越适应云原生的开发环境;反应式编程已经度过了理念阶段,逐渐在实际项目中落地;SpringData正在支持越来越多的数据库类型;SpringCloud在借助Netflix相关的项目成功成为微服务开发的首选方案之后,正在慢慢拜托Netflix相关项目的束缚,开始自立门户;SpringNative更是借助GraalVM的东风,已脱离JVM为噱头,成功吸引了一批流量……

我们依然要从依赖注入、面向切面编程和自动配置等特性入手,探索和掌握新技术的发展思路和实现脉络。

以Quarkus、Micronaut为代表的一些新生代开发框架在强力挑战Spring的主导地位。

关于本书的目的是让你学会使用Spring框架、Spring Boot及Spring生态系统中的其他组成部分构建令人赞叹的应用程序。

前言

Spring的基本使命是使Java应用的开发更容易。

无论是创建部署在传统应用服务器上的应用,还是创建部署在云端kubernetes集群上的容器化应用。随着Spring Boot开始提供自动配置、构建依赖辅助和运行时监控等功能,现在是称为Spring开发者的理想时机。

1 Spring起步

20年前最常见的应用形式是基于浏览器的Web应用,后端由关系型数据库作为支撑。但是我们现在感兴趣的还包括如何开发面向云的由微服务组成的应用,这些应用会将数据保存到这种类型的数据库中。另外一个崭新的关注点是反应式编程,他致力于通过非阻塞操作提供更好的扩展性并提升性能。

什么是Spring

​ 任何实际的应用程序都是由很多组件组成的,每个组件负责整个应用功能的一部分,这些组件需要与其他的应用元素协调以完成自己的任务。当应用程序运行时,需要以某种方式创建并引用这些组件。

​ Spring的核心是提供了一个容器(container)他们通常被称为Spring应用上下文,会创建和管理应用的组件。这些组件也可以被称为bean,会在Spring应用上下文中装配在一起,从而形成一个完整的程序。

​ 将bean装配在一起的行为是通过一种基于依赖租入的模式实现的。此时,组件不会再去创建他所依赖的组件并管理他们的生命周期,使用依赖注入的应用依赖于单独的实体(容器)来创建和维护所有组件,并将其注入到需要他们的bean中。通常,这是公国构造器参数和属性访问方法来实现的。

​ 在历史上,指导Spring应用上下文装配bean的方式是使用一个或多个XML文件,例如:

<bean id = "inventoryService"
class="com.example.InventoryService"/>

<bean id = "productService"
class="com.example.ProductService">
<constructor-arg ref="inventoryService"/>
</bean>

​ 最近版本基于Java的配置更为常见。与XML配置是等价的

@Configuration
public class ServiceConfiguration {
@Bean
public InventoryService inventoryService() {
return new InventoryService();
}
@Bean
public ProductService productService() {
return new ProductService(inventoryService());
}
}

​ 这个配置类的方法上使用@Bean注解进行了标注,这表明这些方法所返回的对象会以bean的形式添加到Spring的应用上下文中(默认情况下,这些bean所对应的bean ID与定义他们的方法名称是相同的)。

​ 相对于XML的配置方式,基于Java的配置有更强大的类型安全性以及更好的重构能力。只有在Spring不能自动配置组件的时候才具有必要性。

​ 自动配置起源于所谓的自动装配(autowiring)和组件扫描(component scanning)。借助组件扫描技术,Spring能够自动发现应用类路径下的组件,并创建bean。借助自动装配技术,Spring能够自动为组件注入他们所依赖的其他bean。

​ Spring Boot自动配置的能力已经远远超出了组件扫描和自动装配。能够基于类路径中的条目、环境变量和其他因素合理猜测需要配置的组件,并将他们装配在一起。没有代码就是自动装配的本质,也是他如此美妙的原因所在。

初始化Spring应用

​ 使用Spring Initializr初始化应用的方式有很多,作者推荐的是Spring Tool Suite(可以理解为定制版的eclipse),实际工作中更推荐在IntelliJ IDEA中创建新项目。这里本站也提供了初始化地址: https://start.aerion.top/

​ Spring Boot starter依赖的特别之处在于他们本身并不包含库代码,而是传递性地拉取其他库。

  • 构建文件会显著减小且更易于管理,因为这样不必为所需的每个依赖库都声明依赖。
  • 思考的点从库的名称转移到提供的功能上来。只需添加starter依赖而不必添加一堆单独的库。
  • 不必担心库版本的问题,只需要关心使用哪个版本的Spring Boot就可以了。

Spring Boot插件:

  • 提供了一个Maven goal,允许我们使用Maven来运行应用。
  • 确保依赖的所有库都包含在JAR文件中,并且能够保证运行。
  • 会在JAR文件中生成一个manifest文件,将引导类声明为可执行JAR的主类。
package top.aerion.tacocloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// SpringBoot应用
@SpringBootApplication
public class TacocloudApplication {

public static void main(String[] args) {
// 运行应用
SpringApplication.run(TacocloudApplication.class, args);
}

}

@SpringBootApplication是一个组合注解,包含下面三个注解:

  • @SpringBootConfiguration:这个注解实际上是@Configuration注解的特殊形式。
  • @EnableAutoConfiguration:启动自动配置
  • @ComponentScan:启动组件扫描。这样就能通过像@Component、@Controller、@Service这样的注解注册组件。
mvn package 
java -jar target/tacocloud-0.0.1-SNAPSHOT.jar
# 或者使用Spring Boot的Maven插件
mvn spring-boot:run

TacocloudApplicationTests类中的contextLoads方法虽然没有方法体,还是会执行必要的检查,确保Spring应用上下文成功加载。

@SpringBootTest会告诉JUnit在启动测试的时候添加上Spring Boot的功能

mvn test

编写Spring应用

​ Spring 自带了一个强大的Web框架,Spring MVC,核心是控制器(controller)的理念。控制器是处理请求并以某种方式进行信息响应的类。在面向浏览器的应用中,控制器会填充可选的数据模型并将请求传递给一个视图,以便于生成返回给浏览器的HTML。

package top.aerion.tacocloud;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

// 控制器
@Controller
public class HomeController {
@GetMapping("/") // 处理对/的请求
public String home() {
// 返回视图名
return "home";
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Taco Cloud</title>
</head>
<body>
<h1>Welcome to ...</h1>
<img th:src = "@{/images/TacoCloud.png}">
</body>
</html>
package top.aerion.tacocloud;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(HomeController.class)
public class HomeControllerTest {

@Autowired
private MockMvc mockMvc;

@Test
public void testHomePage() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(view().name("home"))
.andExpect(content().string(
containsString("Welcome to ...")
));
}
}

DevTools:

  • 代码变更后应用会自动重启。
  • 静态资源发生变化时,会自动刷新浏览器。
  • 自动禁用模板缓存。
  • 如果使用H2数据库,则内置了H2控制台。

注意DevTools不是IDE插件,使用与所有环境。当DevTools启动的时候,应用程序会加载到Java虚拟机中的两个独立的类加载器中。其中一个类加载器会加载Java代码、属性文件,以及项目的src/main路径下几乎所有的内容。另一个类加载器会加载依赖的库,这些苦不太可能经常发生变化。

​ 当检测到变更时只会重新加载包含项目代码的类加载器,并重启Spring的应用上下文,拎一个类加载器和JVM会原封不动。这个策略能减少应用启动时间。以个不足之处就是添加、变更或移除依赖的时候,为了让变更生效,需要重新启动应用。

​ DevTools会和你的应用一起,自动启动一个LiveReload服务器。

​ 内置的H2控制台:http://localhost:8080/h2-console

俯瞰Spring风景线

1.Spring核心框架

是Spring领域中一切的基础,提供了核心容器和依赖注入框架。

Spring MVC是Spring的Web框架。

对持久化的支持。

对反应式风格编程的支持,其中包括Spring WebFlux的新反应式Web框架,这个框架大量借鉴了Spring MVC。

2.Spring Boot

优势:starter依赖和自动配置。

  • Actuator洞察应用运行时内部工作状况,包括指标、线程dump信息、应用的健康状况以及应用程序可用的环境属性;
  • 灵活的环境属性规范
  • 在和新框架的测试辅助功能之上,提供了对测试的额外支持。

基于Groovy脚本的编程模型:Spring Boot CLI。使用它,可以将整个应用程序编写为Groovy脚本的集合,并通过命令行运行它们。

3.Spring Data

​ 将应用程序的数据存储库定义为见得Java接口,在定义存储和检索数据的方法时使用一种特定的命名约定即可。

​ 此外,Spring Data能够处理多种不同类型的数据库,包括关系型数据库(通过JDBC或JPA实现)、文档数据库(Mongo)、图数据库(Neo4j)等

4.Spring Security

解决了应用程序通用的安全性需求,包括身份验证、授权和API安全性。

5.Spring Integration和Spring Batch

Spring Integration解决了实时集成问题,在实时集成中,数据在可用时马上就会得到处理。相反,Spring Batch解决的则是批处理集成的问题,在此过程中,数据可以收集一段时间,直到触发器发出信号表明是时候处理批量数据了,才会得到处理。

6.Spring Cloud

使用Spring开发云原生应用程序的一组项目。

7.Spring Native

这个实验性的项目能够使用GraalVM原生镜像编译器将Spring Boot项目编译成原生可执行的文件,从而使镜像的启动速度显著加快,并且占用更小的空间。

2 开发Web应用

展现信息

img

需要构建的组件:

  • 用来定义taco配料属性的领域类
  • Spring MVC控制器类
  • 视图模板

img

定义领域

Ingredient配料

package top.aerion.tacocloud;

import lombok.Data;
@Data
public class Ingredient {

private final String id;
private final String name;
private final Type type;

public enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
// 补充:属性声明为final,是因为@Data给类添加了带参数的构造器。

lombok的魔力是在编译期发挥作用的,所以在运行期没必要用到他们。这样排除他们,最终形成的JAR或WAR文件中就不会包含他了。

<build>
<plugins>
<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>
</plugins>
</build>

Lombok依赖会在开发阶段为你提供注解例如@Data,并且会在编译期进行自动化的方法生成。还需要Lombok扩展到IDE上,否则IDE会报错。

package top.aerion.tacocloud;

import lombok.Data;

import java.util.List;

@Data
public class Taco {

private String name;

private List<Ingredient> ingredients;

}

TacoOrder领域类定义客户如何指定他们想要订购的taco并明确支付信息和投递信息。

package top.aerion.tacocloud;

import lombok.Data;

import java.util.ArrayList;
import java.util.List;

@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);
}

}

创建控制器类

控制器的主要职责是处理HTTP请求,要么将请求传递给视图以便于渲染HTML(浏览器展现),要么直接将数据写入响应体(RESTful)。

package top.aerion.tacocloud.web;

import lombok.extern.slf4j.Slf4j;
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 top.aerion.tacocloud.Ingredient;
import top.aerion.tacocloud.Ingredient.Type;
import top.aerion.tacocloud.Taco;
import top.aerion.tacocloud.TacoOrder;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@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 = 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());
}
}

@Slf4j是Lombok的注解,在编译期会自动生成一个SLF4J Logger静态属性,这与下面显式声明是一样的

private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DesignTacoController.class)

@SessionAttributes("tacoOrder")说明在这类中烧毁放到模型里面的TacoOrder对象应该在会话中一直保持。因为创建taco是创建订单的第一步,而我们创建的订单需要在会话中保存,这样可以跨多个请求。

Spring MVC请求映射注解

  • @RequestMapping通用请求处理
  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

设计视图

Spring 提供了多种定义视图的方式:

JSP、Thymeleaf、FreeMarker、Mustache和基于Groovy的模板。

添加Thymeleaf依赖之后,在运行时Spring Boot的自动配置功能会发现Thymeleaf在类路径中,因此回味Spring MVC自动创建支撑Thymeleaf视图的bean。

Thymeleaf是与web框架解耦的。但是他们可以与Servlet的request属性协作。所以,在Spring将请求转移到视图之前,它会把模型数据复制到request属性中,Thymeleaf和其他的视图模板方案就能访问到他们了。

​ Thymeleaf模板就是增加一些额外元素属性的HTML,这些属性能够指导模板如何渲染request数据。例如:

<p th:text = "${message}">placeholder message</p>
<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><br/>
</div>
<!-- 渲染后 -->
<div>
<input name = "ingredients" type = "checkbox"
value = "FLTO"/>
<span>Flour Tortilla</span><br/>
</div>

处理表单提交

验证表单提交

Spring支持JavaBean校验API(JavaBean Validation API,也称JSR-303)

从Spring Boot2.3.0开始需要手动添加依赖,在此之前web starter自动包含。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

声明验证规则:

package tacos;
import java.util.List;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;

@Data
public class Taco {

@NotNull
@Size(min=5, message="Name must be at least 5 characters long")
private String name;

@NotNull
@Size(min=1, message="You must choose at least 1 ingredient")
private List<Ingredient> ingredients;
}
package tacos;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.CreditCardNumber;
import java.util.List;
import java.util.ArrayList;
import lombok.Data;

@Data
public class TacoOrder {

@NotBlank(message="Delivery name is required")
private String deliveryName;

@NotBlank(message="Street is required")
private String deliveryStreet;

@NotBlank(message="City is required")
private String deliveryCity;

@NotBlank(message="State is required")
private String deliveryState;

@NotBlank(message="Zip code is required")
private String deliveryZip;

@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;

private List<Taco> tacos = new ArrayList<>();

public void addTaco(Taco taco) {
this.tacos.add(taco);
}
}

验证:

import javax.validation.Valid;
import org.springframework.validation.Errors;

...

@PostMapping
public String processTaco(@Valid @ModelAttribute("taco") Taco taco, Errors errors) {
if (errors.hasErrors()) {
return "design";
}
// Save the taco...
// We'll do this in chapter 3
log.info("Processing taco: " + taco);

return "redirect:/orders/current";
}

但是,如果一个控制器足够简单,不填充模型或流程输入(就像 HomeController 一样),那么还有另一种定义控制器的方法。视图控制器。

任何配置类都可以实现 WebMvcConfigurer 并覆盖 addViewController() 方法

3 处理数据

几十年来,关系数据库和 SQL 一直是数据持久化的首选。尽管近年来出现了许多替代数据库类型,但关系数据库仍然是通用数据存储的首选,而且不太可能很快被取代。

在处理关系数据时,Java 开发人员有多个选择。两个最常见的选择是 JDBC 和 JPA。Spring 通过抽象支持这两种方式,这使得使用 JDBC 或 JPA 比不使用 Spring 更容易。

不使用 JdbcTemplate 查询数据库

@Override
public Optional<Ingredient> findById(String id) {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = dataSource.getConnection();
statement = connection.prepareStatement(
"select id, name, type from Ingredient");
statement.setString(1, id);
resultSet = statement.executeQuery();
Ingredient ingredient = null;
if(resultSet.next()) {
ingredient = new Ingredient(
resultSet.getString("id"),
resultSet.getString("name"),
Ingredient.Type.valueOf(resultSet.getString("type")));
}
return Optional.of(ingredient);
} catch (SQLException e) {
// ??? What should be done here ???
} finally {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {}
}
}
return null;
}