4

I am not able to share the actual code because of corporate policies but below is an example of method structures.

So in the example I want to the cache on the method in Class B to be cleared when the exception is thrown in class A.

NB: I can not move the cache to Class A so that is not a feasible solution.

I have tried reading all answers and posts online to get this working but not able to figure it out.

Please help with suggestions. A

I have set the following properties in application.properties

spring.cache.enabled=true
spring.cache.jcache.config=classpath:cache/ehcache.xml

@EnableCaching
@EnableTransactionManagement
    Main Class{


@Autowired
CacheManager cacheManager

@PostConstruct
void postConstruct(){
(JCacheCacheManager)cachemanager).setTransactionAware(true);

}
}

@Service
Class A{

@Autowired
B b;

@Transactional
public List<Data> getAllBusinessData(){

List<Data> dataList = b.getDataFromSystem("key");

//TestCode to test cache clears if exception thrown here
throw new RuntimeException("test");

}
}

@Service
Class B{

@Cacheable("cacheName")
public List<Data> getDataFromSystem(String key){

client call code here

return dataList;

}

}
jccampanero
  • 50,989
  • 3
  • 20
  • 49
Mukul Goel
  • 8,387
  • 6
  • 37
  • 77
  • Adding the solutions you have tried so far would really help. – dekkard Aug 02 '22 at 07:41
  • Please, try to develop a minimal example about it(repo), because I don't see why your code shouldn't be working. – Jonathan JOhx Aug 02 '22 at 21:35
  • What version of spring, related dependencies and java are you using? – pringi Aug 03 '22 at 08:31
  • I assume that if the cache method in Class B doesn't return result in Class A, it will throw an exception in Class A's method. It would be great, if you share the kind of exception and message of it? – Kumaresh Babu N S Aug 03 '22 at 11:50
  • @KumareshBabuNS for testing purpose. I have added a throw new RuntimeException in class A method whenever the result from Class B is not null. As shown in example above. – Mukul Goel Aug 03 '22 at 12:36
  • @pringi spring boot 2.5.6 and java 11. Using ehcache 2.10.6 – Mukul Goel Aug 03 '22 at 12:37
  • @MukulGoel What should be the expected response in this scenario? – Kumaresh Babu N S Aug 03 '22 at 12:45
  • @KumareshBabuNS In which scenario? when I am throwing the new runtime exception in class A? the expected response should be that nothing is cached and when I make the same call from swagger again, class A calls class B and Class B tries to reach out to the external API again to fetch the response. thereby proving that when an exception was thrown within the transaction. Nothing was cached – Mukul Goel Aug 03 '22 at 12:49
  • The actual use case is that. When I receive the response from Class B to Class A. In the class A method. I will validate the response and if validation fails, throw a spring validation exception. In this scenario, I do not want to cache this bad data. – Mukul Goel Aug 03 '22 at 12:51

1 Answers1

4

There should be other ways, but the following could be a valid solution.

The first step will be to define a custom exception in order to be able to handle it later as appropriate. This exception will receive, among others, the name of the cache and the key you want to evict. For example:

public class CauseOfEvictionException extends RuntimeException {

  public CauseOfEvictionException(String message, String cacheName, String cacheKey) {
    super(message);
    
    this.cacheName = cacheName;
    this.cacheKey = cacheKey;
  }

  // getters and setters omitted for brevity
}

This exception will be raised by your B class, in your example:

@Service
Class A{

  @Autowired
  B b;

  @Transactional
  public List<Data> getAllBusinessData(){
 
    List<Data> dataList = b.getDataFromSystem("key");

    // Sorry, because in a certain sense you need to be aware of the cache
    // name here. Probably it could be improved
    throw new CauseOfEvictionException("test", "cacheName", "key");

  }
}

Now, we need a way to handle this kind of exception.

Independently of that way, the idea is that the code responsible for handling the exception will access the configured CacheManager and trigger the cache eviction.

Because you are using Spring Boot, an easy way to deal with it is by extending ResponseEntityExceptionHandler to provide an appropriate @ExceptionHandler. Please, consider read for more information the answer I provided in this related SO question or this great article.

In summary, please, consider for example:

@ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
  
  @Autowired
  private CacheManager cacheManager;

  @ExceptionHandler(CauseOfEvictionException.class)
  public ResponseEntity<Object> handleCauseOfEvictionException(
    CauseOfEvictionException e) {
    this.cacheManager.getCache(e.getCacheName()).evict(e.getCacheKey());

    // handle the exception and provide the necessary response as you wish
    return ...;
  }
}

It is important to realize that when dealing with keys composed by several arguments by default (please, consider read this as well) the actual cache key will be wrapped as an instance of the SimpleKey class that contains all this parameters.

Please, be aware that this default behavior can be customized to a certain extend with SpEL or providing your own cache KeyGenerator. For reference, here is the current implementation of the default one provided by the framework, SimpleKeyGenerator.

Thinking about the problem, a possible solution could be the use of some kind of AOP as well. The idea will be the following.

