Skip to content

Instantly share code, notes, and snippets.

@ivannov
Last active October 2, 2016 23:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ivannov/258ce072b01d0c55dcbc9079c50458d5 to your computer and use it in GitHub Desktop.
Save ivannov/258ce072b01d0c55dcbc9079c50458d5 to your computer and use it in GitHub Desktop.

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…​

@smillidge
Copy link

File a Payara bug as I think we should be able to infer the cache type from the return value and cache key annotation

@ivannov
Copy link
Author

ivannov commented Apr 18, 2016

Do you think it is just Payara specific? I think it was not defined well in the spec?

@gregrluck
Copy link

The JCache team is looking into this. See jsr107/jsr107spec#341

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment