您好,登录后才能下订单哦!
# 在SpringBoot中缓存HTTP请求响应体的方法
## 引言
在现代Web应用开发中,性能优化是一个永恒的话题。HTTP请求响应体的缓存作为提升系统性能的重要手段,能够显著减少重复计算、降低数据库压力并加快客户端响应速度。SpringBoot作为Java生态中最流行的微服务框架,提供了丰富的缓存支持。
本文将深入探讨在SpringBoot应用中实现HTTP请求响应体缓存的完整方案,涵盖从基础概念到高级实现的各个层面,帮助开发者构建高性能的Web服务。
## 一、HTTP缓存基础概念
### 1.1 什么是HTTP缓存
HTTP缓存是指将请求的响应内容存储在中间介质(内存、磁盘等)中,当相同请求再次发生时直接返回已存储的内容,而非重新执行完整处理流程的技术。
```java
// 典型缓存流程示例
if (缓存中存在请求结果) {
return 缓存结果;
} else {
执行业务逻辑;
存储结果到缓存;
return 结果;
}
缓存类型 | 存储位置 | 速度 | 容量限制 | 典型应用场景 |
---|---|---|---|---|
浏览器缓存 | 客户端 | 最快 | 小 | 静态资源缓存 |
CDN缓存 | 边缘节点 | 快 | 大 | 全局内容分发 |
反向代理缓存 | 服务端前置 | 较快 | 较大 | 全页缓存 |
应用层缓存 | 应用内存 | 极快 | 较小 | 动态数据缓存 |
分布式缓存 | 独立服务 | 快 | 大 | 共享数据缓存 |
Spring框架提供了统一的缓存抽象层,通过org.springframework.cache.Cache
和org.springframework.cache.CacheManager
接口支持多种缓存实现。
核心注解:
- @Cacheable
:标记可缓存方法
- @CacheEvict
:清除缓存项
- @CachePut
:更新缓存而不干扰方法执行
- @Caching
:组合多个缓存操作
- @CacheConfig
:类级别共享缓存配置
SpringBoot支持多种缓存实现,通过简单配置即可切换:
Caffeine(推荐):
@Bean
public CaffeineCacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000));
return cacheManager;
}
EhCache:
<!-- ehcache.xml -->
<cache name="responseCache"
maxEntriesLocalHeap="1000"
timeToLiveSeconds="600"/>
Redis(分布式场景):
spring:
cache:
type: redis
redis:
time-to-live: 600000
key-prefix: "CACHE_"
use-key-prefix: true
推荐配置组合:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager() {
@Override
protected Cache createConcurrentMapCache(String name) {
return new ConcurrentMapCache(name,
Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.maximumSize(1000)
.build().asMap(),
false);
}
};
return cacheManager;
}
@Bean
public KeyGenerator customKeyGenerator() {
return (target, method, params) -> {
StringBuilder key = new StringBuilder();
key.append(target.getClass().getSimpleName());
key.append(".");
key.append(method.getName());
for (Object param : params) {
if (param != null) {
key.append("_");
if (param instanceof HttpServletRequest) {
key.append("req_")
.append(((HttpServletRequest) param).getRequestURI());
} else {
key.append(param.toString());
}
}
}
return key.toString();
};
}
}
最直接的实现方式是在Controller方法上添加缓存注解:
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping("/{id}")
@Cacheable(value = "productResponses", key = "#id")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
Product product = productService.findById(id);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.MINUTES))
.eTag(product.getVersion().toString())
.body(product);
}
}
对于统一格式的响应,可以实现ResponseBodyAdvice
进行全局处理:
@ControllerAdvice
public class CachingResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private CacheManager cacheManager;
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.hasMethodAnnotation(Cacheable.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
Cacheable cacheable = returnType.getMethodAnnotation(Cacheable.class);
if (cacheable != null) {
String cacheName = cacheable.value()[0];
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
String key = generateKey(request, returnType);
cache.put(key, body);
// 设置HTTP缓存头
if (response instanceof ServletServerHttpResponse) {
HttpServletResponse servletResponse =
((ServletServerHttpResponse) response).getServletResponse();
servletResponse.setHeader("Cache-Control", "max-age=1800");
}
}
}
return body;
}
private String generateKey(ServerHttpRequest request, MethodParameter returnType) {
// 实现自定义key生成逻辑
}
}
对于更底层的控制,可以创建缓存Filter:
public class CachingFilter extends OncePerRequestFilter {
@Autowired
private CacheManager cacheManager;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
String cacheKey = generateCacheKey(request);
Cache cache = cacheManager.getCache("httpResponses");
if (cache != null) {
CachedResponse cachedResponse = cache.get(cacheKey, CachedResponse.class);
if (cachedResponse != null) {
writeCachedResponse(cachedResponse, response);
return;
}
}
ContentCachingResponseWrapper responseWrapper =
new ContentCachingResponseWrapper(response);
filterChain.doFilter(request, responseWrapper);
if (cache != null && response.getStatus() == 200) {
CachedResponse responseToCache = new CachedResponse(
responseWrapper.getContentAsByteArray(),
response.getContentType(),
response.getHeader("ETag"));
cache.put(cacheKey, responseToCache);
}
responseWrapper.copyBodyToResponse();
}
// 辅助方法实现...
}
在微服务架构中,通常需要Redis等分布式缓存:
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
return builder -> builder
.withCacheConfiguration("productResponses",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues()
.serializeValuesWith(SerializationPair.fromSerializer(
new Jackson2JsonRedisSerializer<>(Object.class))));
}
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
基于TTL的自动失效:
@Cacheable(value = "responses", key = "#root.methodName",
unless = "#result == null")
@CacheConfig(cacheNames = "responses")
手动清除策略: “`java @CacheEvict(value = “responses”, allEntries = true) public void clearAllResponses() {}
@CacheEvict(value = “responses”, key = “#id”) public void evictById(Long id) {}
3. **条件缓存**:
```java
@Cacheable(condition = "#request.getHeader('no-cache') == null")
良好的缓存键设计应考虑: - 包含所有影响响应的参数 - 排除无关参数(如时间戳) - 保持合理长度
示例实现:
public class CacheKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder key = new StringBuilder();
key.append(target.getClass().getSimpleName());
key.append("_");
key.append(method.getName());
if (params.length > 0) {
key.append("_");
for (Object param : params) {
if (param instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) param;
key.append(request.getRequestURI());
Enumeration<String> paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) {
String name = paramNames.nextElement();
if (!name.equals("_")) { // 排除随机参数
key.append("_").append(name)
.append("=").append(request.getParameter(name));
}
}
} else if (param != null) {
key.append("_").append(param.toString());
}
}
}
return DigestUtils.md5DigestAsHex(key.toString().getBytes());
}
}
// 配置类 @Bean public CacheManager randomTtlCacheManager() { return new CaffeineCacheManager() { @Override protected Cache createCaffeineCache(String name) { return new CaffeineCache(name, Caffeine.newBuilder() .expireAfterWrite(30 + new Random().nextInt(15), TimeUnit.MINUTES) .build()); } }; }
2. **穿透防护**:
```java
@Cacheable(value = "products",
key = "#id",
unless = "#result == null")
public Product getProduct(Long id) {
Product product = productRepository.findById(id);
if (product == null) {
// 缓存空值防止穿透
cacheNullValue(id);
return null;
}
return product;
}
集成Micrometer进行指标收集:
@Configuration
public class CacheMetricsConfig {
@Bean
public CacheMetricsRegistrar cacheMetricsRegistrar(
CacheManager cacheManager, MeterRegistry meterRegistry) {
return new CacheMetricsRegistrar(cacheManager, meterRegistry)
.bindTo(meterRegistry);
}
}
配置缓存操作日志:
@Slf4j
@Aspect
@Component
public class CacheLoggerAspect {
@Pointcut("@annotation(org.springframework.cache.annotation.Cacheable)")
public void cacheableMethods() {}
@Pointcut("@annotation(org.springframework.cache.annotation.CacheEvict)")
public void cacheEvictMethods() {}
@Around("cacheableMethods()")
public Object logCacheable(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.debug("Checking cache for {} with args {}", methodName, args);
Object result = joinPoint.proceed();
if (result != null) {
log.debug("Cache hit for {}", methodName);
} else {
log.debug("Cache miss for {}", methodName);
}
return result;
}
}
启用缓存JMX管理:
spring.cache.jmx.enabled=true
@RestController
@RequestMapping("/api/v2/products")
@CacheConfig(cacheNames = "productDetail")
public class ProductControllerV2 {
@GetMapping("/{id}")
@Cacheable(key = "T(com.example.util.CacheKeyGenerator).generateDetailKey(#id, #request)")
public ResponseEntity<ProductDetailDTO> getProductDetail(
@PathVariable Long id,
@RequestHeader(value = "Accept-Language", defaultValue = "zh-CN") String language,
WebRequest request) {
if (request.checkNotModified(getLastModified(id))) {
return null;
}
ProductDetailDTO detail = productService.getDetail(id, language);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
.eTag(detail.getVersion())
.lastModified(detail.getUpdateTime().toEpochMilli())
.body(detail);
}
}
@GetMapping
@Cacheable(key = "{#page, #size, #sort, #request.queryString}")
public Page<ProductListItem> listProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id,desc") String sort,
HttpServletRequest request) {
return productService.findProducts(PageRequest.of(page, size, Sort.by(sort)));
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleProductUpdate(ProductUpdatedEvent event) {
cacheEvictor.evictProductCache(event.getProductId());
}
@Service
@RequiredArgsConstructor
public class CacheEvictor {
private final CacheManager cacheManager;
public void evictProductCache(Long productId) {
// 清除详情缓存
Cache detailCache = cacheManager.getCache("productDetail");
if (detailCache != null) {
detailCache.evict(CacheKeyGenerator.generateDetailKey(productId));
}
// 清除相关列表缓存
Cache listCache = cacheManager.getCache("productList");
if (listCache != null) {
listCache.clear();
}
}
}
问题场景: - 数据库更新后缓存未及时失效 - 分布式环境下各节点缓存不一致
解决方案:
1. 使用Spring的@TransactionalEventListener
确保事务提交后清除缓存
2. 引入消息队列广播缓存失效事件
3. 实现双删策略:
@CacheEvict(value = "products", key = "#product.id")
@Transactional
public Product updateProduct(Product product) {
product = productRepository.save(product);
eventPublisher.publishEvent(new ProductUpdatedEvent(product.getId()));
return product;
}
// 事件处理器延迟二次删除
@EventListener
@Async
public void handleProductUpdated(ProductUpdatedEvent event) {
try {
Thread.sleep(1000);
cacheManager.getCache("products").evict(event.getProductId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
优化方案: 1. 压缩缓存数据:
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
// 配置压缩序列化器
template.setValueSerializer(new GzipRedisSerializer(
new GenericJackson2JsonRedisSerializer()));
}
@Cacheable(value = “productData”, key = “#ref”) public Product getProductByRef(String ref) { Long id = extractIdFromRef(ref); return productRepository.findById(id); }
### 7.3 敏感数据缓存
**安全措施**:
1. 排除敏感字段:
```java
@JsonIgnore
private String password;
@Bean
public CacheManager secureCacheManager() {
return new CaffeineCacheManager() {
@Override
protected Cache createCaffeineCache(String name) {
return new SecureCacheWrapper(
super.createCaffeineCache(name),
encryptionService);
}
};
}
对于相关资源可考虑使用HTTP/2推送:
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。