简介
本质上说Spring是一个组件容器,它负责创建并管理容器中的组件(也被称为Bean),并管理组件之间的依赖关系。
为什么要用SpringBoot?
Spring缺点是配置过多,SpringBoot就是为了解决这个问题。
SpringBoot为大部分第三方框架整合提供了自动配置。SpringBoot使用约定优于配置(CoC Covention over Configuration)为理念,针对企业开发各种场景,提供对应的Starter。
总体来说SpringBoot具有以下特性:
- 内嵌Tomcat、jetty或Undertow服务器。
- SpringBoot应用可被做成独立的Java应用程序。
- 尽可能的自动配置Spring及第三方框架。
- 完全没有代码生成,不需要XML配置。
- 提供产品级监控功能,如运行状况检查和外部化配置等。
创建SpringBoot应用
开发环境准备
Java 、Maven(配置本地资源库和国内镜像)
创建SpringBoot项目
只需要创建一个Maven项目即可,单机IDEA主菜单"File"-->"New"-->"project"-->左边选"Maven"-->"Next"
项目结构如下:
使用SpringInitializr工具自动生成Maven项目
单机IDEA主菜单"File"-->"New"-->"project"
编写控制器
java
@Controller
public class BookController {
@GetMapping("/")
public String index(Model model){
model.addAttribute("tip", "Hello, World!");
return "hello";
}
@GetMapping("/rest")
// 指定该方法生成restful风格的响应
// 在前后端分离的架构中,SpringBoot应用只需要对外提供restful响应,前端通过restful接口与后端通信
@ResponseBody
public ResponseEntity restIndex() {
return new ResponseEntity<>(
"Hello, World from REST!",null,
HttpStatus.OK);
}
}
运行应用
java
package com.example.ali2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// 被@SpringBootApplication修饰的类位于com.example.ali2包下,Spring Boot会自动扫描该包及其子包下的所有配置类(@Configuration修饰的类)
// 和组件类(@Component、@Controller、@Service、@Repository 等修饰的类),将它变成容器中的bean
@SpringBootApplication
public class Ali2Application {
public static void main(String[] args) {
SpringApplication.run(Ali2Application.class, args);
}
}
创建jar包
java
<!-- 打jar包首先保证在pom.xml中添加了spring-boot-maven-plugin插件,这样可以通过mvn package命令打包成可执行的jar文件。-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<!-- 此外,如果在pom.xml中添加了<packaging>元素,请确保是<packaging>jar</packaging>,省略也是可以的-->
执行maven命令打成jar包
mvn clean
mvn package
执行"mvn package"命令,会从默认生命周期的第一阶段一直执行到"package"阶段,最终生成一个可执行的jar文件。这个jar文件会被放在target目录下,文件名通常是<artifactId>-<version>.jar,例如ali2-0.0.1-SNAPSHOT.jar。
而maven的生命周期包含compile(编译)、test(单元测试)、package(打包)、install(安装到本地)、deploy(部署到远程)等阶段,执行"mvn package"命令会自动执行compile、test等阶段,直到package阶段。
如果前面的阶段失败,则打包失败。
开发业务组件
先引入数据库相关依赖
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.3.5</version> <!-- 可根据需要选择版本 -->
</dependency>
<!-- Spring Data JPA 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.3.6</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
<scope>runtime</scope>
</dependency>
service
java
public interface BookService {
List<Book> getAllBooks();
Integer addBook(Book book);
void deleteBook(Integer id);
}
serviceImpl
java
@Service
@Transactional(propagation= Propagation.REQUIRED,timeout = 5) // 事务注解,表示该类中的所有方法需要事务支持
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public List<Book> getAllBooks() {
return (List<Book>)bookDao.findAll();
}
@Override
public Integer addBook(Book book) {
bookDao.save(book);
return book.getId();
}
@Override
public void deleteBook(Integer id) {
bookDao.deleteById(id);
}
}
controller
java
@Controller
public class BookController {
@GetMapping("/")
public String index(Model model){
model.addAttribute("tip", "Hello, World!");
return "hello";
}
@GetMapping("/rest")
// 指定该方法生成restful风格的响应
// 在前后端分离的架构中,SpringBoot应用只需要对外提供restful响应,前端通过restful接口与后端通信
@ResponseBody
public ResponseEntity restIndex() {
return new ResponseEntity<>(
"Hello, World from REST!",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", "Book added successfully!"),
HttpStatus.OK
);
}
@GetMapping("/listBooks")
public String listBooks(Model model) {
model.addAttribute("books", bookService.getAllBooks());
return "list";
}
@GetMapping("/rest/books")
@ResponseBody
public ResponseEntity<List<Book>> restListBooks() {
List<Book> books = bookService.getAllBooks();
return new ResponseEntity<>(books, null,HttpStatus.OK);
}
@GetMapping("/deleteBook")
public String deleteBook(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", "Book deleted successfully!"),null,
HttpStatus.OK
);
}
}
hello.html
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" th:href="@{/webjars/bootstrap/5.3.5/css/bootstrap.min.css}"/>
<script type="text/javascript" th:src="@{/webjars/jquery/3.6.4/jquery.min.js}"></script>
</head>
<body>
<div class="container">
<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" class="form-control" id="title" name="title" placeholder="请输入图书名" required>
</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" class="form-control" id="author" name="author" placeholder="请输入作者" required>
</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" class="form-control" id="price" name="price" placeholder="请输入价格" required>
</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-primary">重设</button>
</div>
</div>
</form>
</div>
</body>
</html>
list.html
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" th:href="@{/webjars/bootstrap/5.3.5/css/bootstrap.min.css}"/>
<script type="text/javascript" th:src="@{/webjars/jquery/3.6.4/jquery.min.js}"></script>
</head>
<body>
<div class="container">
<h2>全部图书</h2>
<table class="table table-hover">
<thead>
<tr>
<th>图书名</th>
<th>作者</th>
<th>价格</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="book : ${books}">
<td th:text="${book.title}">书名</td>
<td th:text="${book.author}">作者</td>
<td th:text="${book.price}">价格</td>
<td>
<a th:href="@{/deleteBook(id=${book.id})}" class="btn btn-danger">删除</a>
</td>
</tr>
</tbody>
</table>
<div class="text-right" >
<a th:href="@{/}" class="btn btn-primary">添加图书</a>
</div>
</div>
</body>
</html>
application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/ali2?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
# default ddl auto
spring.jpa.generate-ddl=true
Book
java
@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;
}
@Override
public String toString()
{
return "Book{" +
"id=" + id +
", title='" + title + '\'' +
", author='" + author + '\'' +
", price=" + price +
'}';
}
// Getters and Setters
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;
}
}
BookDao
java
// 继承CrudRepository接口,提供基本的CRUD操作
public interface BookDao extends CrudRepository<Book, Integer> {
}
编写单元测试
测试restful接口
引入单元测试依赖
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
java
// @SpringBootTest修饰单元测试用例类
// webEnvironment属性指定了测试环境的web环境,这里使用随机端口。不需要知道具体端口号,Spring Boot会自动分配一个可用的端口
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RandomPortTest {
@Autowired
private TestRestTemplate restTemplate;
public void testIndexRest() {
// 使用TestRestTemplate发送请求,测试RESTful接口
String response = restTemplate.getForObject("/rest", String.class);
Assertions.assertEquals("Hello, World from REST!", response);
}
//这个注解告诉 JUnit 测试是参数化的,并且有测试值注入到其中。
@ParameterizedTest
@CsvSource({"西柚击,黧黑,129.0", "Java编程思想,布鲁斯·埃克尔,99.0"})
public void testRestAddBook(String title,String author, Double price) {
var book = new com.example.ali2.domain.Book(title, author, price);
// 使用TestRestTemplate发送请求
var response = restTemplate.postForObject("/rest/books",book, Map.class);
Assertions.assertEquals(response.get("tip"), "Book added successfully!");
}
@Test
public void testRestList() {
// 使用TestRestTemplate发送请求,测试获取书籍列表
var result = restTemplate.getForObject("/rest/books", List.class);
result.forEach(System.out::println);
}
@ParameterizedTest
@ValueSource(ints = {4,5})
public void testRestDelete(Integer id) {
// 使用TestRestTemplate发送请求,测试删除书籍
restTemplate.delete("/rest/books/{0}",id);
}
}
模拟web环境测试控制器
如果想测试普通的控制器处理方法,比如读取方法返回的ModelAndView,则可使用MockMVC。
java
// WebEnvironment.MOCK意味着启动模拟的web环境,不会启动实际的服务器
// WebEnvironment的默认值是WebEnvironment.Mock,其实不写也行
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
// @AutoConfigureMockMvc注解会自动配置MockMvc对象
@AutoConfigureMockMvc
public class MockEnvTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testIndex() throws Exception {
// 使用MockMvc发送请求,测试首页
var result = mockMvc.perform(MockMvcRequestBuilders.get(new URI("/"))).andReturn().getModelAndView();
Assertions.assertEquals(Map.of("tip", "Hello, World!"), 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 = mockMvc.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 = mockMvc.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 = mockMvc.perform(MockMvcRequestBuilders.get("/deleteBook?id={0}", id))
.andReturn().getModelAndView();
Assertions.assertEquals("redirect:listBooks", result.getViewName());
}
}
测试业务组件
如果只是测试service或者Dao组件,则不需要启动web服务器。webEnvironment = WebEnvironment.NONE表示不启动web服务器
java
// WebEnvironment.NONE意味着不启动实际的web环境,适用于只测试服务层逻辑的情况
@SpringBootTest(webEnvironment = SpringBootTest.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);
}
}
使用模拟组件
实际应用中,组件可能需要依赖其他组件来访问数据库,或者调用第三方接口,为避免这些不稳定因素影响单元测试效果,使用mock组件来模拟这些不稳定的组件。
比如BookDao未开发出来,如果对BookService测试,需要使用mock来模拟BookDao。
java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class MockTest {
// 定义要测试的目标组件:BookService
@Autowired
private BookService bookService;
// 使用@MockBean注解模拟BookDao组件
@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");
}
}