First, define some kind of helper annotation. This annotation will be of help in determining which methods should be advised. For example:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EvictCacheOnError {
}

The next step will be defining the aspect that will handle the actual cache eviction process. Assuming you only need to advice Spring managed beans, for simplicity we can use Spring AOP for that. You can use either an @Around or an @AfterThrowing aspect. Consider the following example:

@Aspect
@Component
public class EvictCacheOnErrorAspect {

  @Autowired
  private CacheManager cacheManager;

  @Around("@annotation(your.pkg.EvictCacheOnError)")
  public void evictCacheOnError(ProceedingJoinPoint pjp) {
    try {
      Object retVal = pjp.proceed();
      return retVal;
    } catch (CauseOfEvictionException e) {
      this.cacheManager.getCache(
          e.getCacheName()).evict(e.getCacheKey()
      );

      // rethrow 
      throw e;
    }    
  }
}

The final step would be annotate the methods in which the behavior should be applied:

@Service
Class A{

  @Autowired
  B b;

  @Transactional
  @EvictCacheOnError
  public List<Data> getAllBusinessData(){

    List<Data> dataList = b.getDataFromSystem("key");

    throw new CauseOfEvictionException("test", "cacheName", "key");
  }
}

You may even try generalizing the idea, by providing in the EvictCacheOnError annotation all the necessary information you need to perform the cache eviction:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EvictCacheOnError {
    String cacheName();
    int[] cacheKeyArgsIndexes();
}

With the following aspect:

@Aspect
@Component
public class EvictCacheOnErrorAspect {

  @Autowired
  private CacheManager cacheManager;

  @Autowired
  private KeyGenerator keyGenerator;

  @Around("@annotation(your.pkg.EvictCacheOnError)")
  // You can inject the annotation right here if you want to
  public void evictCacheOnError(ProceedingJoinPoint pjp) {
    try {
      Object retVal = pjp.proceed();
      return retVal;
    } catch (Throwable t) {
      // Assuming only is applied on methods
      MethodSignature signature = (MethodSignature) pjp.getSignature();
      Method method = signature.getMethod();
      // Obtain a reference to the EvictCacheOnError annotation
      EvictCacheOnError evictCacheOnError = method.getAnnotation(EvictCacheOnError.class);
      // Compute cache key: some safety checks are imperative here,
      // please, excuse the simplicity of the implementation
      int[] cacheKeyArgsIndexes = evictCacheOnError.cacheKeyArgsIndexes();
      Object[] args = pjp.getArgs();
      List<Object> cacheKeyArgsList = new ArrayList<>(cacheKeyArgsIndexes.length);
      for (int i=0; i < cacheKeyArgsIndexes.length; i++) {
        cacheKeyArgsList.add(args[cacheKeyArgsIndexes[i]]);
      }
      
      Object[] cacheKeyArgs = new Object[cacheKeyArgsList.size()];
      cacheKeyArgsList.toArray(cacheKeyArgs);

      Object target = pjp.getTarget();

      Object cacheKey = this.keyGenerator.generate(target, method, cacheKeyArgs);

      // Perform actual eviction
      String cacheName = evictCacheOnError.cacheName();
      this.cacheManager.getCache(cacheName).evict(cacheKey);

      // rethrow: be careful here if using in it with transactions
      // Spring will per default only rollback unchecked exceptions
      throw new RuntimeException(t);
    }    
  }
}

This last solution depends on the actual method arguments, which may not be appropriate if the cache key is based on intermediate results obtained within your method body.

jccampanero
  • 50,989
  • 3
  • 20
  • 49
  • Thank you for taking the time out to answer. I am trying out your solution. In my use case, The method in class B has 3 parameters. Can you suggest how can I create the `key` object for , the method accepts a single object, not sure how to convert 3 parameters to cache key. `this.cacheManager.getCache(e.getCacheName()).evict(keyObject); method? ` – Mukul Goel Aug 03 '22 at 21:07
  • Nevermind on above. I found that spring uses `SimpleKey simpleKey = new SimpleKey(Object... keyElements)` when using @cacheable framework. Trying this out now. – Mukul Goel Aug 03 '22 at 21:14
  • Sorry for the late reply. You are welcome @MukulGoel. I am happy to see that you were able to find yourself how to create a cache key according to your needs. I hope the proposed solution is helpful. – jccampanero Aug 03 '22 at 22:16
  • Thank you for the solution. it does work as expected. For the completeness of the answer can you please update the answer to include the concept that for methods annotated with `cacheable` that has multi parameters we need to provide an instance of `SimpleKey` They key scenarios are listed here. https://www.logicbig.com/tutorials/spring-framework/spring-integration/cache-key-generation.html – Mukul Goel Aug 03 '22 at 22:53
  • 1
    Thank you very much for the feedback @MukulGoel. I am happy to hear tat the solution worked as expected. Yes, of course, I updated the answer with the information you provided and some additional information. I hope it helps as well. – jccampanero Aug 03 '22 at 23:14