Reputation: 8467
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;
}
}
Upvotes: 4
Views: 2789
Reputation: 53381
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.
Upvotes: 4