spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
55.71k stars 37.79k forks source link

EnableAsync prevents EnableCaching from working in self-injection scenarios [SPR-15915] #20469

Open spring-projects-issues opened 6 years ago

spring-projects-issues commented 6 years ago

Henri Tremblay opened SPR-15915 and commented

If I use @Async and @CacheEvict on the same class, calling the method with @CacheEvict directly won't evict anymore.

Let's say I have the following class.

public class MyClass {

  private CountDownLatch latch;
  private MyClass meWithAProxy;

  @Autowired
  ApplicationContext applicationContext;

  @PostConstruct
  public void init() {
    meWithAProxy = applicationContext.getBean(MyClass.class);
  }

  public CountDownLatch getLatch() {
    return latch;
  }

  public void setLatch(CountDownLatch latch) {
    this.latch = latch;
  }

  @Async
  public void function1() {
    meWithAProxy.anotherFunction(123);

    if(latch != null) {
      latch.countDown();
    }
  }

  @CacheEvict(cacheNames = "cache", key = "#testId")
  public List<Integer> anotherFunction(int testId) {
    return Collections.emptyList();
  }

}

And I then use it like that:

@Configuration
@EnableCaching
@EnableAsync
public class App {

  public static void main(String[] args) throws InterruptedException {
    ApplicationContext context = new AnnotationConfigApplicationContext(App.class);
    CacheManager cacheManager = context.getBean(CacheManager.class);
    Cache cache = cacheManager.getCache("cache");

    MyClass myClass = context.getBean(MyClass.class);

    cache.put(123, "test"); // value to evict

    myClass.setLatch(new CountDownLatch(1));
    myClass.function1(); // this is correctly called asynchronously
    myClass.getLatch().await();
    assertThat(cache.get(123)).describedAs("Reentrant call failed").isNull(); // and the value is evicted as expected

    cache.put(1, "test"); // new value to evict
    assertThat(cache.get(1)).isNotNull();

    myClass.anotherFunction(1); // direct call
    assertThat(cache.get(1)).describedAs("Direct call failed").isNull(); // fails!
  }

  @Bean
  public MyClass myClass() {
    return new MyClass();
  }

  @Bean
  public TaskExecutor taskExecutor() {
    return new SimpleAsyncTaskExecutor();
  }

  @Bean
  public CacheManager cacheManager() {
    javax.cache.CacheManager cacheManager = Caching.getCachingProvider().getCacheManager();
    cacheManager.createCache("cache", new MutableConfiguration<>().setStoreByValue(false));
    return new JCacheCacheManager(cacheManager);
  }
}

For some reason, the cache interceptor is not there. It seems that the Advisors with the cache are replaced by the async ones but I don't know why.


Affects: 4.3.10

Reference URL: https://stackoverflow.com/questions/45938279/cache-not-refreshing-when-being-called-from-a-asynchrounous-function-in-spring/45963494#45963494

Issue Links:

spring-projects-issues commented 6 years ago

Juergen Hoeller commented

This is strongly related to self-injection: That self-injected early bean reference gets flagged as such in AbstractAutoProxyCreator, expecting to see a pre-proxied instance in postProcessAfterInitialization which it doesn't have to add any interceptors to anymore. While this is an accurate assumption in most cases, it can lead to accidental exposure of an incomplete proxy in self-injection scenarios.

spring-projects-issues commented 6 years ago

Juergen Hoeller commented

It's generally a good idea to break such a self-reference initialization cycle with an @Lazy reference (or corresponding ObjectFactory/Provider declaration), in this case for the meWithAProxy reference. We'll see what we can do to make it work without that as well but probably only in 5.x.

anishlukk123 commented 8 months ago

Is there anyone working on this?

amitarcade commented 6 months ago

Hi, I would like to contribute on it, pls assign to me, thanks

amitarcade commented 6 months ago

When I tested, it works fine : Code:

@Service @CacheConfig public class Util {

Util util;

@Autowired
ApplicationContext appCont;

@Cacheable(value = "pi", key = "#inp")
public int calPi(int inp) {
    System.out.println("inp "+inp);
    return inp * 3;
}

@PostConstruct
public void init() {
    util = appCont.getBean(Util.class);
    System.out.println("POST construct "+util.getClass());

}

@Async
public void methodOne() {
    System.out.println("asyc");
    util.evictCache();

}

@CacheEvict(value= "pi", allEntries = true)
public void evictCache() {
    System.out.println("cacheevict");

}

public void methodTwo() {
    util.evictCache();
    System.out.println(this.getClass());

}

}

@SpringBootApplication @Configuration @EnableCaching public class DemoApplication {

public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(DemoApplication.class);

    Util util = context.getBean(Util.class);

    util.calPi(2);
    util.calPi(2);
    util.methodOne();
    util.calPi(2);
    util.evictCache();
    util.calPi(2);
    util.methodTwo();
    util.calPi(2);

} }

Output: POST construct class com.amit.test.demo.Util$$SpringCGLIB$$0 inp 2 asyc cacheevict inp 2 cacheevict inp 2 cacheevict class com.amit.test.demo.Util inp 2

Here we get "inp 2" printed 4 times which is expected