跳到主要内容
Spring Boot RESTful API 开发与测试指南 | 极客日志
Java java
Spring Boot RESTful API 开发与测试指南 Spring Boot RESTful API 开发涵盖资源设计、HTTP 方法映射及分层架构实现。通过 JPA 管理数据持久层,结合单元测试、集成测试与 Mock 验证确保质量。安全机制支持 Spring Security 基础认证与 JWT 无状态令牌方案,适用于商品展示、订单管理等常见业务场景。
微码行者 发布于 2026/3/27 更新于 2026/6/13 22 浏览Spring Boot RESTful API 开发与测试指南
核心概念与 HTTP 规范
RESTful API 是 Java Web 开发中主流的设计风格,它基于 REST 架构风格,通过 URI 表示资源,利用 HTTP 方法(GET、POST、PUT、DELETE)操作资源,并通过状态码反馈结果。这种设计提供了统一的编程模型,能显著提升开发效率。
在实际应用中,我们主要关注以下 HTTP 方法与状态码:
GET :获取资源,对应 200 OK。
POST :创建资源,对应 201 Created。
PUT :全量更新资源,对应 200 OK。
DELETE :删除资源,对应 204 No Content。
PATCH :部分更新资源。
错误处理同样重要,常见的如 400 请求参数错误、401 未授权、403 禁止访问、404 资源不存在以及 500 服务器内部错误。
分层架构实战
构建一个标准的 Spring Boot RESTful 服务通常遵循 Controller-Service-Repository 的分层模式。下面我们以商品管理为例,展示从依赖配置到代码实现的完整流程。
1. 项目依赖配置
首先需要在 pom.xml 中添加 Web、JPA 及测试相关依赖:
<dependencies >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-web</artifactId >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-data-jpa</artifactId >
</dependency >
<dependency >
<groupId > com.h2database
h2
runtime
org.springframework.boot
spring-boot-starter-test
test
</groupId >
<artifactId >
</artifactId >
<scope >
</scope >
</dependency >
<dependency >
<groupId >
</groupId >
<artifactId >
</artifactId >
<scope >
</scope >
</dependency >
</dependencies >
2. 实体类定义 使用 JPA 注解映射数据库表结构,这里以 Product 为例:
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;
@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) 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 和 @RequestMapping 等注解暴露接口:
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);
}
}
测试策略
单元测试 主要针对 Service 层的逻辑进行验证,无需启动整个容器:
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 );
}
}
集成测试 测试 Controller 与外部交互的情况,需要启动真实服务器端口:
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 );
}
}
Mock 测试 针对 Controller 层,Mock 掉 Service 依赖,专注于接口响应验证:
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 )
);
when (productService.getAllProducts()).thenReturn(products);
mockMvc.perform(get("/api/products/" ))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON));
verify(productService, times(1 )).getAllProducts();
}
}
安全认证与授权 生产环境中的 API 必须考虑安全性,Spring Security 提供了完善的解决方案。
基础认证 (Basic Auth) 适用于内部系统或简单场景,通过用户名密码直接鉴权:
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 更为常用。客户端登录后获取 Token,后续请求携带 Token 即可:
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 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));
}
private String extractUsername (String token) {
return extractClaim(token, Claims::getSubject);
}
private Boolean isTokenExpired (String token) {
return extractExpiration(token).before(new Date ());
}
private <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 Date extractExpiration (String token) {
return extractClaim(token, Claims::getExpiration);
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization" );
String username = null ;
String jwt = null ;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer " )) {
jwt = authorizationHeader.substring(7 );
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null ) {
UserDetails userDetails = this .userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails.getUsername())) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken (userDetails, null , userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource ().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
server.port=8080
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
jwt.secret=mysecret
jwt.expiration=3600
场景落地 实际项目中,RESTful API 广泛应用于商品展示、订单管理、用户中心及博客发布等场景。通过上述分层设计与安全机制,可以快速构建出稳定、可扩展的后端服务。
例如,访问 http://localhost:8080/api/products/ 可获取产品列表,而 http://localhost:8080/api/products/top-selling?topN=3 则返回销量前三的商品。结合测试用例的持续运行,能有效保障功能迭代过程中的稳定性。
相关免费在线工具 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