跳到主要内容
Spring Boot RESTful API 开发与测试 | 极客日志
Java java
Spring Boot RESTful API 开发与测试 Spring Boot RESTful API 开发涉及资源定义、HTTP 方法映射、分层架构实现及自动化测试。核心在于利用 @RestController 注解构建控制器,结合 JPA 处理数据持久化,并通过单元测试、集成测试与 Mock 验证逻辑正确性。安全方面需集成 Spring Security 或 JWT 进行认证授权,确保接口访问受控。实际应用中可灵活组合这些技术栈,快速构建高可用、易维护的后端服务。
RedisGeek 发布于 2026/3/15 更新于 2026/4/27 2 浏览Spring Boot RESTful API 开发与测试
学习目标与重点
本章旨在掌握 Spring Boot RESTful API 的核心开发流程与测试方法。我们将深入探讨 RESTful 的设计原则,学习如何使用 Spring Boot 快速构建接口,并覆盖从单元测试到集成测试的完整验证体系。此外,安全认证(Spring Security、JWT)也是实战中不可或缺的一环。
核心要点:
RESTful 资源设计与 HTTP 状态码规范
Controller、Service、Repository 分层实现
自动化测试策略(Unit, Integration, Mock)
接口安全与权限控制
RESTful API 概述
RESTful API 是 Java 后端开发的主流设计风格,它基于 REST 架构风格,通过 URI 标识资源,利用 HTTP 动词(GET、POST、PUT、DELETE 等)执行操作,并使用标准响应状态码反馈结果。这种设计不仅统一了编程模型,还能显著提升开发效率。
常用 HTTP 方法与状态码
在定义接口时,遵循以下约定能让调用方更直观地理解意图:
GET :获取资源
POST :创建资源
PUT :更新资源
DELETE :删除资源
PATCH :部分更新资源
常见的响应状态码包括:
200 OK:请求成功
201 Created:资源创建成功
400 Bad Request:参数错误
401 Unauthorized:未授权
403 Forbidden:禁止访问
404 Not Found:资源不存在
500 Internal Server Error:服务器内部错误
Spring Boot RESTful API 开发
开发一个标准的 RESTful 服务通常包含以下步骤:创建项目、引入依赖、定义实体、编写 Repository、Service 层逻辑以及 Controller 接口。
1. 项目依赖配置
首先需要在 pom.xml 中添加 Web、JPA 和数据源依赖。这里我们使用 H2 内存数据库以便快速演示。
<dependencies >
<dependency >
<groupId > org.springframework.boot</groupId >
< > spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-jpa
com.h2database
h2
runtime
org.springframework.boot
spring-boot-starter-test
test
artifactId
</artifactId >
</dependency >
<dependency >
<groupId >
</groupId >
<artifactId >
</artifactId >
</dependency >
<dependency >
<groupId >
</groupId >
<artifactId >
</artifactId >
<scope >
</scope >
</dependency >
<dependency >
<groupId >
</groupId >
<artifactId >
</artifactId >
<scope >
</scope >
</dependency >
</dependencies >
2. 实体类定义 使用 JPA 注解映射数据库表结构。注意字段命名规范和 Getter/Setter 的生成。
import javax.persistence.*;
@Entity
@Table(name = "product")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String productId;
private String productName;
private double price;
private int sales;
public Product () {}
public Product (String productId, String productName, double price, int sales) {
this .productId = productId;
this .productName = productName;
this .price = price;
this .sales = sales;
}
public Long getId () { return id; }
public void setId (Long id) { this .id = id; }
public String getProductId () { return productId; }
public void setProductId (String productId) { this .productId = productId; }
public String getProductName () { return productName; }
public void setProductName (String productName) { this .productName = productName; }
public double getPrice () { return price; }
public void setPrice (double price) { this .price = price; }
public int getSales () { return sales; }
public void setSales (int sales) { this .sales = sales; }
@Override
public String toString () {
return "Product{" +
"id=" + id +
", productId='" + productId + '\'' +
", productName='" + productName + '\'' +
", price=" + price +
", sales=" + sales +
'}' ;
}
}
3. 数据访问层 (Repository) 继承 JpaRepository 即可自动获得基础 CRUD 能力,还可以自定义查询方法。
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductRepository extends JpaRepository <Product, Long> {
List<Product> findBySalesGreaterThan (int sales) ;
}
4. 业务逻辑层 (Service) Service 层负责事务管理和复杂业务逻辑编排。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Transactional
public void addProduct (Product product) {
productRepository.save(product);
}
@Transactional
public void updateProduct (Product product) {
productRepository.save(product);
}
@Transactional
public void deleteProduct (Long id) {
productRepository.deleteById(id);
}
@Transactional(readOnly = true)
public List<Product> getAllProducts () {
return productRepository.findAll();
}
@Transactional(readOnly = true)
public List<Product> getTopSellingProducts (int topN) {
List<Product> products = productRepository.findBySalesGreaterThan(0 );
products.sort((p1, p2) -> p2.getSales() - p1.getSales());
if (products.size() > topN) {
return products.subList(0 , topN);
}
return products;
}
}
5. 控制器层 (Controller) 使用 @RestController 和路径映射注解暴露 HTTP 接口。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/")
public ResponseEntity<List<Product>> getAllProducts () {
List<Product> products = productService.getAllProducts();
return new ResponseEntity <>(products, HttpStatus.OK);
}
@PostMapping("/")
public ResponseEntity<Void> addProduct (@RequestBody Product product) {
productService.addProduct(product);
return new ResponseEntity <>(HttpStatus.CREATED);
}
@PutMapping("/{id}")
public ResponseEntity<Void> updateProduct (@PathVariable Long id, @RequestBody Product product) {
product.setId(id);
productService.updateProduct(product);
return new ResponseEntity <>(HttpStatus.OK);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct (@PathVariable Long id) {
productService.deleteProduct(id);
return new ResponseEntity <>(HttpStatus.NO_CONTENT);
}
@GetMapping("/top-selling")
public ResponseEntity<List<Product>> getTopSellingProducts (@RequestParam int topN) {
List<Product> products = productService.getTopSellingProducts(topN);
return new ResponseEntity <>(products, HttpStatus.OK);
}
}
Spring Boot RESTful API 的测试 高质量的代码离不开完善的测试。Spring Boot 提供了丰富的测试支持,涵盖单元测试、集成测试和 Mock 测试。
单元测试 针对 Service 层逻辑进行独立验证,不启动完整的 Web 容器。
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class ProductServiceTests {
@Autowired
private ProductService productService;
@Test
void testAddProduct () {
Product product = new Product ("P006" , "平板" , 2000.0 , 70 );
productService.addProduct(product);
List<Product> products = productService.getAllProducts();
assertThat(products).hasSize(6 );
}
@Test
void testUpdateProduct () {
Product product = new Product ("P001" , "手机" , 1500.0 , 120 );
product.setId(1L );
productService.updateProduct(product);
List<Product> products = productService.getAllProducts();
assertThat(products.get(0 ).getPrice()).isEqualTo(1500.0 );
}
@Test
void testDeleteProduct () {
productService.deleteProduct(2L );
List<Product> products = productService.getAllProducts();
assertThat(products).hasSize(4 );
}
@Test
void testGetTopSellingProducts () {
List<Product> topSellingProducts = productService.getTopSellingProducts(3 );
assertThat(topSellingProducts).hasSize(3 );
assertThat(topSellingProducts.get(0 ).getProductId()).isEqualTo("P004" );
assertThat(topSellingProducts.get(1 ).getProductId()).isEqualTo("P005" );
assertThat(topSellingProducts.get(2 ).getProductId()).isEqualTo("P001" );
}
}
集成测试 import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductControllerTests {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void testGetAllProducts () {
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/" , List.class);
assertThat(products).hasSize(5 );
}
@Test
void testAddProduct () {
Product product = new Product ("P006" , "平板" , 2000.0 , 70 );
restTemplate.postForEntity("http://localhost:" + port + "/api/products/" , product, Void.class);
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/" , List.class);
assertThat(products).hasSize(6 );
}
@Test
void testUpdateProduct () {
Product product = new Product ("P001" , "手机" , 1500.0 , 120 );
restTemplate.put("http://localhost:" + port + "/api/products/1" , product);
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/" , List.class);
assertThat(products.get(0 ).getPrice()).isEqualTo(1500.0 );
}
@Test
void testDeleteProduct () {
restTemplate.delete("http://localhost:" + port + "/api/products/2" );
List<Product> products = restTemplate.getForObject("http://localhost:" + port + "/api/products/" , List.class);
assertThat(products).hasSize(4 );
}
@Test
void testGetTopSellingProducts () {
List<Product> topSellingProducts = restTemplate.getForObject("http://localhost:" + port + "/api/products/top-selling?topN=3" , List.class);
assertThat(topSellingProducts).hasSize(3 );
assertThat(topSellingProducts.get(0 ).getProductId()).isEqualTo("P004" );
assertThat(topSellingProducts.get(1 ).getProductId()).isEqualTo("P005" );
assertThat(topSellingProducts.get(2 ).getProductId()).isEqualTo("P001" );
}
}
Mock 测试 使用 MockMvc 隔离外部依赖,专注于 Controller 层的输入输出验证。
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Arrays;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(ProductController.class)
class ProductControllerMockTests {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductService productService;
@Test
void testGetAllProducts () throws Exception {
List<Product> products = Arrays.asList(
new Product ("P001" , "手机" , 1000.0 , 100 ),
new Product ("P002" , "电脑" , 5000.0 , 50 ),
new Product ("P003" , "电视" , 3000.0 , 80 ),
new Product ("P004" , "手表" , 500.0 , 200 ),
new Product ("P005" , "耳机" , 300.0 , 150 )
);
when (productService.getAllProducts()).thenReturn(products);
mockMvc.perform(get("/api/products/" ))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$[0].productId" ).value("P001" ));
verify(productService, times(1 )).getAllProducts();
}
@Test
void testAddProduct () throws Exception {
Product product = new Product ("P006" , "平板" , 2000.0 , 70 );
doNothing().when (productService).addProduct(any(Product.class));
mockMvc.perform(post("/api/products/" )
.contentType(MediaType.APPLICATION_JSON)
.content("{\"productId\":\"P006\",\"productName\":\"平板\",\"price\":2000.0,\"sales\":70}" ))
.andExpect(status().isCreated());
verify(productService, times(1 )).addProduct(any(Product.class));
}
@Test
void testUpdateProduct () throws Exception {
Product product = new Product ("P001" , "手机" , 1500.0 , 120 );
doNothing().when (productService).updateProduct(any(Product.class));
mockMvc.perform(put("/api/products/1" )
.contentType(MediaType.APPLICATION_JSON)
.content("{\"id\":1,\"productId\":\"P001\",\"productName\":\"手机\",\"price\":1500.0,\"sales\":120}" ))
.andExpect(status().isOk());
verify(productService, times(1 )).updateProduct(any(Product.class));
}
@Test
void testDeleteProduct () throws Exception {
doNothing().when (productService).deleteProduct(anyLong());
mockMvc.perform(delete("/api/products/2" ))
.andExpect(status().isNoContent());
verify(productService, times(1 )).deleteProduct(anyLong());
}
@Test
void testGetTopSellingProducts () throws Exception {
List<Product> topSellingProducts = Arrays.asList(
new Product ("P004" , "手表" , 500.0 , 200 ),
new Product ("P005" , "耳机" , 300.0 , 150 ),
new Product ("P001" , "手机" , 1000.0 , 100 )
);
when (productService.getTopSellingProducts(3 )).thenReturn(topSellingProducts);
mockMvc.perform(get("/api/products/top-selling?topN=3" ))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$[0].productId" ).value("P004" ));
verify(productService, times(1 )).getTopSellingProducts(3 );
}
}
Spring Boot RESTful API 的认证与授权
Spring Security 基础认证 Spring Security 提供了强大的安全框架,支持基于角色的访问控制。
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-security</artifactId >
</dependency >
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure (AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(NoOpPasswordEncoder.getInstance())
.withUser("admin" ).password("admin123" ).roles("ADMIN" )
.and()
.withUser("user" ).password("user123" ).roles("USER" );
}
@Override
protected void configure (HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/products/top-selling" ).hasRole("ADMIN" )
.antMatchers("/api/products/**" ).hasRole("USER" )
.and().httpBasic();
}
}
JWT 无状态认证 对于前后端分离架构,JWT(JSON Web Token)是更常用的方案。
JwtUtil :负责生成和解析 Token。
JwtRequestFilter :拦截请求,验证 Token 有效性。
SecurityConfig :配置无状态会话模式。
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String extractUsername (String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration (String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim (String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims (String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired (String token) {
return extractExpiration(token).before(new Date ());
}
public String generateToken (String username) {
Map<String, Object> claims = new HashMap <>();
return createToken(claims, username);
}
private String createToken (Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date (System.currentTimeMillis()))
.setExpiration(new Date (System.currentTimeMillis() + expiration * 1000 ))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
public Boolean validateToken (String token, String username) {
final String extractedUsername = extractUsername(token);
return (extractedUsername.equals(username) && !isTokenExpired(token));
}
}
配置文件 application.properties:
# JWT 配置
jwt.secret=mysecret
jwt.expiration=3600
实际应用场景 在实际项目中,RESTful API 广泛应用于商品管理、订单处理、用户中心等场景。结合上述技术栈,可以快速搭建起高可用、易维护的后端服务。
例如,通过 ProductApplication 启动服务后,访问 http://localhost:8080/api/products/ 即可获取产品列表,而 /api/products/top-selling?topN=3 则返回销量前三的商品。配合安全配置,不同角色用户可访问不同级别的接口。
总结 Spring Boot RESTful API 开发涉及资源定义、HTTP 方法映射、分层架构实现及自动化测试。核心在于利用 @RestController 注解构建控制器,结合 JPA 处理数据持久化,并通过单元测试、集成测试与 Mock 验证逻辑正确性。安全方面需集成 Spring Security 或 JWT 进行认证授权,确保接口访问受控。实际应用中可灵活组合这些技术栈,快速构建高可用、易维护的后端服务。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online