本文为书籍《Spring实战(第五版)》的摘抄笔记,推荐大家购买实体书籍学习。
什么是Spring
任何实际的应用都是由许多组件组成的,每个组件负责整个应用功能的一部分,这些组件需要与其它的应用元素进行协调,以完成自己的任务。当应用程序运行时,需要以某种方式创建并引入这些组件。
Spring的核心是提供了一个容器(container),通常称为Spring应用上下文(Spring application context),它们会创建和管理应用组件。这些组件也可以称为bean,会在Spring应用上下文中装配在一起,从而形成一个完整的应用程序。
将bean装配在一起的行为是通过一种基于依赖注入(dependency injection,DI)的模式实现的。此时,组件不会再创建它所依赖的组件并管理他们的生命周期。使用依赖注入的应用程序依赖于单体的实体(容器)来创建和维护所有组件,并将其注入到需要的bean中,通常这是通过构造器参数 和属性访问方法来实现的。
举例来说,假设在应用的众多组件中,有两个是我们需要处理的:库存服务(用来获取库存水平)和商品服务(用来提供基本的商品信息)。商品服务需要依赖于库存服务,这样他才能提供完整的商品信息。下图阐述这些bean和应用上下文之间的关系。
在核心容器之上,Spring及其一系列的相关库提供了Web框架、各种持久化可选方案、安全框架与其他系统集成、运行时监控、微服务支持,反应式编程以及众多现代应用开发所需要的特性。
在历史上,指导Spring应用上下文将bean装配在一起的方式是使用一个或者多个XML文件(描述各个组件以及它们与其他组件的关联关系)。例如,如下的XML描述了两个bean,也就是InventoryService bean
和ProductService bean
,并且通过构造器参数将InventoryService
装配到了ProductService
中:
<bean id="inventoryService" class="com.excmple.InventoryService">
</bean>
<bean id="ProcdcutService" class="com.excmple.ProductService">
<constructor-arg ref="inventoryService"/>
</bean>
但是在最近的Spring版本中,基于Java的配置更为常见,如下基于Java的配置类是与XML配置等价的:
@Configuration
public class ServiceConfiguration {
@Bean
public InventoryService inventoryService(){
return new InventoryService();
}
@Bean
public ProductService productService(){
return new productService();
}
}
@Configuration注解会告诉这是一个配置类,会为Spring应用上下文提供bean。这个配置类的方法使用@bean注解进行了标注,表明这些方法所返回的对象会以bean的形式添加到Spring的应用上下文中(默认情况下,这些所对应的beanID与定义他们的方法名称是相同的)。
相对于基于XML的配置方式,基于Java的配置会带来多项额外的收益,包括更强的类型安全性以及更好的重构能力。即便如此,不管是使用Java还是使用XML的显示配置,只有当Spring不能进行自动配置的时候才是必要的。
在Spring技术中,自动配置起源于所谓的自动装配(autowiring)和组件扫描(component scanning),借助组件扫描技术,Spring能够自动发现应用类路径下的组件,并将他们创建成Spring应用上下文中的bean,并借助自动装配技术,Spring能够自动为组件注入他们所依赖的其他bean。
最近,随着Spring Boot的引入,自动配置的能力已经远远超出了组件扫描和自动装配。Spring Boot是Spring框架的扩展,提供了很多增强生产效率的方法,最为大家所熟知的增强方法就是自动配置(autoconfiguration),Spring Boot能够基于类路径中的条目、环境变量和其他因素合理猜测需要配置的组件并将它们装配在一起。
初始化Spring应用
在书本中,我们将会创建一个名为Taco Cloud的在线应用,它能够订购人类所发明的一种美味,也就是墨西哥煎玉米卷(taco)。当然,在这个过程中,为了达成我们的目标,我们将会用到Spring,SpringBoot以及各种相关的库和框架。
我们有很多种初始化Spring应用的可选方案。书中推荐的是使用Spring Initializr初始化应用。
Spring Initializr是一个基于浏览器的Web应用,同时也是一个REST API,能够生成一个Spring项目的骨架。
书中使用的是使用Spring Tool Suite的工具,可以在如Eclipse上使用,我由于使用的是Idea,Idea自带的新建中就有Spring Initializr选项,所以我都是使用Idea来创建的。
使用IDEA 初始化Spring项目
如果要使用IDEA初始化Spring项目,就在File>New菜单下选择Project菜单项。
需要注意:使用Spring Initializr
本质都是从一个web应用下载的初始化模板,如果IDE构建时出现网络问题,可以自己去start.spring.io下载。
最后设置项目在本地路径即可。
检查Spring项目的结构
项目加载到IDE之后,我们将其展开。
这是一个典型的Maven或Gradle项目结构,其中源代码放在了“src/main/java”中,测试代码放到了“src/test/java”中,而非Java的资源放到了“src/main/resources”。在这个项目结构中,我们需要注意一下几点。
- mvnw和mvnw.cmd:这是Maven包装器(wrapper)脚本。借助这些脚本,即便你的机器上没有安装Maven,也可以构建项目。
- pom.xml:这是Maven构建规范,随后我们将深入介绍该文件。
- TacoCloudApplication.java:这是Spring Boot主类,它会启动该项目。随后,我们会详细介绍这个类。
- application.properties:这个文件起初是空的,但是他们为我们提供了指定配置属性的地方。在本章中,我们会稍微修改一下这个文件,但是我会将配置属性的详细阐述放到第五章。
- static:在这个文件夹下,你可以存放任意为浏览器提供服务的静态内容(图片、样式表、JavaScript等),该文件夹初始为空。
- templates:这个文件夹中存放用来渲染页面到浏览器的模板文件,这个文件夹初始是空的。不过我们很快就会往里添加Thymeleaf模板。
- TacoCloudApplicationTests.java:这是一个简单的测试类,他能确保Spring应用上下文可以成功加载。在开发应用的过程中,我们会将更多的测试添加进来。
现在我们来看看Spring Initializr提供的几个条目。
探索构建规范
在填充Initializr表单的时候,我们声明项目要使用Maven来进行构建。因此,Spring Initializr所生成的pom.xml文件已经包含了我们所选择的依赖。
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version><!--Spring Boot的版本-->
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.brunocheng</groupId>
<artifactId>taco-cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging><!--默认没有packageing标签,也意味着默认的打包方式为JAR-->
<name>taco-cloud</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency><!--Starter依赖-->
<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.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin><!--Spring Boot插件-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在pom.xml文件中,我们第一个需要注意的地方就是<packaging>
。我们选择了将应用构建成一个可执行的JAR文件,而不是WAR文件,这可能是你所做出的最奇怪的选择之一,对于Web应用来说尤为如此。毕竟,传统的应用都是打包成WAR文件,JAR只是用来打包库和较少的桌面UI应用的。
打包为JAR文件是基于云思维做出的选择,尽管WAR文件非常适合部署到传统的Java应用服务器上,但对于大多数云平台来说,他们并不是理想的选择。有些云平台也能够部署和运行WAR文件,但是所有的云平台都能够运行可执行的JAR文件。因此,默认会使用基于JAR的打包方式,除非我们明确告诉它采用其他的方式。
如果你想将应用部署到传统的Java应用服务器上,那么需要选择基于WAR的打包方式并要包含一个Web初始化类。第二章会有详细的介绍如何创建WAR文件。
接下来,请留意<parent>
元素,更具体来说是它的<version>
子元素,这表明我们的项目要以spring-boot-starter-parent
作为其父POM。除了其他的一些功能之外,这个父POM为Spring项目常用的一些库提供了依赖管理,现在你不需要指定他们的版本,因为这是通过父POM来管理的。这里的2.4.3
表明要使用Spring Boot 2.4.3
,所以会根据这个版本的Spring Boot来定义继承依赖管理。
你可能也会注意到这三个依赖的artifactId上都有starter这个单词。Spring Boot依赖的特别之处在于它们本身并不包含库代码,而是传递性的拉取其他库。这种依赖主要有三个好处:
- 构建文件会显著减小并且更易于管理,因为这样不必为每个所需的依赖库都声明依赖。
- 我们能够根据他所提供的功能来思考依赖,而不是根据库的名称,如果是开发Web应用,那么你只需要添加
web starter
就可以了,而不必添加一堆单独的库再编写Web应用。 - 我们不必再担心库版本的问题,你可以直接相信给定版本的SpringBoot,传递性引入的库的版本是兼容的。现在,你只需要关心使用的是哪一个版本的Spring Boot就可以了。
到最后,构建规范还包含一个Spring Boot插件。这个插件提供了一些重要的功能。
- 它提供了一个Maven goal,允许我们使用Maven来运行应用。后面会尝试使用这个goal。
- 它会确保依赖的所有库都会包含在可执行JAR文件中,并且能够保证他们在运行时类路径下是可用的。
- 它会在JAR中生成一个mainifest文件,将引导类(在我们的场景中,也就是
TacoCloudApplication
)声明为可执行JAR的主类。
引导应用
因为我们将会通过可执行JAR文件的形式来运行应用,所以很重要的一点就是要有一个主类,它将会在JAR运行的时候被执行。我们同时还需要一个最小化的Spring配置,以引导该应用。这就是TacoCloudApplication
类所做的事情。
package com.brunocheng.tacocloud.controller;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
//@SpringBootApplication表面这是一个SpringBoot的应用,它是一个组合注解
public class TacoCloudApplication {
public static void main(String[] args) {
SpringApplication.run(TacoCloudApplication.class, args);//运行应用
}
}
尽管在TacoCloudApplication
中只用很少的代码,但是它包含了很多的内容。其中,最强大的一行代码也是最短的。@SpringBootApplication
注解明确表明这是一个Spring Boot应用。但是。@SpringBootApplication
远比看上去更强大。
@SpringBootApplication
是一个组合注解,它组合了三个其他的注解。
@SpringBootConfiguration
:将该类声明为配置类。尽管这个类目前还没有太多的配置,但是后续我们可以按需添加基于Java的Spring框架配置。这个注解实际上是@Configuration
注解的特殊形式。@EnableAutoConfiguration
:启用Spring Boot的自动配置。我们随后会介绍自动配置的更多功能。就现在来说,我们只需要知道这个注解会告诉Spring Boot自动配置它认为我们会用到的组件。@ComponentScan
:启动组件扫描。这样我们能够通过像@Component
、@Controller
、@Service
这样的注解声明其他类,Spring会自动发现它们并将它们注册为Spring应用上下文中的组件。
TacoCloudApplication 另外一个很重要的地方是它的main()
方法。这是JAR文件执行的时候要运行的方法。在大多数情况下,这个方法都是样板代码,我们编写的每个Spring Boot应用都会有一个类似或完全相同的方法(类名不同则另当别论)。
这个main()
方法会调用SpringApplication中静态的run()
方法,后者会真正执行应用的引导过程,也就是创建Spring的应用上下文。在传递给run()
的两个参数中,一个是配置类,另一个是命令行参数。尽管传递给run()的配置类不一定要和引导类相同,但这是最便利和最典型的做法。
测试应用
测试时软件开发的重要组成部分。鉴于此,Spring Initializr为我们提供了一个测试类作为起步。
package com.brunocheng.tacocloud;
import org.junit.Test;//注意导入的包,还有一个类似@Test注解
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)//使用Spring的运行器、
/*
* @RunWith是Junit的注解,它会提供一个测试运行器来指导Junit如何运行,可以想象为Junit的一个插件。
* SpringRunnner是SpringJunit4ClassRunner的别名
* */
@SpringBootTest
/*
* @SpringBootTest会告诉Junit在测试的时候添加上Spring Boot功能。
* */
public class TacoCloudApplicationTests {
@Test
//测试方法
public void contextLoads() {
}
}
TacoCloudApplicationTests
类中的内容不多:这个类中只有一个空的测试方法。即便如此,这个测试类还是会执行必要的检查,确保Spring应用上下能够成功加载。如果你所做的变更导致Spring应用上下文无法创建,那么这个测试将会失败,你就可以做出反应来解决相关问题了。
另外,注意这个类带有@RunWith(SpringRunner.class)注解。@RunWith是JUnit的注解,它会提供一个测试运行器(runner)来指导JUnit如何运行测试。可以将其想象为个JUnit应用一个插件,以提供自定义的测试行为。在本例中,为JUnit提供的是SpringRunner,这是一个Spring提供的测试运行器,它会创建运行所需的Spring应用上下文。
编写Spring应用
处理Web请求
Spring自带了一个强大的Web框架,名为SpringMVC。SpringMVC的核心是控制器(controller)的理念。控制器是处理请求并以某种方式进行信息响应的类。在面向浏览器的应用中,控制器会填充可选的数据模型并将请求传递给一个视图,以便于生成返回给浏览器的HTML。
在第二章中,我们将会学习更多关于Spring MVC的知识。现在,我们会编写一个简答的控制器类以处理对根路径(比如,“/”)的请求。并将这些请求转发至主页视图,在这个过程中不会填充任何的模型数据。
package com.brunocheng.tacocloud.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller//控制器
public class HomeController {
@GetMapping("/")//处理对根路径“/”的请求
//等同于@RequestMapping(method = RequestMethod.GET)
public String home() {
return "home";//返回视图名
}
}
可以看到,这个类带有@Controller
。就其本身而言,@Controller
并没有做太多的事情。它的主要目的是让组件扫描功能会自动发送它,并创建一个HomeController实例作为Spring应用上下文的bean。
home()
是一个简单的控制器方法。它带有@GetMapping
注解,表明如果针对“/”发送HTTP.GET请求,那么这个方法将会处理请求。该方法所做的只是返回String类型的home值。
这个值将会被解析为视图的逻辑名。视图如何实现取决于多个因素,但因为Thymeleaf位于类路径中,所以我们可以使用Thymeleaf来定义模板。
模板名称是由逻辑名派生而来的,在加上“/template”前缀和“.html”后缀。最终形成的模板路径将是“/template/home.html”。所以,我们需要将模板放到项目的‘“src\main\resources\templates\home.html”目录中。现在,就让我们来创建这个模板。
定义视图
为了让主页尽可能简单,除了欢迎用户访问站点之外,它不会做其他的任何事情。下面的代码展现了Thymeleaf模板,它定义了Taco Cloud的主页。
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset=" UTF-8 />">
<title>Taco Cloud</title>
</head>
<body>
<h1>Welcome to...</h1>
<img th:src="@{/images/good.jpg}">
</body>
</html>
这个模板没有太多需要哦讨论的。唯一需要注意的一行代码是用于展现图片的<img>
标签。它使用了Thymeleaf的th:src属性和@{…}表达式,以便于引用相对于上下文路径的图片。除此之外,他就是一个Hello World页面。
启动TacoCloudApplication即可访问。
测试控制器
在测试Web应用时,对HTML页面的内容进行断言是比较困难的。幸好Spring对测试提供了强大的支持,这使得测试Web应用变得非常简单。
对主页来说,我们所编写的测试在复杂性上与主页本身差不多。测试需要针对根路径“/”发送一个HTTP GET请求并期望得到成功结果,其中视图名称为home并且结果内容包含“Welcome to…”。
package com.brunocheng.tacocloud.controller;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.containsString;
@RunWith(SpringRunner.class)
@WebMvcTest(HomeController.class)
/*
* @WebMvcTest注解是Spring Boot提供的一个特殊测试注解,它会让这个测试在SpringMVC应用到上下文中。
* */
public class HomeControllerTest {
@Autowired
private MockMvc mockMvc;
//MockMvc是模拟请求
@Test
public void testHome() throws Exception {
//模拟发送一起请求
mockMvc.perform(MockMvcRequestBuilders.get("/"))//对/路径(根路径)发送HTTP.GET请求
.andExpect(status().isOk())//响应状态为200
.andExpect(view().name("home"))//视图逻辑名称为homw
.andExpect(content().string(containsString("Welcome to...")));//视图包含文本
}
}
对于这个测试,我们首先注意到可能就是它使用了与TacoCloudApplicationTests类不同的注解。HomeControllerTest 没有使用@SpringBootTest
标记,而添加了@WebMvcTest
注解。这是Spring Boot所提供的一个特殊测试注解,它会让这个测试在Spring MVC应用的上下文中执行。更具体来说,在本例中,它会将HomeController注册到Spring MVC中,这样的话,我们测试可以向它发送请求了。
@WebMvcTest
同样会为了测试Spring MVC应用提供Spring环境的支持。尽管我们可以启动一个服务器来进行测试。但是对于我们的场景来说,仿造一下Spring MVC的运行机制就可以。测试类被注入了一个MockMvc,能够让测试实现mockup。
通过testHomepage()方法,我们定义了针对主页想要执行的测试。它首先使用MockMvc对象对“/”(根路径)发起HTTP GET请求。对于这个请求,我们设置了如下预期:
- 响应应该具备HTTP 200(OK)状态;
- 视图的逻辑名称应该是home;
- 渲染后的视图应该包含文本“Welcome to…”。
如果在MockMvc对象发送请求之后,这些期望有不满足的话,那么这个测试会失败。但是,我们控制器和模板引擎在编写是满足了这个预期,所以测试应该能够过,并且带有绿色的背景。