问题 如何让Spring缓存存储ResponseBody而不是中间对象


我使用spring cache和this方法,它将查询的值作为JSON返回:

@RequestMapping("/getById")
@ResponseBody
@Cacheable
public HugeValue getHugeValueFromSlowFoo( @RequestParam(value = "id", defaultValue = "") String id ) {
  return Foo.getById( id );
}

这工作正常,HugeValue对象存储在缓存中(在本例中为Hazelcast)。我想进一步改进这个,因为从HugeValue创建JSON的时间非常长。 我可以告诉spring cache缓存我对象的JSON-ified版本吗?

我使用Jackson和Spring Boot 1.2以及Spring 4.1


8918
2018-01-12 08:59


起源



答案:


在没有真正了解您的确切用例的情况下(我还不允许在不幸的情况下添加评论),我尝试简要总结一下我的想法。他们都假设你使用杰克逊进行json映射,至少 春季3.1

据我所知,SpringMVC中没有enableResponseBodyCaching功能。

第一种选择: 使用http缓存,因为您似乎真的想要缓存整个http响应。 Spring提供了一种直接的全局配置方式:

<mvc:interceptors>
    <bean id="webContentInterceptor"
          class="org.springframework.web.servlet.mvc.WebContentInterceptor">
        <property name="cacheSeconds" value="0"/>
        <property name="useExpiresHeader" value="true"/>
        <property name="useCacheControlHeader" value="true"/>
        <property name="useCacheControlNoStore" value="true"/>
    </bean>
</mvc:interceptors>

如果你希望控制每个Controller,继承Spring 一个AbstractController 并根据javaDoc设置cacheSeconds属性。

当然,http缓存的真正威力来自于服务器前面的http代理。

第二个想法: 实现自己的子类 MappingJackson2HttpMessageConverter。在 writeInternal() 您可以添加一些访问缓存的逻辑来检索已映射的版本,而不是映射输入对象。这种方法意味着您将点击您的服务以检索Json流后面的java对象。如果这对你来说没问题,因为在某些时候也有缓存,这种方法值得一试。

第三个想法: 在提供原始json字符串/流的专用包装器服务中自己进行json映射。您可以轻松注入杰克逊映射器(类名称 ObjectMapper)并完全控制映射。注释此服务然后允许您缓存结果。在您的控制器中,您只提供一个 ResponseEntity 您要使用的相应类型(字符串或某些流)。如果存在缓存结果,这甚至会阻止更深入的服务访问。

编辑:可能是 MappingJackson2JsonView 也可以派上用场。说实话,我之前从未使用它,所以我无法真正说出它的用法。

希望有助于和/或给予灵感! 干杯


7
2018-01-13 08:05



感谢您的全面解答和最佳选择。如果有其他答案,我会等待一段时间,但是现在我喜欢你所描述的所有替代品,第三种想法似乎适合,尽管有一些工作要实现这一点。因此,懒惰是我等待可能需要较少工作的第四个答案的唯一原因;-)。浏览器缓存不是我想要的,因为我有成千上万的用户会收到相同的结果,并且browseride缓存不能以完美的方式工作,因为需要为每个用户检索和jsonified HugeValue。 - Marged
WebContentInterceptor配置实际上告诉客户端不要缓存响应... - Brian Clozel


答案:


在没有真正了解您的确切用例的情况下(我还不允许在不幸的情况下添加评论),我尝试简要总结一下我的想法。他们都假设你使用杰克逊进行json映射,至少 春季3.1

据我所知,SpringMVC中没有enableResponseBodyCaching功能。

第一种选择: 使用http缓存,因为您似乎真的想要缓存整个http响应。 Spring提供了一种直接的全局配置方式:

<mvc:interceptors>
    <bean id="webContentInterceptor"
          class="org.springframework.web.servlet.mvc.WebContentInterceptor">
        <property name="cacheSeconds" value="0"/>
        <property name="useExpiresHeader" value="true"/>
        <property name="useCacheControlHeader" value="true"/>
        <property name="useCacheControlNoStore" value="true"/>
    </bean>
</mvc:interceptors>

如果你希望控制每个Controller,继承Spring 一个AbstractController 并根据javaDoc设置cacheSeconds属性。

当然,http缓存的真正威力来自于服务器前面的http代理。

第二个想法: 实现自己的子类 MappingJackson2HttpMessageConverter。在 writeInternal() 您可以添加一些访问缓存的逻辑来检索已映射的版本,而不是映射输入对象。这种方法意味着您将点击您的服务以检索Json流后面的java对象。如果这对你来说没问题,因为在某些时候也有缓存,这种方法值得一试。

第三个想法: 在提供原始json字符串/流的专用包装器服务中自己进行json映射。您可以轻松注入杰克逊映射器(类名称 ObjectMapper)并完全控制映射。注释此服务然后允许您缓存结果。在您的控制器中,您只提供一个 ResponseEntity 您要使用的相应类型(字符串或某些流)。如果存在缓存结果,这甚至会阻止更深入的服务访问。

编辑:可能是 MappingJackson2JsonView 也可以派上用场。说实话,我之前从未使用它,所以我无法真正说出它的用法。

希望有助于和/或给予灵感! 干杯


7
2018-01-13 08:05



感谢您的全面解答和最佳选择。如果有其他答案,我会等待一段时间,但是现在我喜欢你所描述的所有替代品,第三种想法似乎适合,尽管有一些工作要实现这一点。因此,懒惰是我等待可能需要较少工作的第四个答案的唯一原因;-)。浏览器缓存不是我想要的,因为我有成千上万的用户会收到相同的结果,并且browseride缓存不能以完美的方式工作,因为需要为每个用户检索和jsonified HugeValue。 - Marged
WebContentInterceptor配置实际上告诉客户端不要缓存响应... - Brian Clozel


我认为这可行。

public interface HugeValueService{
    public String getHugeValueFromSlowFoo(String id);
}

public class HugeValueServiceImpl implements HugeValueService{
    @Cacheable
    public String getHugeValueFromSlowFoo(String id ) {
      return Foo.getById( id );
    }
}

Your Class
----------
@RequestMapping("/getById")
@ResponseBody
public HugeValue getHugeValueFromSlowFoo( @RequestParam(value = "id", defaultValue = "") String id ) {
  return hugeValueService.getHugeValueFromSlowFoo(id);
}

祝你好运!!!


1
2018-06-08 21:30





缓存json并手动将其放入响应中。

public void getHugeValueFromSlowFoo( @RequestParam(value = "id", defaultValue = "") String id, ServletResponse resp ) 
{
  String jsonSerializedHugeValue = Bar.getById(id); // serialize manually with ObjectMapper
  resp.getWriter().write(jsonSerializedHugeValue);
  resp.setContentType("application/json");
  resp.flushBuffer();
}

1
2018-06-09 06:53



这可行,但不是很“引导样式”。其中我承认没有提到作为先决条件;-) - Marged
虽然不常用(并且正确,因为大部分时间都没有必要),但这是一个合适的弹簧和弹簧启动代码。它也干净,明显,可读性强。 - Dariusz