The usecase
I have this simple interface:
interface UserManager {
User getUser(String userName, String password);
void addUser(User newUser);
void addUser(String userName, User newUser);
User findUserByName(String userName);
}
And I provide an implementation that uses JPA and another one that uses JCache, but then falls back to JPA. The first one is straightforward, here is the second one:
@JCache
public class JCacheUserManager implements UserManager {
static final String USERS_CACHE_NAME = "users";
@Inject
private JPAUserManager passThroughUserManager;
@Inject
private CacheManager cacheManager;
@PostConstruct
public void createUserCache() {
cache = cacheManager.getCache(USERS_CACHE_NAME, String.class, User.class);
if (cache == null) {
cache = cacheManager.createCache(
USERS_CACHE_NAME,
new MutableConfiguration<String, User>()
.setTypes(String.class, User.class));
}
}
@Override
public User getUser(String userName, String password) {
User user = cache.get(userName);
if (user != null) {
if (user.getPassword().equals(password)) {
return user;
} else {
return null;
}
}
User userInDb = passThroughUserManager.getUser(userName, password);
if (userInDb != null) {
cache.put(userName, userInDb);
}
return userInDb;
}
@Override
public void addUser(User newUser) {
passThroughUserManager.addUser(newUser);
cache.put(newUser.getUserName(), newUser);
}
@Override
public User findUserByName(String userName) {
User user = cache.get(userName);
if (user != null) {
return user;
}
User userInDb = passThroughUserManager.findUserByName(userName);
if (userInDb != null) {
cache.put(userName, userInDb);
}
return userInDb;
}
}
Now I spot that the methods addUser
and findUserByName
are just putting and getting things in cache.
And in the mean time communicating with the DB layer.
So they are perfect fit for the JCache annoations defined in Chapter 11 of the spec.
So, let’s do that, let’s substitute the calls to the JCache API in those two particular methods with the JCache annotations. First things first, though, we need to declare the cache name as the spec goes:
@CacheDefaults(cacheName = JCacheUserManager.USERS_CACHE_NAME)
public class JCacheUserManager implements UserManager {
static final String USERS_CACHE_NAME = "users";
// ...
}
And then, let’s rework the above mentioned methods. Note, the methods that do not simply read or write data in the cache, but are rather doing some processing on the parameters, are not touched. We only change two of the methods in our class.
@Override
@CachePut
public void addUser(@CacheKey String userName, @CacheValue User newUser) {
passThroughUserManager.addUser(userName, newUser);
}
@Override
@CacheResult
public User findUserByName(@CacheKey String userName) {
return passThroughUserManager.findUserByName(userName);
}
Now let’s deploy this in Payara, start it, and try to somehow get to findUserByName
method.
This is what we get at the end:
java.lang.IllegalArgumentException: Cache users was defined with specific types Cache<class java.lang.String, class bg.jug.guestbook.entities.User> in which case CacheManager.getCache(String, Class, Class) must be used at com.hazelcast.cache.impl.AbstractHazelcastCacheManager.getCache(AbstractHazelcastCacheManager.java:221) at com.hazelcast.cache.impl.AbstractHazelcastCacheManager.getCache(AbstractHazelcastCacheManager.java:63) at fish.payara.cdi.jsr107.impl.PayaraCacheResolverFactory.getCacheResolver(PayaraCacheResolverFactory.java:47) at fish.payara.cdi.jsr107.CacheResultInterceptor.cacheResult(CacheResultInterceptor.java:49)
Why does it happen?
Well, because I combined a typed cache and an untyped cache in one and he same class.
In my @PostConstruct
method I created users cache with type String
as key and User
as value.
However, when the findUserByName()
method was called, the JCache interceptor of Payara that handles @CacheResult
tried to get (or create) a cache with the same name, but this time with types Object
: Object
.
And it failed, as I have already defined this cache with different types.
How I solved it
I changed my @CacheDefaults
annotation to something like this:
@CacheDefaults(cacheName = "x" + JCacheUserManager.USERS_CACHE_NAME)
public class JCacheUserManager implements UserManager {
So now I ended up with two user caches - one for the simple methods and another one for the more sophisticated ones.
Another solution would be to change the types of the cache that I create:
@PostConstruct
public void createUserCache() {
cache = cacheManager.getCache(USERS_CACHE_NAME);
if (cache == null) {
cache = cacheManager.createCache(
USERS_CACHE_NAME,
new MutableConfiguration<Object, Object>()
.setTypes(Object.class, Object.class));
}
}
private Cache<Object, Object> cache;
And then modify all the other methods to cast the objects the get form the cache to User
.
Decide for yourself which solution is uglier.
Why do I want two types of methods on my class?
Yeah, why do I want @CacheResult
and @CachePut
methods along with methods that call directly javax.cache.Cache
?
I don’t know, this is my style of programming.
If I can somehow leave some work to the framework/virtual machine, then I would happily do that.
For example, I use a lot the Java 8 method references.
When in my lambda I call a method, which just receives the lambda parameter and does nothing with it, then I prefer to substitute the whole thing with method reference.
It was designed like that in the language, my IDE proposes that substitution, so why not use it.
Thus in a same class I end up with using several lambdas defined in the cannonical way and several ones with method references.
So I would like to achieve the same thing with JCache as well. I want to let the framework handle some of the methods that do nothing but dumb reading and writing to the cache. But if there’s a method that does something more than that, then take care myself and call the JCache API.
How would I solve that in the API
Obviously, my app server’s @CacherResult
CDI interceptor didn’t have enough information about the types of the cache that I need.
That is why it didn’t call the approriate getCache()
method and I ended up with an Object
:`Object` cache.
But what if I could specify:
@CacheDefaults(cacheName = JCacheUserManager.USERS_CACHE_NAME, cacheKeyType = String.class, cacheValueType = User.class)
public class JCacheUserManager implements UserManager {
}
Then Payara’s CacheResultInterceptor
would know which type of CacheConfiguration
to create, wouldn’t it?
Way to go…
The JCache team is looking into this. See jsr107/jsr107spec#341