Using Class<T> as an attribute for an annotation in Java

Viewed 655

We are trying to create a rudimentary implementation of a Cache in Java. For this, we have created an annotation @Cached defined as:

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

    <T> Class<T> keyClass();

    <V> Class<V> valueClass();

    String cacheId();
}

This leads to an error: @interface members may not have type parameters. This however is needed as we would like to typecast the values of the returned method and store them in the cache. We are using AspectJ for intercepting the method calls whose results need to be cached. What is the other way in which we can achieve the result? Since not all methods have the same signature, we have to rely on the methods marked @Cached for our cache implementation.

UPDATE Here is the class that stores data in the cache:

public class Cache<K extends Serializable, V> {
   private Map<K, CacheEntry<V> cache;
   // Some other fields and accessors
}

The class CacheEntry is defined as:

public class CacheEntry<V> {
   private V value;
   // Some other fields used for invalidating the cache entry and accessors
}

Now when accessing the cache, I would like to do something like this:

cache.getCache().values().stream()
        .map(value -> cached.valueClass().cast(value))
        .collect(Collectors.toList());

In above code, cached is the reference of @Cached annotation used on the method as:

@Cached(keyClass = Long.class, valueClass = Person.class, cacheId = "personCache")
List<Person> findAll();
1 Answers

cached.valueClass().cast(value) is unable to help you in this case, primarily because there's no way for the compiler to enforce that cached.valueClass() is compatible with Cached.valueClass().

Your best way to help cache clients with type casting is to make them specify the target type at read time:

public <V> List<V> getAllCachedValues(Class<V> valueClass) {
    return cache.getCache().values().stream()
    .map(value -> valueClass.cast(value))
    .collect(Collectors.toList());
}

or with this unchecked cast:

public <V> List<V> getAllCachedValues() {
    return cache.getCache().values().stream()
    .map(value -> (V) value)
    .collect(Collectors.toList());
}

Here are some uses to show how unrealistic it would be to attempt to enforce type safety using the annotation:

  1. You can't enforce compatibility between the return type and the annotation metadata:

    @Cached(keyClass = Long.class, valueClass = Person.class, cacheId = "personCache")
    List<Banana> findAll();
    
  2. The client gets to pick the T and V associated with the actual cache instance (otherwise this would be pointless, right?). With that said, this is possible:

    @Cached(keyClass = Long.class, valueClass = Person.class, cacheId = "personCache")
    
    //and then
    Cache<Long, Banana> cacheInstance = cacheManager.getCache("personCache");
    

    Of course, you could add Class parameters to getCache:

    Cache<Long, Banana> cacheInstance = cacheManager.getCache("personCache", 
                        Long.class, Banana.class);
    

    And validate using the actual Class instances. But what would be the point of doing this when you can take the first approach (cast at retrieval)... The bottom line is the caller will ultimately ensure type compatibility, and annotation metadata doesn't help.

In my opinion, @Cached(keyClass = Long.class, valueClass = Person.class, cacheId = "personCache") may help with typing if you're serializing cached values to such formats as JSON and XML, and need the original class just to deserialize them when the cache is hit (expecting the client to cast, of course).

Related