Spring Cache @Cacheable - не работает при вызове из другого метода того же компонента

108

Кеш 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при втором вызове , как ожидалось. Но когда getEmployeeDataметод вызывается внутри AServiceкласса (in getEmployeeEnrichedData), Cache не используется.

Так работает весенний кеш или мне что-то не хватает?

Бала
источник
вы используете то же значение для someDateпараметра?
Dewfy 03
@Dewfy Да, это то же самое
Бала

Ответы:

162

Я считаю, что это так. Из того, что я помню, читал, создается прокси-класс, который перехватывает все запросы и отвечает кэшированным значением, но «внутренние» вызовы внутри того же класса не получат кэшированного значения.

Из https://code.google.com/p/ehcache-spring-annotations/wiki/UsingCacheable

Перехватываются только вызовы внешних методов, поступающие через прокси. Это означает, что самовызов, по сути, метод в целевом объекте, вызывающий другой метод целевого объекта, не приведет к фактическому перехвату кеша во время выполнения, даже если вызванный метод отмечен @Cacheable.

Шон Д.
источник
1
Что ж, если вы сделаете второй вызов Cacheable, у него будет только один промах кеша. То есть только первый вызов getEmployeeEnrichedData будет обходить кеш. Второй вызов к нему будет использовать ранее кэшированный возврат из первого вызова getEmployeeEnrichedData.
Шон Д.
1
@Bala У меня такая же проблема, мое решение - перейти @Cacheableв DAO :( Если у вас есть лучшее решение, дайте мне знать, спасибо.
VAdaihiep
2
вы также можете написать Service, например CacheService, и поместить все свои методы кеширования в службу. Подключите Сервис к нужному месту и вызовите методы. В моем случае помогло.
DOUBL3P
1
Начиная с Spring 4.3, это можно было решить с помощью @Resourceсамоавтоматизации, см. Пример stackoverflow.com/a/48867068/907576
radistao
1
Также @Cacheableдолжен быть внешний метод public, он не работает с частными методами пакета. Нашел трудный путь.
anand
38

Начиная с 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);
    }
}
Radistao
источник
2
Пробовал это под, 4.3.17и это не сработало, призывы selfне проходить через прокси, а кеш (все еще) обходится.
Madbreaks
Работал у меня. Попадания в кеш. На данный момент я использую последние версии Spring.
Tomas Bisciak
Я единственный, кто думает, что это ломает шаблоны, выглядит как синглтон и т. д.?
2mia
Я использовал стартовую версию весенней загрузки - 2.1.0.RELEASE, и у меня была такая же проблема. Это конкретное решение работало как шарм.
Дипан Прабху Бабу
Уилл, это не создает циклической зависимости?
Чандреш Мишра
18

Пример ниже - это то, что я использую для обращения к прокси из одного и того же 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
Да, лучше бы этого избежать, но лучшего решения этой проблемы я не вижу.
Молхольм
10

Вот что я делаю для небольших проектов с минимальным использованием вызовов методов в одном классе. Настоятельно рекомендуется внутренняя документация, поскольку она может показаться коллегам странной. Но его легко протестировать, просто, быстро реализовать и избавить меня от полноценного инструментария 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?
Серхио Билелло
Этот ответ является дубликатом stackoverflow.com/a/34090850/1371329 .
jaco0646
3

В моем случае я добавляю переменную:

@Autowired
private AService  aService;

Поэтому я вызываю getEmployeeDataметод, используяaService

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

}

В этом случае он будет использовать кеш.

Ибтиссам Ибтиссама
источник
2

Используйте статическое плетение для создания прокси вокруг вашего bean-компонента. В этом случае корректно работали бы даже «внутренние» методы.

Dewfy
источник
Что такое «статическое плетение»? Google мало помогает. Есть ли указатели, чтобы понять эту концепцию?
Бала
@Bala - например, в нашем проекте мы используем <iajcкомпилятор (от ant), который решает все аспекты необходимости для классов с возможностью кеширования.
Dewfy
0

FactoryInternalCacheДля этой цели я использую внутренний внутренний bean ( ) с реальным кешем:

@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="/programming/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="/programming/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);
    }
}
}
Radistao
источник
0

самое простое решение - просто сослаться на это:

AService.this.getEmployeeData(date);
Джейсон
источник