0

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.

  • 1
    Is there a special requirement on the caller’s side to receive a `CopyOnWriteArrayList`? Collecting into a `CopyOnWriteArrayList` is horribly inefficient, so even if you need a `CopyOnWriteArrayList` as result, it’s more efficient to use `new CopyOnWriteArrayList<>(cached)` (or `new CopyOnWriteArrayList<>((List)cached)` for the generic type issue) – Holger Aug 28 '23 at 07:09
  • I wasn't aware of that. And no, I don't think there is. As long as it's threadsafe when in the cache it's fine. It could use something more efficient to collect into – G. B. Wanscher Aug 29 '23 at 10:06
  • 1
    As long as you put only completely constructed lists into the map and the receiver only reads the list(s), there are no requirements to the list implementation, but the map must be thread safe and used correctly. E.g., use a `ConcurrentHashMap` and the `computeIfAbsent` method as shown in your answer and you’re safe regardless of the list implementation. But it’s recommended to enforce the read-only-after-construction behavior, e.g. on up-to-date Java versions, use `return (List>) (List>) servicesProvidersMap.computeIfAbsent(clazz, k -> getLoader(clazz).stream().toList());` – Holger Aug 29 '23 at 10:15

2 Answers2

0

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.

Akhilesh Pandey
  • 855
  • 5
  • 10
  • Think I lost my patience a bit too quick here , as I just found one too. I'm interested in the actual difference tho. Didn't know you could "chain-cast" like that. In your honest opinion, which is safer from a runtime perspective? Mine got less lines of code, but it doesn't mean that less can go wrong. – G. B. Wanscher Jul 30 '23 at 20:47
0

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.