1. 创建Spring Boot项目
我们将使用Spring Initializr 工具引导我们的应用程序。
请访问http://start.spring.io。
项目名称为:spring-boot-crud-example
在Language部分中选择Java。
在 Artifact [ˈɑrtɪˌfækt] 中填入:spring-boot-crud-example
添加Web、Lombok、JPA和MariaDB依赖项。
单击生成以生成并下载项目。
2. 配置 Gradle
通过 文件 -> 设置 -> 构建、执行、部署 -> 构建工具 -> Gradle
来设置 Gradle 的一些属性。
同时修改 ./gradle/wrapper/gradle-wrapper.properties
文件,gradle 使用本地下载的 7.5版本,同时使用代理IP(不需要代理可删除相关配置)
gradle-wrapper.properties文件内容
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=file:///data/gradle-7.5/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
systemProp.http.proxyHost=XXX.XXX.XXX.XXX
systemProp.http.proxyPort=3128
systemProp.https.proxyHost=XXX.XXX.XXX.XXX
systemProp.https.proxyPort=3128
3. Maven依赖
编辑 ./build.gradle.kts
文件,将 maven 中央仓库源修改为阿里仓库源。
build.gradle.kts文件样例
plugins {
java
id("org.springframework.boot") version "2.7.6"
id("io.spring.dependency-management") version "1.0.15.RELEASE"
}
group = "xin.qishuo"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
maven ("https://maven.aliyun.com/repository/jcenter")
maven ( "https://maven.aliyun.com/repository/google")
maven ( "https://maven.aliyun.com/repository/central")
maven ( "https://maven.aliyun.com/repository/gradle-plugin")
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
compileOnly("org.projectlombok:lombok")
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
annotationProcessor("org.projectlombok:lombok")
implementation("com.alibaba.fastjson2:fastjson2:2.0.20")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType <Test> {
useJUnitPlatform()
}
额外增补了 fastjson2
的依赖包。
4. 配置 MariaDB 数据库
首先在MariaDB
服务器中创建一个名为study
的数据库。
然后配置数据库URL、用户名和密码,以便Spring Boot可以创建数据源。
打开./src/main/resources/application.properties
文件,并向其中添加以下属性:
application.properties文件样例
spring.datasource.url = jdbc:mariadb://172.16.4.43:6006/study?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
spring.datasource.driver-class-name = org.mariadb.jdbc.Driver
spring.datasource.username = study
spring.datasource.password = studyxy00yz
# timeout 60 sec
spring.datasource.hikari.connection-timeout=60000
# max 5
spring.datasource.hikari.maximum-pool-size=5
# spring.jpa.hibernate.ddl-auto = create-drop
spring.jpa.hibernate.ddl-auto = update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
在该配置中,spring.jpa.hibernate.ddl-auto
属性的属性值及其说明如下:
属性值 | 说明 |
---|---|
none | 不指定数据库初始化模式 |
create | 当Spring Boot应用运行时,会删除并重新创建数据库。所以每次启动时,所有的数据都会被清空 |
create-drop | 当sessionFactory关闭,表会自动删除 |
validate | 在Spring Boot应用运行时,会检查数据库中的表与java实体类是否匹配。如果不匹配,则运行失败 |
update | 当在java实体类中新增了一个字段,在应用重新运行时,会将字段添加到数据库表中的新列,但不会移除先前存在的列或约束 |
注意:当数据库是嵌入式数据库时,Spring Boot会指定该属性默认值为create-drop;当不是嵌入式数据库时,Spring Boot指定该属性的默认值为none。
在开发阶段中,通常使用update,但需要注意,update不会移除先前已经存在的列和约束,即使是不再需要的。当产品发布的时候,建议使用none或直接不指定该属性(数据库单独维护)。
至此,我们可以启动该Spring Boot项目:
5. 创建 模型(Model) 层
创建一个entity
的包名,并在其下创建一个Product
类,其中包含以下内容:
实体类Product的样板代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "PRODUCT_TBL")
public class Product {
@Id
@GeneratedValue
private int id;
private String name;
private int quantity;
private BigDecimal price;
}
6. 创建 存储库(Repository) 层
创建一个repository
的包名,并在其下创建ProductRepository
接口,使其继承JpaRepository
。具体内容如下:
ProductRepository接口的样板代码
import org.springframework.data.jpa.repository.JpaRepository;
import xin.qishuo.sprintbootcrudexample.entity.Product;
public interface ProductRepository extends JpaRepository <Product,Integer> {
Product findByName(String name);
}
7. 创建 服务(Service) 层
创建一个service
的包名,并在其下创建一个ProductService类,其中包含以下内容:
ProductService类的样板代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xin.qishuo.sprintbootcrudexample.entity.Product;
import xin.qishuo.sprintbootcrudexample.repository.ProductRepository;
import java.util.List;
@Service
public class ProductService {
@Autowired
private ProductRepository repository;
public Product saveProduct(Product product) {
return repository.save(product);
}
public List<Product> saveProducts(List<Product> products) {
return repository.saveAll(products);
}
public List<Product> getProducts() {
return repository.findAll();
}
public Product getProductById(int id) {
return repository.findById(id).orElse(null);
}
public Product getProductByName(String name) {
return repository.findByName(name);
}
public String deleteProduct(int id) {
repository.deleteById(id);
return "product removed !! " + id;
}
public Product updateProduct(Product product) {
Product existingProduct = repository.findById(product.getId()).orElse(null);
existingProduct.setName(product.getName());
existingProduct.setQuantity(product.getQuantity());
existingProduct.setPrice(product.getPrice());
return repository.save(existingProduct);
}
}
8. 创建 控制器(Controller) 层
创建一个controller
的包名,并在其下创建一个ProductController
类,其中包含以下内容:
ProductController类的样板代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import xin.qishuo.sprintbootcrudexample.entity.Product;
import xin.qishuo.sprintbootcrudexample.service.ProductService;
import java.util.List;
@RestController
public class ProductController {
@Autowired
private ProductService service;
@PostMapping("/addProduct")
public Product addProduct(@RequestBody Product product) {
return service.saveProduct(product);
}
@PostMapping("/addProducts")
public List<Product> addProducts(@RequestBody List<Product> products) {
return service.saveProducts(products);
}
@GetMapping("/products")
public List<Product> findAllProducts() {
return service.getProducts();
}
@GetMapping("/productById/{id}")
public Product findProductById(@PathVariable int id) {
return service.getProductById(id);
}
@GetMapping("/product/{name}")
public Product findProductByName(@PathVariable String name) {
return service.getProductByName(name);
}
@PutMapping("/update")
public Product updateProduct(@RequestBody Product product) {
return service.updateProduct(product);
}
@DeleteMapping("/delete/{id}")
public String deleteProduct(@PathVariable int id) {
return service.deleteProduct(id);
}
9. 运行Spring Boot
应用程序
10. 使用 SpringBootTest 测试
一旦依赖了spring-boot-starter-test
,下面这些类库将被一同依赖进去:
- JUnit:java测试事实上的标准,默认依赖版本是4.12(JUnit5和JUnit4差别比较大,集成方式有不同)。
- Spring Test & Spring Boot Test:Spring的测试支持。
- AssertJ:提供了流式的断言方式。
- Hamcrest:提供了丰富的matcher。
- Mockito:mock框架,可以按类型创建mock对象,可以根据方法参数指定特定的响应,也支持对于mock调用过程的断言。
关于Mock的介绍请参阅:附录 - JSONassert:为JSON提供了断言功能。
- JsonPath:为JSON提供了XPATH功能。
编写测试类:TestProductController
TestProductController类的样板代码
import com.alibaba.fastjson2.JSON;
import org.junit.jupiter.api.Test;
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.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import xin.qishuo.sprintbootcrudexample.entity.Product;
import xin.qishuo.sprintbootcrudexample.repository.ProductRepository;
import java.math.BigDecimal;
import java.net.URI;
@SpringBootTest
@AutoConfigureMockMvc
public class TestProductController {
@Autowired
ProductRepository productRepository;
@Autowired
private MockMvc mockMvc;
/**
* post请求,json数据提交
*
* @return
* @throws Exception
*/
@Test
void addProduct() throws Exception {
Product product = new Product();
product.setId(2);
product.setName("earphone");
product.setPrice(new BigDecimal(29.9));
product.setQuantity(222);
String productJsonStr = JSON.toJSONString(product);
ResultActions ra = mockMvc.perform(MockMvcRequestBuilders
.post(new URI("/addProduct"))
.content(productJsonStr)
.contentType(MediaType.APPLICATION_JSON_VALUE)
).andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(2))
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("earphone"))
.andExpect(MockMvcResultMatchers.jsonPath("$.quantity").value(222))
.andExpect(MockMvcResultMatchers.jsonPath("$.price").value(29.9))
.andDo(MockMvcResultHandlers.print());
MvcResult result = ra.andReturn();
System.out.println("--------return: " + result.getResponse().getContentAsString());
}
/**
* get请求, @PathVariable 路径参数提交
*
* @return
* @throws Exception
*/
@Test
void findProductById() throws Exception {
int id = 1;
ResultActions ra = mockMvc.perform(MockMvcRequestBuilders
.get("/productById/{id}", id)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1))
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("book"))
.andExpect(MockMvcResultMatchers.jsonPath("$.quantity").value(111))
.andExpect(MockMvcResultMatchers.jsonPath("$.price").value(19.9))
.andDo(MockMvcResultHandlers.print());
MvcResult result = ra.andReturn();
System.out.println("--------return: " + result.getResponse().getContentAsString());
}
}
在测试过程中可以看到请求参数与响应参数:
11. 使用 DataJpaTest 测试
附录
附录A. 相关联的文章
附录B. 其他参考
附录C. Mock简要介绍
-
什么是Mock
在面向对象的程序设计中,模拟对象(英语:mock object)是以可控的方式模拟真实对象行为的假对象。在编程过程中,通常通过模拟一些输入数据,来验证程序是否达到预期结果。 -
为什么使用Mock对象
使用模拟对象,可以模拟复杂的、真实的对象行为。如果在单元测试中无法使用真实对象,可采用模拟对象进行替代。
在以下情况可以采用模拟对象来替代真实对象:- 真实对象的行为是不确定的(例如,当前的时间或温度);
- 真实对象很难搭建起来;
- 真实对象的行为很难触发(例如,网络错误);
- 真实对象速度很慢(例如,一个完整的数据库,在测试之前可能需要初始化);
- 真实的对象是用户界面,或包括用户界面在内;
- 真实的对象使用了回调机制;
- 真实对象可能还不存在;
- 真实对象可能包含不能用作测试(而不是为实际工作)的信息和方法。
-
MockMvc
MockMvc是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。
接口MockMvcBuilder,提供一个唯一的build方法,用来构造MockMvc。主要有两个实现:StandaloneMockMvcBuilder和DefaultMockMvcBuilder,分别对应两种测试方式,即独立安装和集成Web环境测试(并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)。MockMvcBuilders提供了对应的创建方法standaloneSetup方法和webAppContextSetup方法,在使用时直接调用即可。 -
Mock中的一些方法的介绍
- mockMvc.perform执行一个请求。
- MockMvcRequestBuilders.get("XXX")构造一个请求。
- ResultActions.param添加请求传值
- ResultActions.accept(MediaType.TEXT_HTML_VALUE))设置返回类型
- ResultActions.andExpect添加执行完成后的断言。
- ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情
比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。 - ResultActions.andReturn表示执行完成后返回相应的结果。