Reputation: 79
I'm trying to implement a cache that holds results from a specific business method call and then refreshes itself every 30 minutes.
I was able to accomplish that by using a singleton EJB using a scheduled method; however, every class that calls that business method now has to instead call the method from the singleton that exposes the cached results.
I want to avoid this behaviour and keep the code from these classes as is, so I thought of using an interceptor that would intercept every call to that particular business method and return instead the results from the cache singleton.
However, this solution has the application stalling since the singleton calls the intercepted business method itself to cache its results, so the interceptor intercepts the call (pardon the repetition) and tries to return the result of the singleton method that exposes the cached values, while the singleton is still waiting for the call to the business method to proceed.
The most obvious solution would be to get the method caller from the interceptor, and check if its
class corresponds to the singleton's; if so, proceed with the call, otherwise return the cached results from the singleton. However, it appears that the InvocationContext
object used by the interceptor doesn't expose any methods to access information about the intercepted method's caller. Is there any other way to access the caller's class, or any workaround to this issue?
Here's my singleton class:
@Singleton
@Startup
public class TopAlbumsHolder {
private List<Album> topAlbums;
@Inject
private DataAgent dataAgent;
@PostConstruct
@Schedule(hour = "*", minute = "*/30", persistent = false)
private void populateCache() {
this.topAlbums = this.dataAgent.getTopAlbums();
}
@Lock(LockType.READ)
public List<Album> getTopAlbums() {
return this.topAlbums;
}
}
And here's my interceptor:
@Interceptor
@Cacheable(type = "topAlbums")
public class TopAlbumsInterceptor {
@Inject
private TopAlbumsHolder topAlbumsHolder;
@AroundInvoke
public Object interceptTopAlbumsCall(InvocationContext invocationContext) throws Exception {
// if the caller's class equals that of the cache singleton, then return invocationContext.proceed();
// otherwise:
return this.topAlbumsHolder.getTopAlbums();
}
}
Note that the @Cacheable
annotation is a custom interceptor binding, not javax.persistence.Cacheable
.
EDIT: I modified the interceptor method that way:
@AroundInvoke
public Object interceptTopAlbumsCall(InvocationContext invocationContext) throws Exception {
for (StackTraceElement stackTraceElement : Thread.currentThread().getStackTrace())
if (TopAlbumsHolder.class.getName().equals(stackTraceElement.getClassName()))
return invocationContext.proceed();
return this.topAlbumsHolder.getTopAlbums();
}
But I doubt that's the cleanest solution, and I don't know if it's portable.
EDIT 2: In case it is not clear enough, I need to access information about the invoker class of the intercepted method, not the invoked class that has its method intercepted; that's why I'm iterating over the stack trace to access the invoker's class, but I reckon this is not an elegant solution, even though it works.
Upvotes: 0
Views: 1090
Reputation: 6753
For what you need to do, I'd say use either interceptor or decorator.
Your interceptor is however wrong. Firtly you are missing the basic part, which is a call to InvocationContext.proceed()
that forwards the call to next-in-line interceptor (if there is any) or the method call itself. Secondly, the injection point you placed there is very specific and would only help you if you intercept this very type of bean. Typically, an around invoke interceptor method looks like this:
@AroundInvoke
Object intercept(InvocationContext ctx) throws Exception {
// do something before the invocation of the intercepted method
return ctx.proceed(); // this invoked next interceptor and ultimately the intercepted method
// do something after the invocation of the intercepted method
}
Furthermore, if you want metadata information about what bean was intercepted, every interceptor can inject a special built-in bean just for that. From the metadata, you can gather information on what bean you're currently intercepting. Here is how you get that metadata:
@Inject
@Intercepted
private Bean<?> bean;
Note that interceptors are unaware of what type they intercept, it can be anything and hence you usually need to operate on plain Object
.
Should you need something more specific, CDI offers a Decorator pattern which in basically a type-aware interceptor. It has a special injection point (a delegate) that gives you direct access to the decorated bean. It might possibly fit your scenario even better, take a look at this part of CDI specification explaining Decorators.
Upvotes: 1
Reputation: 955
Iterate over the stack trace to check on TopAlbumsHolder
exists isn't a good way.
To escape invoking the interceptor during calling the getTopAlbums()
from DataAgent
class you can specify the scheduler direct in the DataAgent
which gathers the data and push it into TopAlbumsHolder
. You can do it another way, but your main point direct invoking the getTopAlbums()
within the DataAgent bean without participating proxy (in this case, the interceptor won't apply).
P.S. Pay attention that cached data should be immutable (both the collection and its objects).
Upvotes: 0
Reputation: 3593
There is a misunderstanding.
You don't inject the Object which gets intercepted into the interceptor, but use the invocationContext.
You just need to call invocationContext.proceed()
then there is no recursion.
The result of proceed() you can cache.
Upvotes: 0