1

I'm trying to back-port to J1.8 an application written for J9 (Update4j); it uses ServiceLoader.Provider class and its methods. The original code is:

public static <T extends Service> T loadService(ModuleLayer layer, ClassLoader classLoader, Class<T> type,
                String classname) {
    if (classname != null && !StringUtils.isClassName(classname)) {
        throw new IllegalArgumentException(classname + " is not a valid Java class name.");
    }

    if (classLoader == null) {
        classLoader = Thread.currentThread().getContextClassLoader();
    }

    ServiceLoader<T> loader;
    List<Provider<T>> providers = new ArrayList<>();

    if (layer != null) {
        loader = ServiceLoader.load(layer, type);
        providers.addAll(loader.stream().collect(Collectors.toList()));
    }

    loader = ServiceLoader.load(type, classLoader);
    providers.addAll(loader.stream().collect(Collectors.toList()));

    if (classname != null) {
        // an explicit class name is used
        // first lets look at providers, to locate in closed modules
        for (Provider<T> p : providers) {
            if (p.type().getName().equals(classname))
                return p.get();
        }

        // nothing found, lets load with reflection
        try {
            Class<?> clazz = classLoader.loadClass(classname);

            if (type.isAssignableFrom(clazz)) {

                // What do you mean?? look 1 line above
                @SuppressWarnings("unchecked")
                T value = (T) clazz.getConstructor().newInstance();
                return value;

            } else {
                // wrong type
                throw new IllegalArgumentException(classname + " is not of type " + type.getCanonicalName());
            }
        } catch (RuntimeException e) {
            throw e; // avoid unnecessary wrapping
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    } else {

        if (providers.isEmpty()) {
            throw new IllegalStateException("No provider found for " + type.getCanonicalName());
        }

        List<T> values = providers.stream().map(Provider::get).collect(Collectors.toList());

        long maxVersion = Long.MIN_VALUE;
        T maxValue = null;
        for (T t : values) {
            long version = t.version();
            if (maxVersion <= version) {
                maxVersion = version;
                maxValue = t;
            }
        }

        return maxValue;
    }
}

How can you achieve the same result in J1.8? Is there a best-practice? Unfortunately J1.8 does not have ServiceLoader.Provider and its utility methods. Should I iterate and select? Is there a reference I can study? Thanks

2 Answers2

2

There’s something you can improve or simplify. I wouldn’t waste resources to check the validity of the class name argument but postpone it until the class truly hasn’t found. Or, since the restrictions on class names at JVM level are much less than in Java source code, I wouldn’t check the name at all, as checking whether a matching class exist is already enough.

There is no point in iterating over all providers and adding them to a List, just to iterate over the list and search for a maximum version, when we could search for the maximum version right in the first iteration, without the List.

Note that for both cases, searching for a service with a given name and for searching for the maximum version, we can use the Stream API.

For the reflection fallback, catching ReflectiveOperationException instead of Exception removes the need to catch and re-throw RuntimeException. Also, knowing about Class.cast(…) and Class.asSubclass(…) helps avoiding unchecked casts.

public static <T extends Service> T loadService(
        Object layer, ClassLoader classLoader, Class<T> type, String classname) {
    if (classLoader == null) {
        classLoader = Thread.currentThread().getContextClassLoader();
    }

    Stream<T> s = StreamSupport.stream(
        ServiceLoader.load(type, classLoader).spliterator(), false);

    if(classname == null)
        return s.max(Comparator.comparingLong(T::version))
            .orElseThrow(() -> new IllegalStateException(
                "No provider found for " + type.getCanonicalName()));

    Optional<T> o = s.filter(t -> t.getClass().getName().equals(classname)).findAny();
    if(o.isPresent()) return o.get();

    try {
        // nothing found, lets load with reflection
        Class<?> clazz = classLoader.loadClass(classname);
        // we could also use a single
        // return clazz.asSubclass(type).getConstructor().newInstance();
        // if we can live with a ClassCastException instead of IllegalArgumentException
        if(!type.isAssignableFrom(clazz)) {
            throw new IllegalArgumentException(
                classname + " is not of type " + type.getCanonicalName());
        }
        return type.cast(clazz.getConstructor().newInstance());
    } catch(ReflectiveOperationException e) {
        if(!StringUtils.isClassName(classname)) { // debatable whether needed
            throw new IllegalArgumentException(
                classname + " is not a valid Java class name.");
        }
        throw new RuntimeException(e);
    }
}
Holger
  • 285,553
  • 42
  • 434
  • 765
  • 1
    Thanks for your input, I'm the author of the upstream update4j. I'll look into your code when I have a chance (I'm on mobile now). – Mordechai Jan 16 '20 at 06:17
1

Well, I do not know if this is the best practice but this solves:

public static <T extends Service> T loadService(Object layer, ClassLoader classLoader, Class<T> type, String classname) {
    if (classname != null && !StringUtils.isClassName(classname)) {
        throw new IllegalArgumentException(classname + " is not a valid Java class name.");
    }

    if (classLoader == null) {
        classLoader = Thread.currentThread().getContextClassLoader();
    }

    ServiceLoader<T> loader;

    loader = ServiceLoader.load(type, classLoader);
    Iterator<T> iterator = loader.iterator();

    if (classname != null) {
        // an explicit class name is used
        // first lets iterate on providers, to locate in closed modules
        while (iterator.hasNext()) {
              T p = iterator.next();
              if (p.getClass().getName().equals(classname))
                   return p;
        }

        // nothing found, lets load with reflection
        try {
            Class<?> clazz = classLoader.loadClass(classname);

            if (type.isAssignableFrom(clazz)) {

                // What do you mean?? look 1 line above
                @SuppressWarnings("unchecked")
                T value = (T) clazz.getConstructor().newInstance();
                return value;

            } else {
                // wrong type
                throw new IllegalArgumentException(classname + " is not of type " + type.getCanonicalName());
            }
        } catch (RuntimeException e) {
            throw e; // avoid unnecessary wrapping
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    } else {
        if (!iterator.hasNext()) {
            throw new IllegalStateException("No provider found for " + type.getCanonicalName());
        }
        List<T> values = new ArrayList();
        while (iterator.hasNext()) {
            T p = iterator.next();
            values.add(p);
        }

        long maxVersion = Long.MIN_VALUE;
        T maxValue = null;
            for (T t : values) {
            long version = t.version();
            if (maxVersion <= version) {
                maxVersion = version;
                maxValue = t;
            }
        }

        return maxValue;
    }
}

Hoping this could help someone downgrading Update4j to J1.8. I forked the original project and may be I'll release my public fork for J1.8 later, when it will be completely working.