基于Spring缓存含StreamingOutput的REST服务的技术问询
我之前处理过类似的场景,直接给@Cacheable加在返回Response的方法上绝对会踩坑——因为StreamingOutput里的输出流是一次性消费的,缓存之后再调用接口,流早就被关闭或者数据已经被写过了,根本没法正常返回数据。下面给你几个实用的解决方案,你可以根据自己的业务场景来选:
方案一:缓存原始数据而非Response/StreamingOutput
这是最稳妥、最容易维护的方案。核心思路是:缓存要流式输出的原始业务数据(比如你的区域列表),而不是缓存已经包装好的Response或StreamingOutput。每次请求时,从缓存中取出原始数据,再重新包装成新的StreamingOutput和Response。
这样做的好处是,每次请求都会生成全新的输出流,完全避免了流重复读取的问题,同时也达到了缓存数据、减少数据源查询的目的。
代码示例如下:
// 单独抽离出获取原始数据的方法,给这个方法加缓存注解 @Cacheable(value = "params", key = "#params") public List<AdminArea> getAreasRawData(String params) { // 这里写你原来获取数据的逻辑:比如从数据库查询、调用其他服务等 return fetchAdminAreasFromDataSource(params); } // REST接口方法,不再直接缓存,而是调用缓存的方法生成流 public Response RetrieveAreas(String params) { // 从缓存获取原始数据 List<AdminArea> areas = getAreasRawData(params); StreamingOutput adminAreaStream = new StreamingOutput() { ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); @Override public void write(OutputStream output) throws IOException, WebApplicationException { // 这里可以复用你原来的循环写入逻辑,遍历areas逐个写入即可 for (AdminArea area : areas) { ow.writeValue(output, area); // 如果需要分隔符(比如JSON数组),可以在这里添加逗号等分隔符 output.write(",\n".getBytes(StandardCharsets.UTF_8)); } } }; return Response.ok(adminAreaStream).build(); }
方案二:缓存数据生成器(适合超大数据量场景)
如果你的原始数据量极大,直接缓存整个集合会占用过多内存,可以考虑缓存一个数据生成器(比如Supplier或Callable),每次需要输出流时,调用生成器获取数据并写入流。
这个方案本质上和方案一类似,但延迟了数据的加载时机,只有在真正需要写流的时候才会从数据源获取(或从缓存的生成器触发获取)数据。
代码示例:
@Cacheable(value = "params", key = "#params") public Supplier<List<AdminArea>> getAreasDataSupplier(String params) { // 返回一个Supplier,只有调用get()时才会执行数据查询逻辑 return () -> fetchAdminAreasFromDataSource(params); } public Response RetrieveAreas(String params) { Supplier<List<AdminArea>> areasSupplier = getAreasDataSupplier(params); StreamingOutput adminAreaStream = new StreamingOutput() { ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); @Override public void write(OutputStream output) throws IOException, WebApplicationException { // 只有在写流时才会触发数据获取 List<AdminArea> areas = areasSupplier.get(); for (AdminArea area : areas) { ow.writeValue(output, area); output.write(",\n".getBytes(StandardCharsets.UTF_8)); } } }; return Response.ok(adminAreaStream).build(); }
不推荐的方案:直接缓存Response
如果你非要尝试直接缓存Response,需要自定义缓存的序列化和反序列化逻辑——因为默认的Spring缓存(比如ConcurrentMapCache、Redis缓存)无法正确处理StreamingOutput这种带状态的对象,即使序列化成功,反序列化后的StreamingOutput的输出流已经失效,根本无法正常使用。
这个方案复杂度极高,维护成本大,很容易引发各种奇怪的问题(比如流关闭、数据丢失),所以非常不推荐。
一些额外的注意事项
- 缓存Key的正确性:确保
@Cacheable的key能唯一标识不同的请求参数。如果params是复杂字符串或对象,建议用SpEL表达式明确指定key,比如@Cacheable(value = "params", key = "#params"),避免默认的key生成逻辑出现冲突。 - 缓存更新与失效:当原始数据发生变化时,一定要用
@CacheEvict清除对应缓存,或者用@CachePut更新缓存,避免返回过期数据。 - 超大数据量的优化:如果数据量实在太大,缓存原始数据会占用过多内存,可以考虑用分布式缓存(比如Redis)配合高效的序列化方式(如Jackson2JsonRedisSerializer),或者拆分数据进行分段缓存。
内容的提问来源于stack exchange,提问作者Hiro Protagonist

