Spring缓存:当从同一类中调用时,@Cacheable方法被忽略

61

我正在尝试从同一个类中调用一个 @Cacheable 方法:

@Cacheable(value = "defaultCache", key = "#id")
public Person findPerson(int id) {
   return getSession().getPerson(id);
} 

public List<Person> findPersons(int[] ids) {
   List<Person> list = new ArrayList<Person>();
   for (int id : ids) {
      list.add(findPerson(id));
   }
   return list;
}

希望findPersons的结果也被缓存,但是@Cacheable注解被忽略了,findPerson方法每次都被执行了。

我做错了什么吗?还是这是预期的结果?


类似问题:https://dev59.com/wmQn5IYBdhLWcg3wW2D3。 这里有另一个简单而好的解决方案:https://dev59.com/wmQn5IYBdhLWcg3wW2D3。 - Grigory Kislin
4个回答

62
这是因为Spring中代理处理缓存、事务相关功能时的创建方式。这里有一份非常好的参考资料 - Transactions, Caching and AOP: understanding proxy usage in Spring 简而言之,自我调用绕过了动态代理以及任何横向关注点(如缓存、事务等),这些都是动态代理逻辑的一部分,也会被绕过。
解决方法是使用AspectJ编译时或加载时织入。

我们在事务注释方面遇到了同样的问题,并且按照Biju发布的方法进行了操作。从那以后就没有出现过问题。 - Gonzalo
1
@Gonzalo 刚刚读了博客,你能详细说明一下如何在编译时使用 AspectJ 来解决这个问题吗?谢谢! - David Zhao
1
我有一个使用编译时AspectJ织入缓存注释的示例 - https://github.com/bijukunjummen/cache-sample.git,行为类似于您的情况,即一个方法自调用一个@Cacheable方法。 - Biju Kunjummen
@David 我们只需要在我们的applicationContext.xml中包含以下内容:<aop:aspectj-autoproxy proxy-target-class="true" />,它可以与相同的Transactional注释一起使用。不过,我没有尝试过Cacheable方法。 - Gonzalo
我在 post-construct 方法中注入了 applicationContext 中的服务实例,并调用该实例而不是“this”,这样就可以正常工作了。 - chrismarx
显示剩余4条评论

30

对于仅在同一类中进行较少方法调用的小型项目,我会这样做。强烈建议进行代码文档记录,因为这可能看起来很奇怪。但是它易于测试、简单快捷且避免了完全使用AspectJ仪器。但是,对于更重度使用,我建议使用AspectJ解决方案。

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

    private final PersonDao _personDao;

    @Autowired
    public PersonDao(PersonDao personDao) {
        _personDao = personDao;
    }

    @Cacheable(value = "defaultCache", key = "#id")
    public Person findPerson(int id) {
        return getSession().getPerson(id);
    }

    public List<Person> findPersons(int[] ids) {
        List<Person> list = new ArrayList<Person>();
        for (int id : ids) {
            list.add(_personDao.findPerson(id));
        }
        return list;
    }
}

请问您能解释一下为什么添加了@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)之后它就可以工作了吗?这个注解的作用是什么? - jeffery.yuan
5
因为要将PersonDao注入到PersonDao中,你需要在它被实例化之前拥有一个PersonDao实例 :) 为了解决这个问题,创建了一个存根代理(请参见文档),注入并稍后用相同的PersonDao实例填充。所以,PersonDao可以持有自己的一个实例,包装在存根代理中。就这么简单 :) 不知道是否清晰明了... :D - Mario Eis
谢谢,它仍然运行得很好!请注意,在@Cacheable... find Person()之前,我不得不将所有行替换为@Autowired private PersonDao _personDao; - nkatsar
如果类自动装配了某个第三方类,这种方法就不起作用了。在这种情况下,_personDao实例中的第三方类实例将不会被填充(保持为空)。 - JRA_TLL
请注意,findPerson方法必须是公共的!如果它是私有的,那么它的所有字段都将为空。 - JRA_TLL

2
对于使用 Grails Spring Cache 插件的任何人,文档中描述了一种解决方法。我在一个 Grails 应用程序上遇到了这个问题,但不幸的是,被接受的答案似乎无法在 Grails 上使用。这个解决方案很丑陋,但它确实有效。
示例代码很好地演示了它:
class ExampleService {
    def grailsApplication

    def nonCachedMethod() {
        grailsApplication.mainContext.exampleService.cachedMethod()
    }

    @Cacheable('cachedMethodCache')
    def cachedMethod() {
        // do some expensive stuff
    }
}

只需将exampleService.cachedMethod()替换为您自己的服务和方法即可。


0
如果您在同一类中访问缓存,可能根本不需要使用Spring缓存。
请查看不同的映射实现方式:
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(
    new CacheLoader<Key, Graph>() {
      public Graph load(Key key) throws AnyException {
        return createExpensiveGraph(key);
      }
    });

基于时间的Java映射/缓存,具有过期键


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