Spring Cache @Cacheable - tidak berfungsi saat memanggil dari metode lain dari kacang yang sama

107

Cache musim semi tidak berfungsi saat memanggil metode cache dari metode lain dari kacang yang sama.

Berikut adalah contoh untuk menjelaskan masalah saya dengan jelas.

Konfigurasi:

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

Layanan dalam cache:

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

}

Hasil:

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

The getEmployeeDatapenggunaan metode panggilan-cache employeeDatadalam panggilan kedua seperti yang diharapkan. Tetapi ketika getEmployeeDatametode dipanggil di dalam AServicekelas (dalam getEmployeeEnrichedData), Cache tidak digunakan.

Apakah ini cara kerja cache musim semi atau saya melewatkan sesuatu?

Bala
sumber
apakah Anda menggunakan nilai yang sama untuk someDateparam?
Dewfy
@Dewfy Ya, itu sama
Bala

Jawaban:

158

Saya yakin begitulah cara kerjanya. Dari apa yang saya ingat pernah membaca, ada kelas proxy yang dihasilkan yang mencegat semua permintaan dan merespons dengan nilai yang di-cache, tetapi panggilan 'internal' dalam kelas yang sama tidak akan mendapatkan nilai yang di-cache.

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

Hanya panggilan metode eksternal yang masuk melalui proxy yang dicegat. Ini berarti bahwa pemanggilan sendiri, pada dasarnya, metode dalam objek target yang memanggil metode lain dari objek target, tidak akan menyebabkan intersepsi cache yang sebenarnya pada waktu proses meskipun metode yang dipanggil ditandai dengan @Cacheable.

Shawn D.
sumber
1
Nah, jika Anda membuat panggilan kedua Cacheable juga, itu hanya akan memiliki satu cache yang hilang. Artinya, hanya panggilan pertama ke getEmployeeEnrichedData yang akan melewati cache. Panggilan kedua untuk itu akan menggunakan pengembalian yang sebelumnya di-cache dari panggilan pertama ke getEmployeeEnrichedData.
Shawn D.
1
@Bala Saya memiliki masalah yang sama, solusi saya pindah @Cacheableke DAO :( Jika Anda memiliki solusi yang lebih baik, beri tahu saya, terima kasih.
VAdaihiep
2
Anda juga dapat menulis Layanan misalnya CacheService dan memasukkan semua metode ke cache Anda ke dalam layanan. Autowire Layanan di mana Anda perlu dan panggil metode. Membantu dalam kasus saya.
DOUBL3P
Sejak Spring 4.3, hal ini dapat diselesaikan menggunakan @Resourceself-autowiring, lihat contoh stackoverflow.com/a/48867068/907576
radistao
1
Juga @Cacheablemetode eksternal seharusnya public, tidak berfungsi pada metode paket-privat. Menemukannya dengan cara yang sulit.
anand
36

Sejak Spring 4.3, masalah tersebut dapat diselesaikan menggunakan self-autowiring melalui @Resourceanotasi:

@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.dll
sumber
2
Mencoba ini di bawah 4.3.17dan tidak berhasil, panggilan untuk selftidak melalui proxy dan cache (masih) dilewati.
Madbreaks
Bekerja untuk saya. Cache hits. Saya menggunakan dependensi pegas terbaru pada tanggal ini.
Tomas Bisciak
Apakah saya satu-satunya yang berpikir ini merusak pola, terlihat seperti campuran tunggal, dll.?
2mia
saya menggunakan versi starter boot musim semi - 2.1.0.RELEASE, dan saya memiliki masalah yang sama. Solusi khusus ini bekerja dengan sangat baik.
Deepan Prabhu Babu
18

Contoh di bawah ini adalah apa yang saya gunakan untuk menekan proxy dari dalam kacang yang sama, ini mirip dengan solusi @ mario-eis, tetapi saya merasa sedikit lebih mudah dibaca (mungkin bukan :-). Bagaimanapun, saya ingin menyimpan anotasi @Cacheable di tingkat layanan:

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

Lihat juga Memulai transaksi baru di Spring bean

Molholm
sumber
1
Mengakses konteks aplikasi, misalnya applicationContext.getBean(SettingService.class);, adalah kebalikan dari injeksi ketergantungan. Saya sarankan untuk menghindari gaya itu.
SingleShot
2
Ya akan lebih baik untuk menghindarinya, tetapi saya tidak melihat solusi yang lebih baik untuk masalah ini.
molholm
10

Inilah yang saya lakukan untuk proyek kecil dengan hanya penggunaan marginal dari pemanggilan metode dalam kelas yang sama. Dokumentasi dalam kode sangat disarankan, karena mungkin terlihat aneh bagi rekan kerja. Tapi mudah untuk menguji, sederhana, cepat untuk dicapai dan membuat saya tidak perlu menggunakan instrumentasi AspectJ sepenuhnya. Namun, untuk penggunaan yang lebih berat, saya akan menyarankan solusi 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
sumber
1
dapatkah Anda memberi contoh dengan AspectJ?
Sergio Bilello
Jawaban ini adalah duplikat dari stackoverflow.com/a/34090850/1371329 .
jaco0646
3

Dalam Kasus saya, saya menambahkan variabel:

@Autowired
private AService  aService;

Jadi saya memanggil getEmployeeDatametode dengan menggunakanaService

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

}

Ini akan menggunakan cache dalam kasus ini.

Ibtissam Ibtissama
sumber
2

Gunakan tenun statis untuk membuat proxy di sekitar kacang Anda. Dalam hal ini bahkan metode 'internal' akan bekerja dengan benar

Dewfy
sumber
Apa itu "tenun statis"? google tidak banyak membantu. Ada petunjuk untuk memahami konsep ini?
Bala
@Bala - hanya sebagai contoh pada proyek kami, kami menggunakan <iajckompiler (dari ant) ​​yang menyelesaikan semua aspek kebutuhan untuk kelas yang dapat disimpan dalam cache.
Dewfy
0

Saya menggunakan kacang dalam internal ( FactoryInternalCache) dengan cache nyata untuk tujuan ini:

@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.dll
sumber
0

solusi termudah sejauh ini hanya dengan referensi seperti ini:

AService.this.getEmployeeData(date);
Jason
sumber