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的返回值,所以测试结果总是稳定的。
  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

最笨的羊羊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值