SpringBoot从入门到精通系列一:SpringBoot入门
创建第一个SpringBoot应用:
一、SpringBoot简介
Springboot是Java企业开发里最流行的框架,它为各种第三方框架的快速整合提供了自动配置,一旦用上了SpringBoot,开发者只需专注于应用中业务逻辑功能的实现。
二、Java EE应用与Spring
Spring是Java领域中应用最广的框架,从本质上说:
- Spring只是一个组件容器,负责创建并管理容器中的组件(称为Bean),并管理组件之间的依赖关系。
- 由于Spring将容器功能做到了极致,Java EE应用所涉及的以下各种组件,都处于Spring容器的管理之下:
- 前端控制器组件
- 安全组件
- 业务逻辑组件
- 消息组件
- DAO组件(Spring也称其为Repository)
- 连接数据库的基础组件(如DataSource、ConnectionFactory、SessionFactory等)
对于其他各种功能型的框架,它们都需要一个容器来承载其运行,而Spring正是这个不可替代的容器。
关于Java领域中的功能型框架,各方面的功能都存在不少框架可供选择:
- 前端:Spring WebFlux、Spring MVC、Struts2
- 安全领域:Spring Security、Shiro等
- 消息组件:ActiveMQ、RabbitMQ、Kafka等
- 缓存:JCache、EhCache、Hazelcast、Redis等
- 持久层框架:JPA、MyBatis、jOOQ、R2DBC
- 分布式:ZooKeeper、Dubbo等
- NoSQL存储:Redis、MongoDB、Neo4j、Cassandra、Geodo等
- 搜索引擎:Lucene、Solr、Elasticsearch等
- 数据库存储:MySQL、PostgreSQL、Oracle等
- Web服务器:Tomcat、Jetty、Undertow等
作为容器的Spring,是无可替代的,上面列出的这些框架,它们都需要与Spring进行整合,这就是Spring的魅力
传统Spring使用XML配置或注解来管理这些组件,因此搭建一个Java EE应用往往需要进行大量的配置和注解。这些配置工作都属于项目的基础搭建,与业务功能无关,这些工作对于初、中级开发者往往难度不小,很容易出错,在这种背景下,Spring推出了SpringBoot。
三、SpringBoot优势
Spring框架唯一的缺点是配置过多,搭建项目时需要进行大量的配置,而SpringBoot的出现,就是为了解决这个问题。
SpringBoot为绝大部分第三方框架的快速整合提供了自动配置,SpringBoot使用约定优先于配置的理念,针对企业应用开发各种场景提供了对应的Starter,开发者只要将该Starter添加到项目的类加载路径中,该Starter即可完成第三方框架的整合。
总体来说:SpringBoot具有以下特性:
- 内嵌Tomcat、Jetty或Undertow服务器,因此SpringBoot应用无需被部署到其他服务器中。
- SpringBoot应用可被做成独立的Java应用程序。
- 尽可能地自动配置Spring及第三方框架。
- 完全没有代码生成,也不需要XML配置
- 提供产品级监控功能,如运行状况检查和外部化配置等
SpringBoot主要功能就是:
- 为Spring及第三方框架的快速启动提供自动配置
- 当Spring及第三方框架整合起来之后,SpringBoot的责任也就完成了
- 在实际开发中发挥功能的依然是前面列出的那些框架和技术
四、SpringBoot应用打包类型
由于SpringBoot内嵌了Tomcat、Jetty或Undertow服务器,因此SpringBoot应用通常不需要被部署到Web服务器中,选择打包成JAR即可。
只有在极个别的情况下,不想使用SpringBoot内嵌服务器,才会考虑将它打包成WAR包(Web应用包),部署到独立的Web服务器中
五、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>
<!-- 指定继承spring-boot-starter-parent POM文件 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/>
</parent>
<!-- 定义基本的项目信息 -->
<groupId>org.crazyit</groupId>
<artifactId>firstboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>firstboot</name>
<description>First SpringBoot</description>
<properties>
<!-- 定义所使用的Java版本和源代码所用的字符集 -->
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Spring Web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Thymeleaf依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 添加Bootstrap WarJar的依赖 -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.5.3</version>
</dependency>
<!-- Spring Boot Data JPA依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 定义Spring Boot Maven插件,可用于运行Spring Boot应用 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
六、编写控制器
SpringBoot的功能只是为整合提供自动配置,因此SpringBoot应用的控制器依然是Spring MVC、Spring WebFlux或Struts 2的控制器,具体定义哪种控制器取决于项目技术栈的前端框架,本例采用Spring MVC作为前端框架,因此这里定义一个Spring MVC的前端控制器类。
@Controller
public class BookController
{
@GetMapping("/")
public String index(Model model)
{
model.addAttribute("tip","欢迎访问第一个SpringBoot应用");
return "hello";
}
@GetMapping("/rest")
@ResponseBody
public ResponseEntity restIndex()
{
return new ResponseEntity<>("欢迎访问第一个springboot应用",null,HttpStatus.OK);
}
}
上面的BookController就是一个再普通不过的Spring MVC的控制器,@Controller、@GetMapping、@ResponseBody注解都是最基本的SpringMVC注解。
- @Controller:用于修饰类,指定该类的实例作为控制器组件
- @GetMapping:用于修饰方法,指定该方法所能处理的Get请求的地址
- @ResponseBody:用于修饰方法,指定该方法生成Restful响应
上面的控制器类中定义了两个处理方法,其中第一个index()方法返回的"hello"字符串只是一个逻辑视图名,因此它还需要物理视图资源。
SpringBoot推荐使用Thymeleaf作为视图模版技术,同时可以使用BootStrap UI库美化界面。
使用前后端分离架构的应用,SpringBoot应用根本不需要生成视图响应,自然也就不需要任何视图模版技术了,在前后端分离架构的应用中,SpringBoot只需要对外提供Restful响应,前端应用则通过Restful接口与后端通信,前端应用负责生成界面,与用户交互。
七、定义视图页面
为BookController的第一个处理方法返回的"hello"逻辑视图名定义Thymeleaf视图页面hello.html
SpringBoot默认要求将Thymeleaf视图页面放在resource\templates\目录下,因此需要将hello.html的视图放在该目录下。由于该页面是为hello逻辑视图名提供视图,因此该页面的文件名为"hello.html".
Thymeleaf视图页面的语法核心设计就是:以“th:”开头的属性来处理表达式的值,比如th:text属性的作用就是用目标HTML元素来显示表达式的值。
八、运行应用
package com.bigdata.boot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MainApplication{
public static void main(String[] args) {
//创建Spring容器,运行SpringBoot应用
SpringApplication.run(MainApplication.class,args);
}
}
调用SpringApplication类的run()方法来创建Spring容器,运行SpringBoot应用。
SpringBoot是基于Spring框架的,而Spring框架最重要的核心就是Spring容器,对于Spring来说,万物都是Bean,而容器就是所有Bean所在的天地,负责管理所有Bean的生死。一个Spring容器就是天地万物,因此所有Spring框架的应用的第一步都是创建Spring容器。
SpringApplication类中run()方法的返回值就是ConfigurableApplicationContext,这就是Spring容器,可见run()方法将会创建并返回Spring容器
有了Spring容器之后,容器中的Bean来自任意用@Configuration注解修饰的Java类(Java配置类,相当于传统的XML配置文件),Spring容器会加载该配置类并创建该配置类中的所有Bean,并且会扫描该配置类相同包或其子包下的所有Bean。
run()方法的第一个参数是MainApplication类,该类应该是带@Configuration注解修饰的配置类。查看@SpringBootConfiguration源码,发现@SpringBootConfiguration就是@Configuration源码。由此可见,@SpringBootApplication注解相当于3个注解的组合版。
- @Configuration:该注解修饰的类将作为Java配置类
- @EnableAutoConfiguration:启用自动配置
- @ComponentScan:指定零配置时扫描哪些包及其子包下的Bean
可见SpringBootConfiguration注解只是一个快捷方式,同时启用了3个注解,从而完成了3个功能:
- 将被修饰的类变成Java配置类
- 启用自动配置
- 定义了Spring容器扫描Bean类的包及其子包
所谓的自动配置,只是SpringBoot提供了预配置。由于SpringBoot提供的预配置,因此开发者无须过多配置即可把项目搭建起来。被@SpringBootApplication修饰的类位于org.crazyit.firstboot包下,因此Spring容器会自动扫描并处理该包及其子包下的所有配置类(@Configuration注解修饰的类)和组件类(@Component、@Controller、@Service、@Repository等注解修饰的类)。
- @Configuration、@Controller、@Service、@Repository等注解的本质都是@Component,Spring容器会扫描@Component修饰的类,将它变成容器中的Bean。
由于前面定义的控制器类BookController位于org.crazyit.firstboot.controller包下,且使用了@Controller修饰,因此Spring容器就能将它加载成容器中的Bean.
九、创建可执行的JAR包
由于SpringBoot应用内嵌了Web服务器(Tomcat、Jetty),所以无须将SpringBoot应用部署到其他Web服务器中,SpringBoot应用完全可以独立运行。
在发布SpringBoot应用时,只需要将该应用打包成一个可执行的JAR包,以后就可以使用该JAR包来运行SpringBoot应用了。
为了将SpringBoot应用打包成JAR包,需要保证在pom.xml文件中添加了Spring Boot Maven插件,也就是其中包含如下配置:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<excludes>
<exclude> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
只要执行如下两条命令即可生成可执行的JAR包:
mvn clean
mvn package
- mvn clean:用于清除所有在构建过程中生成的文件。
- mvn package:指定执行到Maven生命周期的package阶段,用于生成可执行的JAR包
十、执行JAR包启动SpringBoot应用
java -jar firstboot-0.0.1-SNAPSHOT.jar
十一、Maven生命周期
mvn package命令会从默认生命周期的第一阶段一直执行到package阶段,Maven的默认生命周期包含compile(编译项目)->test(单元测试)->package(项目打包)->install(安装到本地仓库)->deploy(部署到远程)这几个核心阶段
十二、开发业务组件
前面控制器的处理方法直接返回了字符串作为响应,这在实际项目中肯定是不行的,实际项目中的控制器要调用业务组件来处理用户响应,因此需要开发一个业务组件来处理用户请求。
业务组件要实现添加图书、删除图书、列出全部图书这三个功能。下面是本例业务组件的接口代码。
\service\BookService.java
public interface BookService
{
List<Book> getAllBooks();
Integer addBook(Book book);
void deleteBook(Integer id);
}
该Service组件的实现类则调用DAO组件的方法来实现上述方法。下面是BookService组件的实现类代码。
\service\impl\BookServiceImpl.java
@Service
@Transactional(propagation=Propagation.REQUIRED,timeout=5)
public class BookServiceImp1 implements BookService
{
//依赖注入容器中的BookDao组件
@Autowired
private BookDao bookDao;
@Override
public List<Book> getAllBooks()
{
return (List<Book>) bookDao.findAll();
}
@Override
public Integer addBook(Book book)
{
book.save(book);
return book.getId();
}
@Override
public void deleteBook(Integer id)
{
bookDao.deleteById(id);
}
}
上面第一行粗体字代码使用了@Service注解修饰该实现类,且该类位于org.crazyit.firstboot.service.impl包下,也就是位于FirstbootApplication类所在包的子包下,因此SpringBoot会自动扫描该实现类,并将它配置成容器中的Bean。
上面第二行粗体字代码使用了@Transaction注解修饰该Service组件,该注解指定事务传播规则为REQUIRED,事物超时时长为5秒,Spring将会为该Service组件生成事物代理,从而为该Service组件中的每个方法都添加事务。为目标组件生成事务代理是Spring AOP的功能,但生成事务代码所需要的事务管理器,同样由SpringBoot自动配置提供。
上面第三行粗体字代码使用了@Autowired注解修饰符BookDao实例变量,这也是Spring的基本用法,Spring将会把容器中唯一的、类型为BookDao的Bean注入该实例变量。
下面修改后的BookController类的代码
\controller\BookController.java
@Controller
public class BookController
{
@GetMapping("/")
public String index(Model model)
{
model.addAttribute("tip","欢迎访问第一个SpringBoot应用");
return "hello";
}
@GetMapping("/rest")
@ResponseBody
public ResponseEntity restIndex()
{
return new ResponseEntity<>("欢迎访问第一个springboot应用",null,HttpStatus.OK);
}
@Autowired
private BookService bookService;
@PostMapping("/addBook")
public String addBook(Book book,Model model)
{
bookService.addBook(book);
return "redirect:listBooks";
}
@PostMapping("/rest/books")
@ResponseBody
public ResponseEntity<Map<String,String>> restAddBook(@RequestBody Book book)
{
bookService.addBook(book);
return new ResponseEntity<>(Map.of("tip","添加成功"),null,HttpStatus.Ok);
}
@GetMapping("/listBooks")
public String list(Model model){
model.addAttribute("books",bookService.getAllBooks());
return "list";
}
@GetMapping("/rest/books")
@ResponseBody
public ResponseEntity<List<Book>> restList()
{
return new ResponseEntity<>(bookService.getAllBooks(),null,HttpStatus.OK);
}
@GetMapping("/deleteBook")
public String delete(Integer id)
{
bookService.deleteBook(id);
return "redirect:listBooks";
}
@DeleteMapping("/rest/books/{id}")
@ResponseBody
public ResponseEntity<Map<String,String>> restDelete(@PathVariable Integer id)
{
bookService.deleteBook(id);
return new ResponseEntity<>(Map.of("tip","删除成功"),null,HttpStatus.OK);
}
}
上面的BookController类增加了一个BookService实例变量,且使用了@Autowired注解修饰,因此Spring会将容器中唯一的、类型为BookService的Bean注入该实例变量。
接下来该BookController定义了6个处理方法,这些处理方法使用了@GetMapping、@PostMapping、@DeleteMapping等注解修饰,映射这些处理方法能处理来自不同URL地址的请求。这些注解都属于Spring MVC的基本注解,并不属于SpringBoot。
上面这些方法定义了两个版本,带界面响应的版本和Restful版本,使用@ResponseBody修饰的方法就是用于生成Restful响应的方法。
对于RESTful响应的处理方法,Spring Boot应用无须提供视图页面,在前后端分离的架构中,前端应用会负责提供用户界面、处理用户交互。SpringBoot应用主要暴露RESTful接口即可。
对于要生成界面的处理方法,程序还需要为它们提供视图页面。首先在前面的hello.html页面中增加一个表单,该表单供用户填写图书信息,修改后的hello.html页面代码如下。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>第一个Spring Boot应用</title>
<!-- 引用WarJar中的静态资源-->
<link rel="stylesheet" th:href="@{/webjars/bootstrap/4.5.3/css/bootstrap.min.css}"/>
<script type="text/javascript" th:src="@{/webjars/jquery/3.5.1/jquery.js}"></script>
</head>
<body>
<div class="container">
<!-- 使用th:text将表达式的值绑定到标准HTML元素 -->
<div class="alert alert-primary" th:text="${tip}"></div>
<h2>添加图书</h2>
<form method="post" th:action="@{/addBook}">
<div class="form-group row">
<label for="title" class="col-sm-3 col-form-label">图书名:</label>
<div class="col-sm-9">
<input type="text" id="title" name="title"
class="form-control" placeholder="输入图书名">
</div>
</div>
<div class="form-group row">
<label for="author" class="col-sm-3 col-form-label">作者:</label>
<div class="col-sm-9">
<input type="text" id="author" name="author"
class="form-control" placeholder="输入作者">
</div>
</div>
<div class="form-group row">
<label for="price" class="col-sm-3 col-form-label">价格:</label>
<div class="col-sm-9">
<input type="number" step="0.1" id="price" name="price"
class="form-control" placeholder="输入价格">
</div>
</div>
<div class="form-group row">
<div class="col-sm-6 text-right">
<button type="submit" class="btn btn-primary">添加</button>
</div>
<div class="col-sm-6">
<button type="reset" class="btn btn-danger">重设</button>
</div>
</div>
</form>
</div>
</body>
</html>
上面页面添加了一个Bootstrap样式的表单,该表单的界面看起来会比较美观,该表单的提交地址是addBook,与前面BookController中处理方法定义的处理地址对应。
还需要一个list.html页面用于显示所有图书,该页面代码如下:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>所有图书</title>
<!-- 引用WarJar中的静态资源-->
<link rel="stylesheet" th:href="@{/webjars/bootstrap/4.5.3/css/bootstrap.min.css}"/>
<script type="text/javascript" th:src="@{/webjars/jquery/3.5.1/jquery.js}"></script>
</head>
<body>
<div class="container">
<h2>全部图书</h2>
<table class="table table-hover">
<tr>
<th>书名</th>
<th>作者</th>
<th>价格</th>
<th>操作</th>
</tr>
<tr th:each="book : ${books}">
<td th:text="${book.title}">书名</td>
<td th:text="${book.author}">作者</td>
<td th:text="${book.price}">0</td>
<td><a th:href="@{/deleteBook?id=} + ${book.id}">删除</a></td>
</tr>
</table>
<div class="text-right"><a class="btn btn-primary"
th:href="@{/}">添加图书</a></div>
</div>
</body>
</html>
十三、开发DAO组件
前面BookService中用到了BookDao组件和Boolk类,这些都是与持久化相关的类,本例直接使用SpringBoot Data JPA来访问数据库,为此首先要为项目添加如下依赖:
- SpringBoot Data JPA依赖
- MySQL数据库驱动依赖
增加下面两个依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jps</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
添加了上面依赖后,重新加载项目依赖库,然后在项目的src\main\application\目录下添加一个application.properties文件,这个文件是SpringBoot项目的配置文件,当整合不同的项目时,该配置文件支持大量不同的属性,不同的属性也由不同的类处理类负责读取。
#数据库url地址
sparing.datasource.url=jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=32147
#指定显示SQL语句
spring.jpa.show-sql=true
#指定根据实体自动建表
spring.jpa.generate-ddl=true
上面配置文件指定了连接数据库的基本信息:URL地址、用户名和密码,并指定了JPA能根据实体类自动建表,还会显示所执行的SQL语句。
为项目创建一个Book实体类,该实体类代码如下:
\firstbook\domain\Book.java
import javax.persistence.*;
@Entity
@Table(name = "book_inf")
public class Book
{
@Id
@Column(name = "book_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String title;
private String author;
private double price;
public Book(){}
public Book(String title, String author, double price)
{
this.title = title;
this.author = author;
this.price = price;
}
public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
public String getTitle()
{
return title;
}
public void setTitle(String title)
{
this.title = title;
}
public String getAuthor()
{
return author;
}
public void setAuthor(String author)
{
this.author = author;
}
public double getPrice()
{
return price;
}
public void setPrice(double price)
{
this.price = price;
}
@Override
public String toString()
{
return "Book{" +
"id=" + id +
", title='" + title + '\'' +
", author='" + author + '\'' +
", price=" + price +
'}';
}
}
配置文件指定了连接数据库的基本信息,SpringBoot将会自动在容器中配置一个DataSource Bean,SpringBoot将会自动在容器中配置一个EntityManagerFactory Bean。
为项目创建DAO组件:BookDao,该DAO组件接口代码如下:
\dao\BookDao.java
import org.crazyit.firstboot.domain.Book;
import org.springframework.data.repository.CrudRepository;
public interface BookDao extends CrudRepository<Book, Integer>
{
}
该BookDao接口完全是一个空接口,仅仅继承了CrudRepository,实际上已经拥有了大量方法。得益于Spring Data的优秀设计,集成了CrudRepository接口的BookDao不需要提供实现类,Spring Data会自动为它动态生成实现类,并将给实现类的实例部署在Spring容器中,Spring Data还可为BookDao动态增加很多查询方法。
该应用还提供了RESTful接口,可使用Postman来测试RESTful接口。使用Postman向"http://localhost:8080/rest/books"发送GET请求
十四、单元测试:测试RESTful接口
SpringBoot提供了@SpringBootTest注解,该注解用于修饰单元测试用例类。测试用例的测试方法依然使用@Test或@ParameterizedTest注解修饰。
<!-- SpringBoot单元测试的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
该依赖就是指SpringBoot单元测试的依赖库,由于该依赖又依赖JUnit 5.x,因此添加该依赖会自动添加JUnit5依赖。
\controller\RandomPortTest.java
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.crazyit.firstboot.domain.Book;
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
public class RandomPortTest
{
@Autowired
private TestRestTemplate restTemplate;
@Test
public void testIndexRest()
{
//测试restIndex方法
var result = restTemplate.getForObject("/rest",String.class);
Assertions.assertEquals("欢迎访问第一个SpringBoot应用")
}
@ParameterizedTest
@CsvSource({"java,fy,180","python,py,160"})
public void testRestAddBook(String title,String author,double price)
{
var book =new Book(title,author,price);
//测试restAddBook方法
var result = restTemplate.postForObject("/rest/books",book,Map.class);
Assertions.assertEquals(result.get("tip"),"添加成功");
}
@Test
public void testRestList()
{
//测试restList方法
var result = restTemplate.getForObject("/rest/books",List.class);
result.forEach(System.out::println);
}
@ParameterizedTest
@ValueSource(ints={4,5})
public void testRestDelete(Integer id){
//测试restDelete方法
restTemplate.delete("/rest/books/{0}",id);
}
}
上面测试用例类使用了@SpringBootTest注解yt.RANDOM_PORT,这表示在运行测试时,将会为Web服务器随机分配端口。
上面测试方法有的使用了@Test修饰,有的使用了@ParameterizedTest修饰,后者是JUnit 5.x新增的测试注解,用于表示参数化测试。@ValueSource、@CsvSource等注解提供的参数来调用参数化测试方法。
上面的测试用例中依赖注入了一个TestRestTemplate对象,这个TestRestTemplate对象实际上是对Resttemplate进行了封装,可以在测试环境中更方便地使用RestTemplate的功能,因此TestRestTemplate主要用于测试RESTful接口的功能。
上面的@SpringBootTest注解指定了webEnvironment = WebEnvironment.RANDOM_PORT,不需要知道web服务器端口是多少,可以直接进行测试。如果想使用固定端口,可以将webEnvironment属性指定为WebEnvironment.DEFINED_PORT,这样SpringBoot就会读取项目配置文件中的端口来启动Web服务器,若没有配置的话,默认值为8080端口
十五、单元测试:模拟web环境测试控制器
在设置@SpringBootTest的webEnvironment属性时,在运行单元测试时,都会启动一个真实的Web服务器,不想启动Web服务器,则可以将webEnvironment属性设置为WebEnvironment.MOCK,该属性设置启动模拟的Web服务器。
前面的测试用例使用TestRestTemplate来测试RESTful接口,如果想测试普通的控制器处理方法,比如读取处理方法返回的ModelAndView,则可以使用MockMvc.
下面是使用MockMvc测试控制器处理方法的测试用例。
import org.crazyit.firstboot.domain.Book;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import java.net.URI;
import java.util.List;
import java.util.Map;
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class MockEnvTest
{
@Autowired
private MockMvc mvc;
@Test
public void testIndex() throws Exception
{
// 测试index方法
var result = mvc.perform(MockMvcRequestBuilders.get(new URI("/")))
.andReturn().getModelAndView();
Assertions.assertEquals(Map.of("tip", "欢迎访问第一个Spring Boot应用")
, result.getModel());
Assertions.assertEquals("hello", result.getViewName());
}
@ParameterizedTest
@CsvSource({"疯狂Java讲义, 李刚, 129.0", "疯狂Android讲义, 李刚, 128.0"})
public void testAddBook(String title, String author, double price) throws Exception
{
// 测试addBook方法
var result = mvc.perform(MockMvcRequestBuilders.post(new URI("/addBook"))
.param("title", title)
.param("author", author)
.param("price", price + ""))
.andReturn().getModelAndView();
Assertions.assertEquals("redirect:listBooks", result.getViewName());
}
@Test
public void testList() throws Exception
{
// 测试list方法
var result = mvc.perform(MockMvcRequestBuilders.get(new URI("/listBooks")))
.andReturn().getModelAndView();
Assertions.assertEquals("list", result.getViewName());
List<Book> books = (List<Book>) result.getModel().get("books");
books.forEach(System.out::println);
}
@ParameterizedTest
@ValueSource(ints = {7, 8})
public void testDelete(Integer id) throws Exception
{
// 测试delete方法
var result = mvc.perform(MockMvcRequestBuilders.get("/deleteBook?id={0}", id))
.andReturn().getModelAndView();
Assertions.assertEquals("redirect:listBooks", result.getViewName());
}
}
- @SpringBootTest修饰该测试用例类,并将webEnvironment属性指定为WebEnvironment.MOCK,意味着启动模拟的Web服务器。
- 使用@AutoConfigureMockMvc启用MockMvc的自动配置,SpringBoot会在容器中自动配置一个MockMvc Bean
- private MockMvc mvc;定义了一个类型为MockMvc的实例变量,并使用了@Autowired注解修饰,以便Spring容器为该属性依赖注入容器中的MockMvc对象。
使用MockMvc执行测试的方法只要两步:
- 使用MockMvcRequesBuilders的get()、post()、put()、patch()、delete()、options()、header()等方法创建对应的请求,需要设置请求参数、请求头等,则接着调用MockHttpServletRequestBuilder的param()、header()等方法
- 调用MockMvc对象的perform()方法执行请求。
- MockMvc的perform()方法返回ResultActions,通过该对象的返回值可读取到控制器处理方法的ModelAndView,还可通过getResponse()获取控制器处理方法返回的响应,具体读取哪种信息根据测试需求决定,本测试用例主要读取控制器处理方法返回的ModelAndView。
十六、单元测试:测试业务组件
如果只是测试Service组件或DAO组件等,则不需要启动Web服务器,可将@SpringBootTest注解的webEnvironment属性设置为WebEnvironment.NONE,这就代表不启动Web服务器。
下面测试用例用于测试上面的BookService组件
import org.crazyit.firstboot.domain.Book;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class BookServiceTest
{
@Autowired
private BookService bookService;
@Test
public void testGetAllBooks()
{
bookService.getAllBooks().forEach(System.out::println);
}
@ParameterizedTest
@CsvSource({"疯狂Java讲义, 李刚, 129.0", "疯狂Android讲义, 李刚, 128.0"})
public void testAddBook(String title, String author, double price)
{
var book = new Book(title, author, price);
Integer result = bookService.addBook(book);
System.out.println(result);
Assertions.assertNotEquals(result, 0);
}
@ParameterizedTest
@ValueSource(ints = {9, 10})
public void testDeleteBook(Integer id)
{
bookService.deleteBook(id);
}
}
- 第一行粗体字代码指定了webEnvironment属性为WebEnvironment.NONE,这意味着不启动Web服务器来运行该测试用例
- private BookService bookService;定义了一个BookService类型的实例变量,并使用了@Autowired注解修饰,Spring容器会将容器中唯一的类型为BookService的Bean注入该实例变量。
- 由于此处测试的只是普通的BookService对象,因此测试方法直接调用被测试组件的方法即可。
十七、单元测试:使用模拟组件
实际应用中的组件可能需要依赖其他组件来访问数据库,或者调用第三方接口提供的服务。为了避免这些不稳定因素影响单元测试的效果,可以使用Mock组件来模拟这些不稳定的组件,用于确保被测试组件代码的健壮性。
上面例子中BookService组件需要调用BookDao来访问数据库,而BookDao有可能还不稳定,设置组件还未被开发出来,此时想对BookService组件进行测试,就需要提供一个Mock组件来模拟BookDao。
下面是使用Mock模拟BookDao,对BookService执行单元测试的测试用例
import org.crazyit.firstboot.dao.BookDao;
import org.crazyit.firstboot.domain.Book;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.util.List;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class MockTest
{
// 定义要测试的目标组件:BookService
@Autowired
private BookService bookService;
// 为BookService依赖的组件定义一个Mock Bean
// 该Mock Bean将会被注入被测试的目标组件
@MockBean
private BookDao bookDao;
@Test
public void testGetAllBooks()
{
// 模拟bookDao的findAll()方法的返回值
BDDMockito.given(this.bookDao.findAll()).willReturn(
List.of(new Book("测试1", "李刚", 89.9),
new Book("测试2", "yeeku", 99.9)));
List<Book> result = bookService.getAllBooks();
Assertions.assertEquals(result.get(0).getTitle(), "测试1");
Assertions.assertEquals(result.get(0).getAuthor(), "李刚");
Assertions.assertEquals(result.get(1).getTitle(), "测试2");
Assertions.assertEquals(result.get(1).getAuthor(), "yeeku");
}
}
- 上面测试用例中定义了一个BookDao类型的实例变量,但并未使用@Autowired注解修饰该实例变量,而是使用了@MockBean修饰该实例变量,这就表明Spring会使用Mock Bean来模拟该BookDao实例变量
- 在testGetAllBooks()测试方法中,BDDMockito类的given()静态方法为bookDao(不是BookDao组件,而是一个Mock Bean)的findAll()方法指定了返回值。
- 当BookService调用getAllBooks()方法时,该方法所依赖的BookDao组件的findAll()方法将直接使用该Mock Bean的findAll()方法的返回值。
- 运行上面的testGetAllBooks()测试方法,此时不管底层数据库包含什么样的数据,上面的测试用例总能通过测试。这是因为该测试用例并未使用真正的BookDao组件,而是直接使用了Mock Bean的返回值,所以测试结果总是稳定的。