You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 12:45:27