在现代微服务架构中,Spring Boot 已成为构建 RESTful API 的主流框架。然而,仅仅实现接口功能是不够的,生产环境中的接口需要考虑性能、稳定性、安全性和可维护性。本文将从性能优化、异常处理、安全防护和监控运维四个维度,详细阐述 Spring 接口调用的最佳实践。
一、性能优化:让接口飞起来
1.1 异步处理:提升吞吐量
同步阻塞是性能杀手。当接口需要调用多个下游服务时,使用异步处理可以显著提升吞吐量。
最佳实践:使用 CompletableFuture 进行并行调用
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
/**
* 同步方式 - 性能差
* 依次调用用户服务、商品服务、库存服务,总耗时 = 三个服务耗时之和
*/
@GetMapping("/sync/{orderId}")
public OrderDTO getOrderSync(@PathVariable String orderId) {
// 调用用户服务
UserDTO user = restTemplate.getForObject("http://user-service/users/" + orderId, UserDTO.class);
// 调用商品服务
ProductDTO product = restTemplate.getForObject("http://product-service/products/" + orderId, ProductDTO.class);
// 调用库存服务
StockDTO stock = restTemplate.getForObject("http://stock-service/stocks/" + orderId, StockDTO.class);
return OrderDTO.builder()
.user(user)
.product(product)
.stock(stock)
.build();
}
/**
* 异步方式 - 性能优
* 并行调用三个服务,总耗时 = 最慢的那个服务耗时
*/
@GetMapping("/async/{orderId}")
public CompletableFuture<OrderDTO> getOrderAsync(@PathVariable String orderId) {
// 创建线程池(实际项目中应该配置为Bean)
ExecutorService executor = Executors.newFixedThreadPool(10);
// 并行调用三个服务
CompletableFuture<UserDTO> userFuture = CompletableFuture.supplyAsync(() ->
restTemplate.getForObject("http://user-service/users/" + orderId, UserDTO.class), executor);
CompletableFuture<ProductDTO> productFuture = CompletableFuture.supplyAsync(() ->
restTemplate.getForObject("http://product-service/products/" + orderId, ProductDTO.class), executor);
CompletableFuture<StockDTO> stockFuture = CompletableFuture.supplyAsync(() ->
restTemplate.getForObject("http://stock-service/stocks/" + orderId, StockDTO.class), executor);
// 组合结果
return userFuture.thenCombineAsync(productFuture, (user, product) -> {
OrderDTO order = new OrderDTO();
order.setUser(user);
order.setProduct(product);
return order;
}, executor).thenCombineAsync(stockFuture, (order, stock) -> {
order.setStock(stock);
return order;
}, executor);
}
/**
* 使用 @Async 注解(推荐)
* 需要配置 @EnableAsync
*/
@Async("customExecutor")
public CompletableFuture<UserDTO> getUserAsync(String userId) {
return CompletableFuture.completedFuture(
restTemplate.getForObject("http://user-service/users/" + userId, UserDTO.class)
);
}
}
配置异步线程池:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "customExecutor")
public Executor customExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数:CPU核心数
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
// 最大线程数:核心线程数的2倍
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
// 队列容量
executor.setQueueCapacity(100);
// 线程名前缀
executor.setThreadNamePrefix("async-executor-");
// 拒绝策略:调用者运行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化
executor.initialize();
return executor;
}
}
1.2 缓存策略:减少重复计算
缓存是提升性能最有效的手段之一。Spring 提供了强大的缓存抽象。
最佳实践:多级缓存策略
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
/**
* 本地缓存(Caffeine)- 最快
* 最大容量1000,过期时间5分钟
*/
@Cacheable(value = "localProducts",
key = "#productId",
cacheManager = "localCacheManager")
public ProductDTO getProductLocal(String productId) {
log.info("从数据库查询产品: {}", productId);
return productRepository.findById(productId)
.map(this::convertToDTO)
.orElse(null);
}
/**
* 分布式缓存(Redis)- 中等
* 过期时间1小时
*/
@Cacheable(value = "redisProducts",
key = "#productId",
cacheManager = "redisCacheManager")
public ProductDTO getProductRedis(String productId) {
log.info("从数据库查询产品: {}", productId);
return productRepository.findById(productId)
.map(this::convertToDTO)
.orElse(null);
}
/**
* 缓存穿透保护:返回空值也缓存
*/
@Cacheable(value = "redisProducts",
key = "#productId",
unless = "#result == null",
cacheManager = "redisCacheManager")
public ProductDTO getProductWithNullCache(String productId) {
return productRepository.findById(productId)
.map(this::convertToDTO)
.orElse(null);
}
/**
* 缓存更新:更新数据库后删除缓存
*/
@CacheEvict(value = "redisProducts", key = "#product.id")
public void updateProduct(ProductDTO product) {
Product entity = convertToEntity(product);
productRepository.save(entity);
}
/**
* 批量查询:避免缓存击穿
*/
@Cacheable(value = "redisProducts",
key = "#productIds",
cacheManager = "redisCacheManager")
public List<ProductDTO> getProductsBatch(List<String> productIds) {
List<Product> products = productRepository.findAllById(productIds);
return products.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
}
缓存配置类:
@Configuration
@EnableCaching
public class CacheConfig {
/**
* 本地缓存配置(Caffeine)
*/
@Bean
public CacheManager localCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()); // 记录统计信息
return cacheManager;
}
/**
* Redis缓存配置
*/
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues(); // 不缓存null值
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
1.3 数据库查询优化
N+1 查询问题是性能杀手,需要特别注意。
最佳实践:使用 @EntityGraph 解决 N+1 问题
@Entity
public class Order {
@Id
private String id;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
}
public interface OrderRepository extends JpaRepository<Order, String> {
/**
* N+1问题:查询订单后,每个订单的items和user都需要额外查询
* 会导致:1 + N + N 次查询
*/
@Query("SELECT o FROM Order o WHERE o.id = :id")
Optional<Order> findByIdNPlus1(@Param("id") String id);
/**
* 解决方案1:使用 @EntityGraph
* 一次性加载所有关联数据
*/
@EntityGraph(attributePaths = {"items", "user"})
Optional<Order> findById(String id);
/**
* 解决方案2:使用 JOIN FETCH
*/
@Query("SELECT o FROM Order o JOIN FETCH o.items JOIN FETCH o.user WHERE o.id = :id")
Optional<Order> findByIdWithJoin(@Param("id") String id);
/**
* 解决方案3:批量查询
* 先查询订单列表,再批量查询关联数据
*/
@Query("SELECT o FROM Order o WHERE o.id IN :ids")
List<Order> findByIds(@Param("ids") List<String> ids);
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id IN :ids")
List<Order> findByIdsWithItems(@Param("ids") List<String> ids);
}
1.4 连接池优化
数据库连接池配置直接影响性能。
最佳实践:HikariCP 优化配置
# application.yml
spring:
datasource:
hikari:
# 连接池名称
pool-name: MyHikariCP
# 最小空闲连接数
minimum-idle: 10
# 最大连接数(根据业务调整,一般为CPU核心数*2)
maximum-pool-size: 50
# 连接超时时间(毫秒)
connection-timeout: 30000
# 连接最大生命周期(毫秒)
max-lifetime: 1800000
# 空闲连接存活时间(毫秒)
idle-timeout: 600000
# 连接测试查询
connection-test-query: SELECT 1
# 是否启用JMX
register-mbeans: true
# 连接泄漏检测
leak-detection-threshold: 60000
二、异常处理:优雅地处理错误
2.1 统一异常处理
避免在每个接口中重复写 try-catch,使用 @ControllerAdvice 统一处理。
最佳实践:全局异常处理器
/**
* 统一异常处理
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
ErrorResponse response = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.code(e.getCode())
.message(e.getMessage())
.path(getCurrentPath())
.build();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* 处理参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException e) {
log.warn("参数校验失败: {}", e.getMessage());
// 提取具体的字段错误信息
List<String> errors = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
ErrorResponse response = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.code("VALIDATION_ERROR")
.message("参数校验失败")
.details(errors)
.path(getCurrentPath())
.build();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* 处理HTTP客户端异常(如404、500等)
*/
@ExceptionHandler(HttpClientErrorException.class)
public ResponseEntity<ErrorResponse> handleHttpClientError(HttpClientErrorException e) {
log.error("HTTP客户端异常: {} - {}", e.getStatusCode(), e.getMessage());
ErrorResponse response = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.code("HTTP_CLIENT_ERROR")
.message("下游服务调用失败")
.details(Map.of(
"status", e.getStatusCode().toString(),
"body", e.getResponseBodyAsString()
))
.path(getCurrentPath())
.build();
return ResponseEntity.status(e.getStatusCode()).body(response);
}
/**
* 处理超时异常
*/
@ExceptionHandler(TimeoutException.class)
public ResponseEntity<ErrorResponse> handleTimeoutException(TimeoutException e) {
log.error("请求超时: {}", e.getMessage());
ErrorResponse response = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.code("TIMEOUT_ERROR")
.message("请求超时,请稍后重试")
.path(getCurrentPath())
.build();
return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).body(response);
}
/**
* 处理所有未预期的异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("系统异常: ", e);
ErrorResponse response = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.code("SYSTEM_ERROR")
.message("系统繁忙,请稍后重试")
.details(e.getMessage())
.path(getCurrentPath())
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
private String getCurrentPath() {
// 获取当前请求路径
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
return attributes.getRequest().getRequestURI();
}
return "unknown";
}
}
/**
* 业务异常类
*/
@Getter
public class BusinessException extends RuntimeException {
private final String code;
public BusinessException(String code, String message) {
super(message);
this.code = code;
}
public static BusinessException of(String code, String message) {
return new BusinessException(code, message);
}
}
/**
* 错误响应DTO
*/
@Data
@Builder
public class ErrorResponse {
private LocalDateTime timestamp;
private String code;
private String message;
private Object details;
private String path;
}
2.2 重试机制
对于网络抖动等临时性故障,使用重试机制可以提高成功率。
最佳实践:使用 Spring Retry
@Service
public class PaymentService {
/**
* 重试策略:
* - 最多重试3次
* - 间隔时间:1秒、2秒、4秒(指数退避)
* - 只对特定异常重试
*/
@Retryable(
value = {HttpClientErrorException.class, TimeoutException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 5000),
listeners = "retryListener"
)
public PaymentResult processPayment(PaymentRequest request) {
log.info("尝试处理支付: {}", request.getOrderId());
// 调用支付网关
return restTemplate.postForObject(
"http://payment-gateway/pay",
request,
PaymentResult.class
);
}
/**
* 重试失败后的降级处理
*/
@Recover
public PaymentResult recoverPayment(HttpClientErrorException e, PaymentRequest request) {
log.error("支付重试失败,进入降级处理: {}", request.getOrderId(), e);
// 记录失败订单
saveFailedOrder(request);
// 返回降级结果
return PaymentResult.builder()
.success(false)
.orderId(request.getOrderId())
.message("支付处理失败,请稍后重试")
.build();
}
/**
* 重试失败后的降级处理(TimeoutException)
*/
@Recover
public PaymentResult recoverPayment(TimeoutException e, PaymentRequest request) {
log.error("支付超时,进入降级处理: {}", request.getOrderId(), e);
// 发送延迟消息,异步重试
messageQueueService.sendDelayedMessage("payment-retry", request, 300);
return PaymentResult.builder()
.success(false)
.orderId(request.getOrderId())
.message("支付处理中,请稍后查询结果")
.build();
}
private void saveFailedOrder(PaymentRequest request) {
// 保存失败订单到数据库
}
}
/**
* 重试监听器
*/
@Component
public class RetryListener implements RetryListener {
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
return true;
}
@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
// 重试结束
}
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
log.warn("重试失败: 第{}次,异常: {}", context.getRetryCount(), throwable.getClass().getSimpleName());
}
}
/**
* 启用重试
*/
@SpringBootApplication
@EnableRetry
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2.3 熔断器:防止级联故障
当下游服务不可用时,熔断器可以快速失败,防止故障扩散。
最佳实践:使用 Resilience4j
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
/**
* 熔断器配置
* - 失败率超过50%时熔断
* - 最小调用次数10次
* - 熔断持续时间30秒
* - 半开状态允许1次试探调用
*/
@CircuitBreaker(name = "orderService", fallbackMethod = "createOrderFallback")
@RateLimiter(name = "orderService", fallbackMethod = "rateLimitFallback")
@Retry(name = "orderService", fallbackMethod = "retryFallback")
public OrderDTO createOrder(OrderRequest request) {
// 调用库存服务检查库存
StockDTO stock = restTemplate.getForObject(
"http://stock-service/check/" + request.getProductId(),
StockDTO.class
);
if (stock == null || stock.getQuantity() < request.getQuantity()) {
throw new BusinessException("INSUFFICIENT_STOCK", "库存不足");
}
// 创建订单
OrderDTO order = OrderDTO.builder()
.id(UUID.randomUUID().toString())
.productId(request.getProductId())
.quantity(request.getQuantity())
.status("CREATED")
.build();
// 异步扣减库存
CompletableFuture.runAsync(() -> {
restTemplate.postForObject(
"http://stock-service/deduct",
DeductRequest.builder()
.productId(request.getProductId())
.quantity(request.getQuantity())
.build(),
Void.class
);
});
return order;
}
/**
* 熔断降级
*/
public OrderDTO createOrderFallback(OrderRequest request, Exception e) {
log.error("订单创建熔断降级: {}", request.getOrderId(), e);
// 返回缓存的默认订单或排队处理
return OrderDTO.builder()
.id(request.getOrderId())
.status("PENDING")
.message("系统繁忙,订单已排队处理")
.build();
}
/**
* 限流降级
*/
public OrderDTO rateLimitFallback(OrderRequest request, Exception e) {
log.warn("请求被限流: {}", request.getOrderId());
throw new BusinessException("RATE_LIMIT", "请求过于频繁,请稍后重试");
}
/**
* 重试降级
*/
public OrderDTO retryFallback(OrderRequest request, Exception e) {
log.error("重试失败: {}", request.getOrderId(), e);
// 发送到消息队列异步处理
messageQueueService.send("order-queue", request);
return OrderDTO.builder()
.id(request.getOrderId())
.status("QUEUED")
.message("订单已加入队列异步处理")
.build();
}
}
/**
* 熔断器配置类
*/
@Configuration
public class Resilience4jConfig {
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> circuitBreakerCustomizer() {
return factory -> factory.configure(builder -> builder
.circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()
.toBuilder()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.permittedNumberOfCallsInHalfOpenState(1)
.slidingWindowSize(10)
.build()),
"orderService");
}
@Bean
public Customizer<Resilience4JRateLimiterFactory> rateLimiterCustomizer() {
return factory -> factory.configure(builder -> builder
.rateLimiterConfig(RateLimiterConfig.ofDefaults()
.toBuilder()
.limitForPeriod(10) // 每秒10个请求
.limitRefreshPeriod(Duration.ofSeconds(1))
.timeoutDuration(Duration.ofMillis(100))
.build()),
"orderService");
}
}
三、安全防护:保护你的接口
3.1 请求签名验证
防止请求被篡改,确保请求的完整性。
最佳实践:HMAC 签名验证
/**
* 请求签名验证拦截器
*/
@Component
public class SignatureInterceptor implements HandlerInterceptor {
private static final String SIGNATURE_HEADER = "X-Signature";
private static final String TIMESTAMP_HEADER = "X-Timestamp";
private static final String NONCE_HEADER = "X-Nonce";
@Value("${api.secret-key}")
private String secretKey;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求参数
String signature = request.getHeader(SIGNATURE_HEADER);
String timestamp = request.getHeader(TIMESTAMP_HEADER);
String nonce = request.getHeader(NONCE_HEADER);
// 1. 验证时间戳(防止重放攻击)
if (!validateTimestamp(timestamp)) {
throw new SecurityException("INVALID_TIMESTAMP", "时间戳无效");
}
// 2. 验证nonce(防止重放攻击)
if (!validateNonce(nonce)) {
throw new SecurityException("INVALID_NONCE", "nonce无效");
}
// 3. 验证签名
String calculatedSignature = calculateSignature(request, timestamp, nonce);
if (!calculatedSignature.equals(signature)) {
throw new SecurityException("INVALID_SIGNATURE", "签名验证失败");
}
return true;
}
private boolean validateTimestamp(String timestamp) {
try {
long requestTime = Long.parseLong(timestamp);
long currentTime = System.currentTimeMillis() / 1000;
// 时间戳误差不超过5分钟
return Math.abs(currentTime - requestTime) <= 300;
} catch (Exception e) {
return false;
}
}
private boolean validateNonce(String nonce) {
// 使用Redis检查nonce是否已使用(5分钟内)
String key = "nonce:" + nonce;
Boolean exists = redisTemplate.hasKey(key);
if (Boolean.TRUE.equals(exists)) {
return false;
}
// 设置5分钟过期
redisTemplate.opsForValue().set(key, "1", 5, TimeUnit.MINUTES);
return true;
}
private String calculateSignature(HttpServletRequest request, String timestamp, String nonce) throws Exception {
// 获取请求体
String body = getRequestBody(request);
// 构建签名字符串:secretKey + timestamp + nonce + body
String data = secretKey + timestamp + nonce + body;
// 使用HMAC-SHA256计算签名
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
}
private String getRequestBody(HttpServletRequest request) throws IOException {
if (request.getMethod().equalsIgnoreCase("GET")) {
// GET请求使用查询参数
Map<String, String[]> parameterMap = request.getParameterMap();
return parameterMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(e -> e.getKey() + "=" + String.join(",", e.getValue()))
.collect(Collectors.joining("&"));
} else {
// POST/PUT请求使用请求体
try (BufferedReader reader = request.getReader()) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
}
}
}
/**
* 注册拦截器
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private SignatureInterceptor signatureInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(signatureInterceptor)
.addPathPatterns("/api/secure/**")
.excludePathPatterns("/api/public/**");
}
}
3.2 接口限流
防止接口被滥用,保护系统资源。
最佳实践:使用 Redis + Lua 脚本实现分布式限流
/**
* 分布式限流器
*/
@Component
public class DistributedRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 使用Lua脚本保证原子性
* 限流算法:令牌桶
*/
private static final String LUA_SCRIPT =
"local key = KEYS[1] " +
"local limit = tonumber(ARGV[1]) " +
"local expire = tonumber(ARGV[2]) " +
"local current = tonumber(redis.call('GET', key) or '0') " +
"if current + 1 > limit then " +
" return 0 " +
"else " +
" redis.call('INCR', key) " +
" if current == 0 then " +
" redis.call('EXPIRE', key, expire) " +
" end " +
" return 1 " +
"end";
/**
* 尝试获取令牌
* @param key 限流键(如:userId或IP)
* @param limit 限制数量
* @param expire 过期时间(秒)
* @return true-获取成功,false-获取失败
*/
public boolean tryAcquire(String key, int limit, int expire) {
Long result = redisTemplate.execute(
new DefaultRedisScript<>(LUA_SCRIPT, Long.class),
Collections.singletonList("rate_limit:" + key),
String.valueOf(limit),
String.valueOf(expire)
);
return result != null && result == 1;
}
/**
* 获取剩余令牌数
*/
public int getRemaining(String key) {
String value = redisTemplate.opsForValue().get("rate_limit:" + key);
return value != null ? Integer.parseInt(value) : 0;
}
/**
* 重置限流
*/
public void reset(String key) {
redisTemplate.delete("rate_limit:" + key);
}
}
/**
* 接口限流注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
/**
* 限流标识(支持SpEL表达式)
*/
String key() default "";
/**
* 限制数量
*/
int limit() default 60;
/**
* 时间窗口(秒)
*/
int timeWindow() default 60;
/**
* 限流类型:IP、USER、CUSTOM
*/
LimitType type() default LimitType.IP;
}
public enum LimitType {
IP, // 基于IP限流
USER, // 基于用户ID限流
CUSTOM // 自定义key
}
/**
* AOP实现限流
*/
@Aspect
@Component
@Slf4j
public class RateLimitAspect {
@Autowired
private DistributedRateLimiter rateLimiter;
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
// 构建限流key
String key = buildKey(rateLimit, joinPoint);
// 尝试获取令牌
boolean allowed = rateLimiter.tryAcquire(key, rateLimit.limit(), rateLimit.timeWindow());
if (!allowed) {
log.warn("限流触发: {}", key);
throw new BusinessException("RATE_LIMIT_EXCEEDED", "请求过于频繁,请稍后重试");
}
log.debug("限流通过: {}", key);
return joinPoint.proceed();
}
private String buildKey(RateLimit rateLimit, ProceedingJoinPoint joinPoint) {
StringBuilder key = new StringBuilder();
// 根据类型构建key
switch (rateLimit.type()) {
case IP:
key.append("IP:").append(getIpAddress());
break;
case USER:
key.append("USER:").append(getCurrentUserId());
break;
case CUSTOM:
// 解析SpEL表达式
key.append(rateLimit.key());
break;
}
return key.toString();
}
private String getIpAddress() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
}
return ip;
}
return "unknown";
}
private String getCurrentUserId() {
// 从SecurityContext或Token中获取用户ID
return "mock_user_id";
}
}
/**
* 使用示例
*/
@RestController
@RequestMapping("/api")
public class ApiController {
@Autowired
private DistributedRateLimiter rateLimiter;
/**
* 限制每个IP每分钟最多60次请求
*/
@RateLimit(type = LimitType.IP, limit = 60, timeWindow = 60)
@GetMapping("/public/data")
public ResponseEntity<String> getData() {
return ResponseEntity.ok("success");
}
/**
* 限制每个用户每秒最多10次请求
*/
@RateLimit(type = LimitType.USER, limit = 10, timeWindow = 1)
@PostMapping("/user/action")
public ResponseEntity<String> userAction() {
return ResponseEntity.ok("success");
}
/**
* 自定义限流key(基于用户ID+接口)
*/
@RateLimit(key = "#userId + ':' + 'order_create'", limit = 5, timeWindow = 60)
@PostMapping("/order/create")
public ResponseEntity<String> createOrder(@RequestParam String userId) {
return ResponseEntity.ok("success");
}
}
3.3 输入参数校验
严格的输入校验是安全的第一道防线。
最佳实践:使用 Bean Validation + 自定义校验器
/**
* 创建订单请求
*/
@Data
public class OrderCreateRequest {
@NotBlank(message = "用户ID不能为空")
@Size(min = 1, max = 50, message = "用户ID长度必须在1-50之间")
private String userId;
@NotBlank(message = "产品ID不能为空")
private String productId;
@Min(value = 1, message = "数量必须大于0")
@Max(value = 1000, message = "单次最多购买1000件")
private Integer quantity;
@Email(message = "邮箱格式不正确")
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@DecimalMin(value = "0.01", message = "金额必须大于0")
@DecimalMax(value = "999999.99", message = "金额不能超过999999.99")
private BigDecimal amount;
@Future(message = "期望送达时间必须是未来时间")
private LocalDateTime expectedDeliveryTime;
@Valid
private Address address;
@Size(min = 1, max = 10, message = "商品规格最多选择10项")
private List<@Valid ProductSpec> specs;
}
/**
* 地址对象
*/
@Data
public class Address {
@NotBlank(message = "省不能为空")
private String province;
@NotBlank(message = "市不能为空")
private String city;
@NotBlank(message = "详细地址不能为空")
@Size(max = 200, message = "详细地址不能超过200字")
private String detail;
@Pattern(regexp = "\\d{6}", message = "邮政编码格式不正确")
private String zipCode;
}
/**
* 商品规格
*/
@Data
public class ProductSpec {
@NotBlank(message = "规格名称不能为空")
private String name;
@NotBlank(message = "规格值不能为空")
private String value;
}
/**
* 自定义校验注解:检查库存
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = StockValidator.class)
public @interface ValidStock {
String message() default "库存不足";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
/**
* 自定义校验器
*/
public class StockValidator implements ConstraintValidator<ValidStock, OrderCreateRequest> {
@Autowired
private StockService stockService;
@Override
public boolean isValid(OrderCreateRequest request, ConstraintValidatorContext context) {
if (request == null || request.getProductId() == null) {
return false;
}
// 调用库存服务检查
StockDTO stock = stockService.checkStock(request.getProductId());
if (stock == null || stock.getQuantity() < request.getQuantity()) {
// 自定义错误消息
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
"产品" + request.getProductId() + "库存不足,当前库存:" +
(stock != null ? stock.getQuantity() : 0)
).addConstraintViolation();
return false;
}
return true;
}
}
/**
* 控制器中使用校验
*/
@RestController
@RequestMapping("/api/orders")
public class OrderController {
/**
* 使用 @Valid 触发校验
*/
@PostMapping
public ResponseEntity<OrderDTO> createOrder(@Valid @RequestBody OrderCreateRequest request) {
// 校验通过,执行业务逻辑
OrderDTO order = orderService.create(request);
return ResponseEntity.ok(order);
}
/**
* 分组校验:不同场景使用不同校验规则
*/
@PutMapping("/{id}")
public ResponseEntity<OrderDTO> updateOrder(
@PathVariable String id,
@Validated(UpdateGroup.class) @RequestBody OrderCreateRequest request) {
// 更新逻辑
return ResponseEntity.ok(orderService.update(id, request));
}
}
/**
* 校验分组接口
*/
public interface UpdateGroup {
}
3.4 防止 SQL 注入和 XSS 攻击
最佳实践:使用预编译语句和参数化查询
/**
* 正确的数据库操作方式
*/
@Repository
public class UserRepository {
@PersistenceContext
private EntityManager entityManager;
/**
* 错误示例:字符串拼接,存在SQL注入风险
*/
public List<User> findUsersByKeywordWrong(String keyword) {
// 危险!存在SQL注入风险
String sql = "SELECT * FROM user WHERE name LIKE '%" + keyword + "%'";
return entityManager.createNativeQuery(sql, User.class).getResultList();
}
/**
* 正确示例:使用参数化查询
*/
public List<User> findUsersByKeywordCorrect(String keyword) {
// 安全!使用参数化查询
String jpql = "SELECT u FROM User u WHERE u.name LIKE :keyword";
return entityManager.createQuery(jpql, User.class)
.setParameter("keyword", "%" + keyword + "%")
.getResultList();
}
/**
* 使用原生SQL的正确方式
*/
public List<User> findUsersByKeywordNative(String keyword) {
Query query = entityManager.createNativeQuery(
"SELECT * FROM user WHERE name LIKE :keyword",
User.class
);
query.setParameter("keyword", "%" + keyword + "%");
return query.getResultList();
}
}
/**
* 防止XSS攻击:输入过滤和输出转义
*/
@Component
public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response);
}
}
/**
* XSS包装器:过滤请求参数
*/
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return xssClean(value);
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) return null;
String[] cleanValues = new String[values.length];
for (int i = 0; i < values.length; i++) {
cleanValues[i] = xssClean(values[i]);
}
return cleanValues;
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> map = super.getParameterMap();
Map<String, String[]> cleanMap = new HashMap<>();
for (Map.Entry<String, String[]> entry : map.entrySet()) {
String[] values = entry.getValue();
String[] cleanValues = new String[values.length];
for (int i = 0; i < values.length; i++) {
cleanValues[i] = xssClean(values[i]);
}
cleanMap.put(entry.getKey(), cleanValues);
}
return cleanMap;
}
/**
* XSS过滤:使用正则表达式移除危险标签
*/
private String xssClean(String value) {
if (value == null) {
return null;
}
// 移除脚本标签
value = value.replaceAll("<script[^>]*>([\\s\\S]*?)</script>", "");
value = value.replaceAll("<style[^>]*>([\\s\\S]*?)</style>", "");
// 移除事件处理器
value = value.replaceAll("on\\w+\\s*=", "");
// 转义特殊字符
value = value.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
return value;
}
}
/**
* 输出时的XSS防护(JSON响应)
*/
@RestControllerAdvice
public class XssResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> converterType,
HttpServletRequest request, HttpServletResponse response) {
if (body instanceof String) {
return xssClean((String) body);
}
// 对于复杂对象,可以使用Jackson的Serializer
return body;
}
private String xssClean(String value) {
if (value == null) return null;
return value.replace("<", "<").replace(">", ">");
}
}
四、监控与运维:让系统可观测
4.1 请求日志记录
记录详细的请求日志,便于问题排查。
最佳实践:使用 MDC 和切面记录日志
/**
* 请求日志切面
*/
@Aspect
@Component
@Slf4j
public class RequestLogAspect {
private static final String REQUEST_ID = "requestId";
private static final String START_TIME = "startTime";
/**
* 切点:所有Controller方法
*/
@Around("execution(* com.example.controller..*.*(..))")
public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取请求信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 生成请求ID
String requestId = generateRequestId();
// 设置MDC(用于日志追踪)
MDC.put(REQUEST_ID, requestId);
MDC.put("method", request.getMethod());
MDC.put("path", request.getRequestURI());
MDC.put("clientIp", getClientIp(request));
long startTime = System.currentTimeMillis();
MDC.put(START_TIME, String.valueOf(startTime));
// 记录请求参数
log.info("请求开始: {} {} {}",
request.getMethod(),
request.getRequestURI(),
getParameters(request));
try {
// 执行原方法
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
log.info("请求成功: {} {} - 耗时: {}ms",
request.getMethod(),
request.getRequestURI(),
duration);
return result;
} catch (Exception e) {
long duration = System.currentTimeMillis() - startTime;
log.error("请求失败: {} {} - 耗时: {}ms - 错误: {}",
request.getMethod(),
request.getRequestURI(),
duration,
e.getMessage(),
e);
throw e;
} finally {
// 清理MDC
MDC.clear();
}
}
private String generateRequestId() {
return UUID.randomUUID().toString().replace("-", "");
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
}
return ip;
}
private String getParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
if (parameterMap.isEmpty()) {
return "";
}
return parameterMap.entrySet().stream()
.map(e -> e.getKey() + "=" + String.join(",", e.getValue()))
.collect(Collectors.joining("&"));
}
}
/**
* 异步日志配置(logback-spring.xml)
*/
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 异步日志 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>10000</queueSize>
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="FILE"/>
</appender>
<!-- 文件日志 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{requestId}] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC"/>
</root>
</configuration>
4.2 指标监控
使用 Micrometer 收集性能指标。
最佳实践:自定义指标和健康检查
/**
* 自定义指标收集
*/
@Component
public class CustomMetrics {
private final MeterRegistry meterRegistry;
// 计数器:订单创建数量
private Counter orderCreatedCounter;
// 计时器:订单处理耗时
private Timer orderProcessTimer;
// 仪表:当前活跃连接数
private AtomicInteger activeConnections;
public CustomMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
initMetrics();
}
private void initMetrics() {
// 初始化计数器
orderCreatedCounter = Counter.builder("order.created")
.description("订单创建数量")
.tag("service", "order-service")
.register(meterRegistry);
// 初始化计时器
orderProcessTimer = Timer.builder("order.process.time")
.description("订单处理耗时")
.tag("service", "order-service")
.publishPercentiles(0.5, 0.95, 0.99) // 百分位
.register(meterRegistry);
// 初始化仪表
activeConnections = new AtomicInteger(0);
Gauge.builder("connection.active", activeConnections, AtomicInteger::get)
.description("当前活跃连接数")
.register(meterRegistry);
}
public void recordOrderCreated() {
orderCreatedCounter.increment();
}
public <T> T recordOrderProcess(Supplier<T> supplier) {
return orderProcessTimer.record(supplier);
}
public void incrementActiveConnection() {
activeConnections.incrementAndGet();
}
public void decrementActiveConnection() {
activeConnections.decrementAndGet();
}
}
/**
* 自定义健康检查
*/
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Autowired
private DataSource dataSource;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public Health health() {
Health.Builder builder = Health.up();
// 检查数据库
try (Connection connection = dataSource.getConnection()) {
connection.createStatement().execute("SELECT 1");
builder.withDetail("database", "UP");
} catch (Exception e) {
builder.down().withDetail("database", "DOWN").withException(e);
}
// 检查Redis
try {
redisTemplate.getConnectionFactory().getConnection().ping();
builder.withDetail("redis", "UP");
} catch (Exception e) {
builder.down().withDetail("redis", "DOWN").withException(e);
}
// 检查磁盘空间
File root = new File("/");
long freeSpace = root.getFreeSpace();
long totalSpace = root.getTotalSpace();
double usedPercent = 100.0 * (totalSpace - freeSpace) / totalSpace;
if (usedPercent > 90) {
builder.status("WARNING").withDetail("disk", "磁盘空间不足90%");
} else {
builder.withDetail("disk", "OK");
}
return builder.build();
}
}
/**
* 应用配置
*/
@Configuration
public class ActuatorConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config()
.commonTags("application", "order-service")
.commonTags("environment", System.getProperty("spring.profiles.active", "dev"));
}
}
4.3 接口文档
使用 Swagger/OpenAPI 自动生成接口文档。
最佳实践:详细的文档注解
/**
* 订单接口文档
*/
@RestController
@RequestMapping("/api/v1/orders")
@Tag(name = "订单管理", description = "订单相关接口")
public class OrderApiDocController {
@Autowired
private OrderService orderService;
@Operation(
summary = "创建订单",
description = "根据用户ID和产品信息创建订单,支持批量购买",
responses = {
@ApiResponse(responseCode = "200", description = "订单创建成功",
content = @Content(schema = @Schema(implementation = OrderDTO.class))),
@ApiResponse(responseCode = "400", description = "参数错误或库存不足",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "429", description = "请求过于频繁",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
}
)
@PostMapping
@RateLimit(type = LimitType.USER, limit = 10, timeWindow = 60)
public ResponseEntity<OrderDTO> createOrder(
@RequestBody @Valid OrderCreateRequest request,
@RequestHeader(value = "X-Signature", required = false) String signature,
@RequestHeader(value = "X-Timestamp") String timestamp) {
OrderDTO order = orderService.create(request);
return ResponseEntity.ok(order);
}
@Operation(
summary = "查询订单详情",
description = "根据订单ID查询订单详细信息,包含商品和用户信息",
parameters = {
@Parameter(name = "id", description = "订单ID", required = true, example = "ORD-20240101-001")
}
)
@GetMapping("/{id}")
public ResponseEntity<OrderDTO> getOrder(
@PathVariable @Pattern(regexp = "^ORD-\\d{8}-\\d{3}$", message = "订单ID格式不正确") String id) {
OrderDTO order = orderService.findById(id)
.orElseThrow(() -> new BusinessException("ORDER_NOT_FOUND", "订单不存在"));
return ResponseEntity.ok(order);
}
@Operation(
summary = "分页查询订单列表",
description = "支持按状态、时间范围分页查询订单"
)
@GetMapping
public ResponseEntity<Page<OrderDTO>> listOrders(
@RequestParam(defaultValue = "1") @Min(1) Integer page,
@RequestParam(defaultValue = "10") @Min(1) @Max(100) Integer size,
@RequestParam(required = false) String status,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) {
Pageable pageable = PageRequest.of(page - 1, size, Sort.by("createTime").descending());
Page<OrderDTO> result = orderService.list(status, startDate, endDate, pageable);
return ResponseEntity.ok(result);
}
}
Swagger 配置:
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("订单服务 API")
.version("1.0")
.description("订单管理服务接口文档")
.contact(new Contact()
.name("技术团队")
.email("tech@example.com")
.url("https://example.com"))
.license(new License()
.name("Apache 2.0")
.url("http://springdoc.org")));
}
}
五、配置最佳实践
5.1 配置管理
最佳实践:使用 @ConfigurationProperties 进行类型安全的配置
/**
* API配置类
*/
@Configuration
@ConfigurationProperties(prefix = "app.api")
@Data
public class ApiConfigProperties {
/**
* 超时配置
*/
private Timeout timeout = new Timeout();
/**
* 重试配置
*/
private Retry retry = new Retry();
/**
* 限流配置
*/
private RateLimit rateLimit = new RateLimit();
/**
* 缓存配置
*/
private Cache cache = new Cache();
@Data
public static class Timeout {
private int connect = 5000;
private int read = 10000;
private int write = 5000;
}
@Data
public static class Retry {
private int maxAttempts = 3;
private long backoff = 1000;
private double multiplier = 2.0;
}
@Data
public static class RateLimit {
private int defaultLimit = 60;
private int defaultWindow = 60;
private Map<String, Integer> customLimits = new HashMap<>();
}
@Data
public static class Cache {
private long localTtl = 300;
private long redisTtl = 3600;
private int localMaxSize = 1000;
}
}
/**
* 使用配置
*/
@Service
public class ConfigDrivenService {
@Autowired
private ApiConfigProperties config;
public void doSomething() {
// 使用配置
int connectTimeout = config.getTimeout().getConnect();
int retryAttempts = config.getRetry().getMaxAttempts();
// 自定义限流规则
String customKey = "order_create";
Integer customLimit = config.getRateLimit().getCustomLimits().get(customKey);
if (customLimit == null) {
customLimit = config.getRateLimit().getDefaultLimit();
}
}
}
application.yml 配置示例:
app:
api:
timeout:
connect: 5000
read: 10000
write: 5000
retry:
maxAttempts: 3
backoff: 1000
multiplier: 2.0
rate-limit:
defaultLimit: 60
defaultWindow: 60
customLimits:
order_create: 10
payment_submit: 5
cache:
localTtl: 300
redisTtl: 3600
localMaxSize: 1000
5.2 多环境配置
最佳实践:使用 Spring Profile 和配置文件分离
# application.yml(公共配置)
spring:
application:
name: order-service
profiles:
active: dev
---
# 开发环境
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:mysql://localhost:3306/order_dev?useSSL=false
username: dev_user
password: dev_pass
redis:
host: localhost
port: 6379
app:
api:
timeout:
connect: 3000
rate-limit:
defaultLimit: 1000
logging:
level:
com.example: DEBUG
---
# 生产环境
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:mysql://mysql-cluster:3306/order_prod?useSSL=true&serverTimezone=UTC
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 50
connection-timeout: 30000
redis:
host: redis-cluster
port: 6379
password: ${REDIS_PASSWORD}
ssl: true
app:
api:
timeout:
connect: 5000
read: 15000
rate-limit:
defaultLimit: 60
logging:
level:
com.example: INFO
file:
name: /var/log/order-service/app.log
六、测试最佳实践
6.1 单元测试
最佳实践:使用 JUnit 5 + Mockito
@SpringBootTest
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private RestTemplate restTemplate;
@Mock
private DistributedRateLimiter rateLimiter;
@InjectMocks
private OrderService orderService;
@Test
@DisplayName("创建订单成功")
void createOrderSuccess() {
// Given
OrderCreateRequest request = new OrderCreateRequest();
request.setUserId("U001");
request.setProductId("P001");
request.setQuantity(2);
// Mock库存检查
StockDTO stock = new StockDTO();
stock.setQuantity(100);
when(restTemplate.getForObject(
"http://stock-service/check/P001",
StockDTO.class
)).thenReturn(stock);
// Mock限流检查
when(rateLimiter.tryAcquire(anyString(), anyInt(), anyInt())).thenReturn(true);
// Mock保存订单
Order savedOrder = new Order();
savedOrder.setId("ORD-001");
when(orderRepository.save(any(Order.class))).thenReturn(savedOrder);
// When
OrderDTO result = orderService.create(request);
// Then
assertNotNull(result);
assertEquals("ORD-001", result.getId());
verify(orderRepository, times(1)).save(any(Order.class));
verify(restTemplate, times(1)).getForObject(anyString(), any());
}
@Test
@DisplayName("库存不足时抛出异常")
void createOrderInsufficientStock() {
// Given
OrderCreateRequest request = new OrderCreateRequest();
request.setProductId("P001");
request.setQuantity(1000);
StockDTO stock = new StockDTO();
stock.setQuantity(10);
when(restTemplate.getForObject(anyString(), any())).thenReturn(stock);
// When & Then
assertThrows(BusinessException.class, () -> {
orderService.create(request);
});
}
@Test
@DisplayName("异步创建订单")
void createOrderAsync() throws Exception {
// Given
OrderCreateRequest request = new OrderCreateRequest();
// Mock异步操作
CompletableFuture<OrderDTO> future = CompletableFuture.supplyAsync(() -> {
OrderDTO dto = new OrderDTO();
dto.setId("ASYNC-001");
return dto;
});
when(orderRepository.saveAsync(any())).thenReturn(future);
// When
CompletableFuture<OrderDTO> result = orderService.createAsync(request);
// Then
OrderDTO order = result.get(5, TimeUnit.SECONDS);
assertEquals("ASYNC-001", order.getId());
}
}
6.2 集成测试
最佳实践:使用 Testcontainers 进行真实环境测试
@SpringBootTest
@Testcontainers
class OrderIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
registry.add("spring.redis.host", redis::getHost);
registry.add("spring.redis.port", redis::getFirstMappedPort()::toString);
}
@Autowired
private TestRestTemplate restTemplate;
@Test
@DisplayName("完整流程集成测试")
void fullIntegrationTest() {
// 1. 创建订单
OrderCreateRequest request = new OrderCreateRequest();
request.setUserId("U001");
request.setProductId("P001");
request.setQuantity(1);
ResponseEntity<OrderDTO> createResponse = restTemplate.postForEntity(
"/api/orders",
request,
OrderDTO.class
);
assertEquals(HttpStatus.OK, createResponse.getStatusCode());
assertNotNull(createResponse.getBody());
String orderId = createResponse.getBody().getId();
// 2. 查询订单
ResponseEntity<OrderDTO> getResponse = restTemplate.getForEntity(
"/api/orders/" + orderId,
OrderDTO.class
);
assertEquals(HttpStatus.OK, getResponse.getStatusCode());
assertEquals(orderId, getResponse.getBody().getId());
// 3. 验证缓存
// 再次查询应该走缓存
long start = System.currentTimeMillis();
restTemplate.getForEntity("/api/orders/" + orderId, OrderDTO.class);
long duration = System.currentTimeMillis() - start;
// 缓存命中应该非常快(< 10ms)
assertTrue(duration < 10);
}
}
七、总结
Spring 接口调用的最佳实践是一个系统工程,需要从多个维度综合考虑:
性能优化要点
- 异步处理:使用 CompletableFuture 或 @Async 提升吞吐量
- 缓存策略:多级缓存(本地 + 分布式),防止缓存穿透、击穿、雪崩
- 数据库优化:解决 N+1 问题,合理使用连接池
- 批量处理:减少网络调用次数
异常处理要点
- 统一异常处理:使用 @ControllerAdvice 集中处理
- 重试机制:对临时性故障进行重试(指数退避)
- 熔断降级:防止级联故障,快速失败
- 优雅降级:提供备选方案,保证核心功能可用
安全防护要点
- 请求签名:防止请求被篡改
- 接口限流:保护系统资源,防止滥用
- 参数校验:严格的输入验证
- 安全防护:防止 SQL 注入、XSS 攻击
监控运维要点
- 日志追踪:使用 MDC 实现请求链路追踪
- 指标监控:收集关键指标,设置告警
- 健康检查:实时监控系统状态
- 文档完善:自动生成接口文档
通过遵循这些最佳实践,可以构建出高性能、高可用、安全可靠的 Spring 接口服务。记住,最佳实践不是一成不变的,需要根据具体业务场景和系统规模进行调整和优化。
