Spring Cache@Cacheable-从同一 bean 的另一个方法调用时无法工作

当从同一 bean 的另一个方法调用缓存方法时,Spring 缓存无法工作。

这里有一个例子来清楚地解释我的问题。

配置:

<cache:annotation-driven cache-manager="myCacheManager" />


<bean id="myCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="myCache" />
</bean>


<!-- Ehcache library setup -->
<bean id="myCache"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:shared="true">
<property name="configLocation" value="classpath:ehcache.xml"></property>
</bean>


<cache name="employeeData" maxElementsInMemory="100"/>

缓存服务:

@Named("aService")
public class AService {


@Cacheable("employeeData")
public List<EmployeeData> getEmployeeData(Date date){
..println("Cache is not being used");
...
}


public List<EmployeeEnrichedData> getEmployeeEnrichedData(Date date){
List<EmployeeData> employeeData = getEmployeeData(date);
...
}


}

结果:

aService.getEmployeeData(someDate);
output: Cache is not being used
aService.getEmployeeData(someDate);
output:
aService.getEmployeeEnrichedData(someDate);
output: Cache is not being used

getEmployeeData方法调用按预期在第二个调用中使用缓存 employeeData。但是,当在 AService类(在 getEmployeeEnrichedData中)中调用 getEmployeeData方法时,不会使用 Cache。

这就是 Spring cache 的工作方式吗? 还是我遗漏了什么

139879 次浏览

我相信事情就是这样的。据我所知,生成了一个代理类,它拦截所有请求并使用缓存值进行响应,但同一个类中的“内部”调用不会获得缓存值。

来自 https://code.google.com/p/ehcache-spring-annotations/wiki/UsingCacheable

只有通过代理传入的外部方法调用才是 这意味着自我调用实际上是一种方法 在调用目标对象的另一个方法的目标对象中, 将不会在运行时导致实际的缓存拦截,即使 被调用的方法标记为@Cacheable。

使用静态编织在 bean 周围创建代理。在这种情况下,甚至“内部”方法也可以正常工作

下面是我对在同一个类中只有少量方法调用的小型项目所做的工作。强烈建议使用代码内文档,因为它对同事来说可能看起来很奇怪。但它易于测试,简单,快速实现,省去了我全面的 AspectJ 仪器。但是,对于更多的大量使用,我建议使用 AspectJ 解决方案。

@Service
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
class AService {


private final AService _aService;


@Autowired
public AService(AService aService) {
_aService = aService;
}


@Cacheable("employeeData")
public List<EmployeeData> getEmployeeData(Date date){
..println("Cache is not being used");
...
}


public List<EmployeeEnrichedData> getEmployeeEnrichedData(Date date){
List<EmployeeData> employeeData = _aService.getEmployeeData(date);
...
}
}

下面的例子是我用来从同一个 bean 中访问代理的,它类似于@mario-eis 的解决方案,但是我发现它更具可读性(也许它不是: ——)。无论如何,我喜欢将@Cacheable 注释保留在服务级别:

