Spring缓存@Cacheable:从同一bean的另一个方法调用时无法工作

187

当从同一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方法时,缓存没有被使用。

这是Spring缓存的工作方式吗?还是我漏掉了什么?


你是否使用相同的值作为 someDate 参数? - Dewfy
@Dewfy 是的,它是相同的。 - Bala
15个回答

258
我相信它是这样工作的。据我记得阅读,生成了一个代理类来拦截所有请求并响应缓存值,但是在同一类内部的“内部”调用将不会获得缓存值。
来自https://code.google.com/p/ehcache-spring-annotations/wiki/UsingCacheable

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


1
如果您将第二个调用也变成可缓存的,那么它只会有一个缓存未命中。也就是说,只有第一次对getEmployeeEnrichedData的调用会绕过缓存。第二次对它的调用将使用从第一次调用getEmployeeEnrichedData先前缓存的返回值。 - Shawn D.
1
@Bala 我也遇到了同样的问题,我的解决方案是将 @Cacheable 移动到 DAO :( 如果你有更好的解决方案,请告诉我,谢谢。 - VAdaihiep
2
你也可以编写一个服务,例如CacheService,并将所有需要缓存的方法放入该服务中。在需要的地方进行自动装配并调用这些方法。在我的情况下非常有帮助。 - DOUBL3P
1
自从Spring 4.3版本以后,可以使用@Resource自动装配来解决这个问题,参见示例https://dev59.com/wmQn5IYBdhLWcg3wW2D3#48867068。 - radistao
5
еҗҢж—¶пјҢеӨ–йғЁзҡ„@Cacheableж–№жі•еә”иҜҘжҳҜpublicзҡ„пјҢе®ғж— жі•еңЁеҢ…з§Ғжңүзҡ„ж–№жі•дёҠе·ҘдҪңгҖӮжҲ‘жӣҫз»ҸеҗғиҝҮдәҸгҖӮ - anand
1
Spring中的大多数注解都是通过AOP完成的,这是通过代理类实现的。因此,这些调用不起作用,因为在方法内部调用意味着您不再通过代理。 - amstegraf

59

自从Spring 4.3版本以后,可以使用自动注入自身而不是使用@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);
    }
}

5
我尝试在4.3.17下操作,但失败了。调用self时没有通过代理,并且缓存仍然被绕过。 - Madbreaks
对我有用。缓存命中。我使用截至今日的最新 Spring 依赖项。 - Tomas Bisciak
我使用了Spring Boot Starter版本-2.1.0.RELEASE,但是我遇到了同样的问题。这个特定的解决方案非常有效。 - Deepan Prabhu Babu
8
威尔,这不会产生循环依赖吗? - Chandresh Mishra
3
Spring 5 崩溃:“不鼓励依赖循环引用,并且默认情况下禁止使用。请更新您的应用程序以消除bean之间的依赖循环。” - jnnnnn
显示剩余2条评论

23

下面的示例是我用来在同一个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);
}
...

另请参阅Spring Bean中启动新事务


1
访问应用程序上下文,例如 applicationContext.getBean(SettingService.class);,是依赖注入的反义词。我建议避免使用这种风格。 - SingleShot
2
是的,避免这种情况会更好,但我并没有看到更好的解决方案。 - molholm

13

对于仅在同一类中稍微使用方法调用的小型项目,我会这样做。强烈建议进行代码内文档编写,因为这可能会让同事感到奇怪。但是它易于测试、简单快捷,并且能够避免完全依赖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);
        ...
    }
}

1
你能举一个使用AspectJ的例子吗? - Coder
2
这个答案是 https://dev59.com/2Wct5IYBdhLWcg3wZcqL#34090850 的重复。 - jaco0646

13

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


8

是的,缓存不会发生,因为其他帖子中已经提到了这些原因。然而,我会通过将该方法放入其自己的类(在此情况下为服务)来解决问题。通过这样做,您的代码将更易于维护/测试和理解。

@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");
        ...
    }

}

1
这是一种方法。 - Dexter Legaspi
有趣的方法。进一步解释“更易于维护”的含义:这种方式将所有与缓存相关的方法放置在同一个类中,这使得开发人员需要在哪个类中放置新方法做出选择?在缓存(和驱逐)类中还是其他类中?随着时间的推移,很容易忽略这一点,即当您添加新方法时,您可能还需要从缓存中清除相关数据。 - user07

4
在我的情况下,我添加了变量:
@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);
    ...
}

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


3
更好的方法是创建另一个服务,例如ACachingService并调用 ACachingService.cachingMethod()而不是自我Autowiring(或尝试进行自我注入的任何其他方法)。这样做可以避免循环依赖性,在升级到新的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?

3
我们查看了这里的所有解决方案,并决定使用一个单独的类来缓存方法,因为Spring 5不支持循环依赖。

1
我使用内部的内部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://dev59.com/wmQn5IYBdhLWcg3wW2D3">Spring Cache @Cacheable - not working while calling from another method of the same bean</a>
 * @see <a href="https://dev59.com/2Wct5IYBdhLWcg3wZcqL">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);
    }
}
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接