How To Cache Java ServiceLoader.Provider Efficiently

80 views Asked by At

I currently working on a JPMS project and would like to be able to cache the Providers retrieved on ServiceLoader.load() to be able to use their get() method later for a new set of service instances - without a reference to the ServiceLoader necessarily. In short, retrieve a collection of them from some map and return them as ServiceLoader.Provider<.T>

Even shorter, how do I do:

public static <T> List<ServiceLoader.Provider<T>> getProvidersOf(Class<T> clazz){
    return (List<ServiceLoader.Provider<T>>) servicesProvidersMap.get(clazz);
    }

For a map: Map<Class<?>, List<ServiceLoader.Provider<?>>> servicesProvidersMap = new ConcurrentHashMap<>();

What have I tried:

  • Cast it to object then back to Provider<.T>
  • Used the iterator, but that returns new T instances, not Provider<.T>
  • Not using the streams api. However, "for" uses the iterator.

What does work, but isn't what I'm looking for:

  • Individually casting each provider when adding them to a new list instance. But that is not in line with "having the value pre-calculated and ready to go".

.

public static <T> List<ServiceLoader.Provider<T>> getProvidersOf(Class<T> clazz){
   List<ServiceLoader.Provider<?>> cached = servicesProvidersMap.get(clazz);
   if(cached != null) {
        List<ServiceLoader.Provider<T>> casted = new CopyOnWriteArrayList<>();
        cached.forEach(provider -> casted.add((ServiceLoader.Provider<T>) provider));
        return casted;
    }
    [...on empty cache]
}
  • Caching the loader and re-retrieving the Providers every call. Same as above for my purposes really.

.

List<ServiceLoader.Provider<T>> hereWeGoAgain = getLoaderFor(clazz)
        .stream()
        .collect(Collectors.toCollection(CopyOnWriteArrayList::new));

I understand that a ServiceLoader.Provider instance may be some transition object, and not intended to be kept around, however, it would provide me a great deal of options and flexibility if it was possible.

2

There are 2 answers

1
Akhilesh Pandey On

You can create a helper class to hold the map of providers and encapsulate the necessary casting logic,

public class ServiceProviderMap {
    private final Map<Class<?>, List<ServiceLoader.Provider<?>>> servicesProvidersMap = new ConcurrentHashMap<>();

    public <T> void addProvider(Class<T> clazz, ServiceLoader.Provider<T> provider) {
        servicesProvidersMap.computeIfAbsent(clazz, k -> new CopyOnWriteArrayList<>()).add(provider);
    }

    public <T> List<ServiceLoader.Provider<T>> getProvidersOf(Class<T> clazz) {
        List<ServiceLoader.Provider<?>> cached = servicesProvidersMap.get(clazz);
        if (cached != null) {
            // The unchecked cast here is safe, because we only ever put providers of type T into the list for clazz
            @SuppressWarnings("unchecked")
            List<ServiceLoader.Provider<T>> result = (List<ServiceLoader.Provider<T>>) (List<?>) cached;
            return result;
        } else {
            // handle empty cache, perhaps by returning an empty list
            return new ArrayList<>();
        }
    }
}

here addProvider method puts providers into the map ensuring they have the correct type. When you retrieve providers with getProvidersOf, it performs the unchecked cast, which is safe because of the way providers were added.

This approach minimizes unchecked casts and confines them to a single place, ensuring type safety and reducing potential issues caused by incorrect casting. However, be aware that this class is not thread-safe. If multiple threads might be adding and retrieving providers at the same time, you would need to add appropriate synchronization.

0
G. B. Wanscher On

Found A Solution

Developers answer where fine, but all that was needed was just the "chain-casting" of:

(List<Provider<T>>) (List<?>) value

The method now looks like this:

public static <T> List<Provider<T>> getProvidersOf(Class<T> clazz){
    return (List<Provider<T>>) (List<?>) servicesProvidersMap.computeIfAbsent(
            clazz, k -> getLoader(clazz)
                    .stream()
                    .collect(Collectors.toCollection(CopyOnWriteArrayList::new)));
}

Removed my old post with the proxy class as that didn't work at compile time although it gave no error.