@Service
@Transactional(readOnly=true)
public class SettingServiceImpl implements SettingService {


@Inject
private SettingRepository settingRepository;


@Inject
private ApplicationContext applicationContext;


@Override
@Cacheable("settingsCache")
public String findValue(String name) {
Setting setting = settingRepository.findOne(name);
if(setting == null){
return null;
}
return setting.getValue();
}


@Override
public Boolean findBoolean(String name) {
String value = getSpringProxy().findValue(name);
if (value == null) {
return null;
}
return Boolean.valueOf(value);
}


/**
* Use proxy to hit cache
*/
private SettingService getSpringProxy() {
return applicationContext.getBean(SettingService.class);
}
...

参见 在 Springbean 中启动新事务

为此,我使用内部内部 bean (FactoryInternalCache)和真正的缓存:

@Component
public class CacheableClientFactoryImpl implements ClientFactory {


private final FactoryInternalCache factoryInternalCache;


@Autowired
public CacheableClientFactoryImpl(@Nonnull FactoryInternalCache factoryInternalCache) {
this.factoryInternalCache = factoryInternalCache;
}


/**
* Returns cached client instance from cache.
*/
@Override
public Client createClient(@Nonnull AggregatedConfig aggregateConfig) {
return factoryInternalCache.createClient(aggregateConfig.getClientConfig());
}


/**
* Returns cached client instance from cache.
*/
@Override
public Client createClient(@Nonnull ClientConfig clientConfig) {
return factoryInternalCache.createClient(clientConfig);
}


/**
* Spring caching feature works over AOP proxies, thus internal calls to cached methods don't work. That's why
* this internal bean is created: it "proxifies" overloaded {@code #createClient(...)} methods
* to real AOP proxified cacheable bean method {@link #createClient}.
*
* @see <a href="https://stackoverflow.com/questions/16899604/spring-cache-cacheable-not-working-while-calling-from-another-method-of-the-s">Spring Cache @Cacheable - not working while calling from another method of the same bean</a>
* @see <a href="https://stackoverflow.com/questions/12115996/spring-cache-cacheable-method-ignored-when-called-from-within-the-same-class">Spring cache @Cacheable method ignored when called from within the same class</a>
*/
@EnableCaching
@CacheConfig(cacheNames = "ClientFactoryCache")
static class FactoryInternalCache {


@Cacheable(sync = true)
public Client createClient(@Nonnull ClientConfig clientConfig) {
return ClientCreationUtils.createClient(clientConfig);
}
}
}

由于 Spring 4.3,这个问题可以通过使用 自动装配 over @Resource注释来解决:

@Component
@CacheConfig(cacheNames = "SphereClientFactoryCache")
public class CacheableSphereClientFactoryImpl implements SphereClientFactory {


/**
* 1. Self-autowired reference to proxified bean of this class.
*/
@Resource
private SphereClientFactory self;


@Override
@Cacheable(sync = true)
public SphereClient createSphereClient(@Nonnull TenantConfig tenantConfig) {
// 2. call cached method using self-bean
return self.createSphereClient(tenantConfig.getSphereClientConfig());
}


@Override
@Cacheable(sync = true)
public SphereClient createSphereClient(@Nonnull SphereClientConfig clientConfig) {
return CtpClientConfigurationUtils.createSphereClient(clientConfig);
}
}

在我的例子中,我添加了变量:

@Autowired
private AService  aService;

因此,我使用 aService调用 getEmployeeData方法

@Named("aService")
public class AService {


@Cacheable("employeeData")
public List<EmployeeData> getEmployeeData(Date date){
..println("Cache is not being used");
...
}


public List<EmployeeEnrichedData> getEmployeeEnrichedData(Date date){
List<EmployeeData> employeeData = aService.getEmployeeData(date);
...
}

}

在这种情况下,它将使用缓存。

如果您从同一个 bean 调用一个缓存方法,它将被视为一个私有方法,并且注释将被忽略

是的,缓存不会发生,因为原因已经在其他职位提到。不过,我可以通过将该方法放到它自己的类(本例中为服务)中来解决这个问题。这样,您的代码将更容易维护/测试和理解。

@Service // or @Named("aService")
public class AService {


@Autowired //or how you inject your dependencies
private EmployeeService employeeService;
 

public List<EmployeeData> getEmployeeData(Date date){
employeeService.getEmployeeData(date);
}


public List<EmployeeEnrichedData> getEmployeeEnrichedData(Date date){
List<EmployeeData> employeeData = getEmployeeData(date);
...
}


}
@Service // or @Named("employeeService")
public class EmployeeService {


@Cacheable("employeeData")
public List<EmployeeData> getEmployeeData(Date date){
println("This will be called only once for same date");
...
}


}

我想分享我认为最简单的方法:

  • 自动连接控制器并使用它来调用方法,而不是使用类上下文 this

更新后的代码如下:

@Controller
public class TestController {




@Autowired TestController self;


@RequestMapping("/test")
public String testView(){
self.expensiveMethod();
return "test";
}




@Cacheable("ones")
public void expensiveMethod(){
System.out.println("Cache is not being used");
}


}

更好的方法应该是创建另一个类似于 ACachingService的服务并调用 ACachingService.cachingMethod(),而不是自动装配(或任何其他尝试自我注入的方法)。这样你就不会陷入循环依赖,这可能会导致在升级到新的 Spring 时出现警告/错误(在我的例子中是 Spring 2.6.6) :

ERROR o.s.boot.SpringApplication - Application run failed
org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'webSecurityConfig':
Requested bean is currently in creation: Is there an unresolvable circular reference?

我们研究了这里的所有解决方案,并决定为缓存的方法使用一个单独的类,因为 Spring5不喜欢循环依赖关系。