Spring Cache @Cacheable: no funciona mientras se llama desde otro método del mismo bean

107

Spring cache no funciona cuando se llama al método en caché desde otro método del mismo bean.

Aquí hay un ejemplo para explicar mi problema de manera clara.

Configuración:

<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"/>  

Servicio en caché:

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

}

Resultado:

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

La getEmployeeDatallamada al método usa caché employeeDataen la segunda llamada como se esperaba. Pero cuando getEmployeeDatase llama al método dentro de la AServiceclase (in getEmployeeEnrichedData), Cache no se utiliza.

¿Es así como funciona Spring Caché o me falta algo?

Bala
fuente
¿Usas el mismo valor para someDateparam?
Dewfy
@Dewfy Sí, es lo mismo
Bala

Respuestas:

158

Creo que así es como funciona. Por lo que recuerdo haber leído, hay una clase de proxy generada que intercepta todas las solicitudes y responde con el valor en caché, pero las llamadas 'internas' dentro de la misma clase no obtendrán el valor en caché.

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

Solo se interceptan las llamadas a métodos externos que ingresan a través del proxy. Esto significa que la autoinvocación, en efecto, un método dentro del objeto de destino que llama a otro método del objeto de destino, no conducirá a una intercepción de caché real en tiempo de ejecución, incluso si el método invocado está marcado con @Cacheable.

Shawn D.
fuente
1
Bueno, si también hace que la segunda llamada se pueda almacenar en caché, solo se perderá una caché. Es decir, solo la primera llamada a getEmployeeEnrichedData omitirá el caché. La segunda llamada usaría el retorno almacenado previamente en caché de la primera llamada a getEmployeeEnrichedData.
Shawn D.
1
@Bala Tengo el mismo problema, mi solución es pasar @Cacheablea DAO :( Si tiene una mejor solución, hágamelo saber, gracias.
VAdaihiep
2
también puede escribir un servicio, por ejemplo, CacheService y poner todos sus métodos de caché en el servicio. Autowire el Servicio donde lo necesite y llame a los métodos. Ayudó en mi caso.
DOUBL3P
Desde Spring 4.3, esto podría resolverse usando @Resourceauto-cableado automático, vea el ejemplo stackoverflow.com/a/48867068/907576
radistao
1
Además, el @Cacheablemétodo externo debería ser public, no funciona en métodos privados de paquetes. Lo encontré de la manera más difícil.
Anand
36

Desde Spring 4.3, el problema podría resolverse utilizando autoajuste sobre @Resourceanotación:

@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
fuente
2
Intenté esto 4.3.17y no funcionó, las llamadas selfno pasan por un proxy y la caché (aún) se omite.
Madbreaks
Trabajó para mi. Aciertos de caché. Utilizo las últimas dependencias de primavera a partir de esta fecha.
Tomas Bisciak
¿Soy el único que piensa que esto rompe patrones, parece una mezcla de singleton, etc., etc.?
2mias
Usé la versión de arranque de Spring Boot - 2.1.0.RELEASE, y tuve el mismo problema. Esta solución en particular funcionó a las mil maravillas.
Deepan Prabhu Babu
18

El siguiente ejemplo es lo que utilizo para acceder al proxy desde el mismo bean, es similar a la solución de @ mario-eis, pero la encuentro un poco más legible (tal vez no lo sea :-). De todos modos, me gusta mantener las anotaciones @Cacheable en el nivel de servicio:

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

Consulte también Iniciar una nueva transacción en Spring bean

Molholm
fuente
1
Acceder al contexto de la aplicación, por ejemplo applicationContext.getBean(SettingService.class);, es lo opuesto a la inyección de dependencia. Sugiero evitar ese estilo.
SingleShot
2
Sí, sería mejor evitarlo, pero no veo una mejor solución para este problema.
molholm
10

Esto es lo que hago para proyectos pequeños con un uso marginal de llamadas a métodos dentro de la misma clase. Se recomienda encarecidamente la documentación en código, ya que puede parecer extraño para los colegas. Pero es fácil de probar, simple, rápido de lograr y me ahorra la instrumentación completa de AspectJ. Sin embargo, para un uso más intenso, recomendaría la solución 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);
        ...
    }
}
Mario Eis
fuente
1
¿podrías dar un ejemplo con AspectJ?
Sergio Bilello
Esta respuesta es un duplicado de stackoverflow.com/a/34090850/1371329 .
jaco0646
3

En mi caso agrego variable:

@Autowired
private AService  aService;

Entonces llamo al getEmployeeDatamétodo usando elaService

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

}

Utilizará el caché en este caso.

Ibtissam Ibtissama
fuente
2

Utilice tejido estático para crear un proxy alrededor de su frijol. En este caso, incluso los métodos 'internos' funcionarían correctamente

Dewfy
fuente
¿Qué es el "tejido estático"? google no ayuda mucho. ¿Algún consejo para entender estos conceptos?
Bala
@Bala: solo, por ejemplo, en nuestro proyecto usamos el <iajccompilador (de ant) ​​que resuelve todos los aspectos de necesidad para las clases que se pueden almacenar en caché.
Dewfy
0

Utilizo el bean interno interno ( FactoryInternalCache) con caché real para este propósito:

@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
fuente
0

la solución más fácil de lejos es simplemente hacer referencia a esto:

AService.this.getEmployeeData(date);
Jason
fuente