跳到主要内容
Spring Boot RESTful API 开发、测试与安全实战 | 极客日志
Java java
Spring Boot RESTful API 开发、测试与安全实战 Spring Boot RESTful API 开发涉及资源定义、HTTP 方法映射及状态码处理。涵盖从基础 CRUD 构建到单元测试、集成测试及 Mock 验证的完整流程,并深入讲解 Spring Security 认证授权与 JWT 无状态令牌实现。通过实际商品管理场景示例,展示如何设计高效、安全的后端接口。
内存管理 发布于 2026/3/23 更新于 2026/5/6 6 浏览Spring Boot RESTful API 开发与测试
学习目标与重点提示
本章节旨在掌握 Spring Boot RESTful API 的核心概念与实战方法。我们将深入探讨 RESTful API 的设计原则、基于 Spring Boot 的开发流程、多层次的测试策略(单元测试、集成测试、Mock),以及安全认证与授权机制(Spring Security、JWT)。最终目标是能够独立构建并验证生产级的后端接口。
核心重点 :
RESTful 资源设计与 HTTP 状态码规范
@RestController、@RequestMapping 等注解的实际应用
分层架构下的 Service 与 Repository 协作
使用 JUnit 和 MockMvc 进行全链路测试
基于 Spring Security 与 JWT 的无状态认证实现
RESTful API 概述
RESTful API 是目前 Java Web 开发中最主流的设计风格,它基于 REST 架构风格,强调资源(Resource)的管理与表现层(Representation)的状态转移。
设计原则与特点
RESTful 设计的核心在于通过 URI 标识资源,利用 HTTP 方法表达操作意图,并通过响应状态码反馈结果。
资源(Resource) :所有数据都被视为资源,通过 URI 唯一标识。
表现层(Representation) :使用 GET、POST、PUT、DELETE 等 HTTP 动词对资源进行操作。
状态转移(State Transfer) :HTTP 响应状态码(如 200、201、404)清晰表明操作是否成功或失败。
这种设计不仅统一了编程模型,还能显著提高开发效率和接口的可维护性。
常用 HTTP 方法与状态码
在构建接口时,选择合适的 HTTP 方法和状态码至关重要:
方法 说明 GET 获取资源 POST 创建新资源 PUT 更新整个资源 DELETE 删除资源 PATCH 部分更新资源
Spring Boot RESTful API 开发 开发一个标准的 RESTful 服务通常遵循分层架构:Controller 处理请求,Service 处理业务逻辑,Repository 负责数据持久化。
项目搭建与依赖配置 首先创建一个 Spring Boot 项目,并在 pom.xml 中引入必要的依赖。这里我们使用 H2 内存数据库以便快速演示,实际生产中请替换为 MySQL 或 PostgreSQL。
<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</groupId >
<artifactId > h2</artifactId >
<scope > runtime</scope >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-test</artifactId >
<scope > test</scope >
</dependency >
</dependencies >
实体类定义 我们以商品管理为例,定义 Product 实体。注意 toString 方法的实现要规范,方便调试。
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 +
'}' ;
}
}
数据访问层(Repository) JPA 提供了强大的查询能力,我们可以直接定义接口来生成 SQL。
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) ;
}
业务逻辑层(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;
}
}
控制器层(Controller) Controller 暴露 REST 接口,使用 ResponseEntity 可以更灵活地控制 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 提供了多种测试工具,覆盖从单元到集成的不同层级。
单元测试 单元测试主要关注 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" );
}
}
集成测试 集成测试需要启动真实的 Web 环境,模拟客户端请求,验证 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 );
}
@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 );
}
}
Mock 测试 当需要隔离外部依赖(如数据库或第三方服务)时,可以使用 @WebMvcTest 配合 @MockBean 进行 Mock 测试,这样速度更快且更稳定。
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();
}
@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));
}
}
Spring Boot RESTful API 的认证与授权 生产环境的 API 必须考虑安全性,防止未授权访问和数据泄露。
Spring Security 基础认证 Spring Security 是 Spring 生态中的标准安全框架,支持基于角色的访问控制(RBAC)。
<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 无状态认证 对于前后端分离的项目,传统的 Session 模式不再适用,JWT(JSON Web Token)是更好的选择。
<dependency >
<groupId > io.jsonwebtoken</groupId >
<artifactId > jjwt</artifactId >
<version > 0.9.1</version >
</dependency >
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));
}
}
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 广泛应用于电商、社交、内容管理等场景。以下是一个完整的应用启动与测试示例。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.beans.factory.annotation.Autowired;
@SpringBootApplication
public class ProductApplication {
public static void main (String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
@Autowired
private ProductService productService;
public void run (String... args) {
productService.addProduct(new Product ("P001" , "手机" , 1000.0 , 100 ));
productService.addProduct(new Product ("P002" , "电脑" , 5000.0 , 50 ));
productService.addProduct(new Product ("P003" , "电视" , 3000.0 , 80 ));
productService.addProduct(new Product ("P004" , "手表" , 500.0 , 200 ));
productService.addProduct(new Product ("P005" , "耳机" , 300.0 , 150 ));
}
}
访问 http://localhost:8080/api/products/:返回产品列表。
访问 http://localhost:8080/api/products/top-selling?topN=3:返回销量 TOP3 的产品列表。
通过上述步骤,我们完成了一个具备基本 CRUD 功能、多层级测试覆盖以及安全认证的 Spring Boot RESTful API 服务。在实际项目中,建议根据具体需求调整数据库连接、加密方式及权限粒度。
相关免费在线工具 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