Spring Boot中缓存已渲染的模板
Spring Boot中缓存已渲染的模板
我明白你的痛点——用JTE模板引擎时,控制器只返回模板名称,@Cacheable根本起不到缓存渲染后页面的作用,因为它缓存的只是那个字符串而已。下面给你几个实用的方案,帮你实现已渲染响应的缓存:
方案一:手动渲染模板并缓存结果
这个方案的核心是跳过Spring MVC自动渲染的步骤,手动调用JTE模板引擎生成HTML字符串,再用@Cacheable缓存这个最终的HTML。这样就能直接复用渲染好的内容,不用每次都查数据库+渲染模板了。
示例代码如下:
@GetMapping("/") @Cacheable(value = "homePageCache", key = "#root.methodName") public ResponseEntity<String> home(TemplateEngine templateEngine) { // 1. 从数据库获取数据(只有缓存失效时才会执行这步) List<Product> products = productRepo.findAll(); // 2. 准备模板需要的模型数据 Map<String, Object> templateModel = new HashMap<>(); templateModel.put("products", products); // 3. 手动调用JTE引擎渲染模板 String renderedHtml = templateEngine.render("pages/home", templateModel); // 4. 返回HTML格式的响应 return ResponseEntity.ok() .contentType(MediaType.TEXT_HTML) .body(renderedHtml); }
注意事项:
- 缓存key的设计:如果你的页面有动态参数(比如分页参数、用户ID),要把这些参数加入key,比如
key = "#page + '-' + #size",确保不同参数对应不同的缓存项。 - 缓存失效:当产品数据更新时,记得用
@CacheEvict清空对应缓存,比如在新增/修改/删除产品的方法上添加:@CacheEvict(value = "homePageCache", key = "'home'") public void updateProduct(Product product) { productRepo.save(product); }
方案二:在拦截器/过滤器层面缓存响应内容
如果不想修改控制器代码,或者需要给多个页面统一做缓存,可以在请求的拦截器或过滤器层面实现。核心思路是:
- 请求进来时先检查缓存,有缓存直接返回,跳过控制器和渲染流程
- 没有缓存的话,让请求正常执行,同时用
ContentCachingResponseWrapper捕获渲染后的响应内容,存入缓存
步骤1:实现缓存拦截器
@Component public class PageCacheInterceptor implements HandlerInterceptor { private final CacheManager cacheManager; public PageCacheInterceptor(CacheManager cacheManager) { this.cacheManager = cacheManager; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 生成缓存key,这里用请求URL+查询参数作为唯一标识 String cacheKey = buildCacheKey(request); Cache cache = cacheManager.getCache("pageResponseCache"); if (cache != null) { Cache.ValueWrapper valueWrapper = cache.get(cacheKey); if (valueWrapper != null) { // 缓存存在,直接把缓存的响应内容写入输出流 byte[] cachedContent = (byte[]) valueWrapper.get(); response.setContentType(MediaType.TEXT_HTML_VALUE); response.getOutputStream().write(cachedContent); return false; // 跳过后续的控制器和渲染流程 } } return true; // 没有缓存,继续执行请求 } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 捕获并缓存响应内容 if (response instanceof ContentCachingResponseWrapper) { ContentCachingResponseWrapper cachingResponse = (ContentCachingResponseWrapper) response; byte[] content = cachingResponse.getContentAsByteArray(); if (content.length > 0) { String cacheKey = buildCacheKey(request); Cache cache = cacheManager.getCache("pageResponseCache"); if (cache != null) { cache.put(cacheKey, content); } // 确保原始响应内容能正常输出 cachingResponse.copyBodyToResponse(); } } } // 构建唯一的缓存key,包含URL和查询参数 private String buildCacheKey(HttpServletRequest request) { StringBuilder keyBuilder = new StringBuilder(request.getRequestURI()); String queryString = request.getQueryString(); if (queryString != null) { keyBuilder.append("?").append(queryString); } return keyBuilder.toString(); } }
步骤2:配置拦截器和响应包装过滤器
@Configuration public class WebConfig implements WebMvcConfigurer { private final CacheManager cacheManager; public WebConfig(CacheManager cacheManager) { this.cacheManager = cacheManager; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new PageCacheInterceptor(cacheManager)) .addPathPatterns("/") // 指定要缓存的请求路径 .order(Ordered.HIGHEST_PRECEDENCE); } // 配置过滤器,把响应包装成ContentCachingResponseWrapper @Bean public FilterRegistrationBean<ContentCachingFilter> contentCachingFilter() { FilterRegistrationBean<ContentCachingFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new ContentCachingFilter()); registration.addUrlPatterns("/"); // 和要缓存的路径一致 return registration; } // 自定义过滤器,用来包装响应 public static class ContentCachingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ContentCachingResponseWrapper cachingResponse = new ContentCachingResponseWrapper(response); filterChain.doFilter(request, cachingResponse); cachingResponse.copyBodyToResponse(); } } }
额外提醒
JTE模板引擎本身自带模板编译缓存,这个是用来避免重复编译模板文件的,和我们要的“渲染后内容缓存”不是一回事。如果你的模板文件不会频繁修改,建议保持这个默认开启的配置,能提升模板加载的效率,但它解决不了重复查库和重复渲染的问题。
内容来源于stack exchange
相关产品推荐
相关产品推荐

