Search code examples
javaenumsserviceloader

Enum-aware ServiceLoader implementation?


I would like to be able to indicate an enum type as an interface implementation and then load all enums as separate instances/implementations of the interface via the ServiceLoader API. An example of this use case would be to allow downstream users of my API to specify custom values, but provide an enum with standard/common implementations. My interface only requires a String name(), so any enum implements it already.

For example, the CopyOption interface in the Java NIO APIs, with the provided StandardCopyOption enum. Say I wanted to load all CopyOptions, even new ones on the classpath along with the standards, to a single iterator via ServiceLoader (or I'm open to other suggestions!)

I finally got it to work by wholesale copying ServiceLoader and modifying it to try using getEnumConstants when instantiation fails (the part in the try is how it works currently and the part in the catch is what I added/changed):

try {
    S p = service.cast(c.newInstance());
    providers.put(cn, p);
    return p;
} catch (Throwable x) {
    Object[] arr = c.getEnumConstants();
    if (arr == null || arr.length == 0) {
        fail(service, "Provider " + cn + " could not be instantiated", x);  
    }

    List<S> list = new LinkedList<>();
    for (Object o : arr) {
        Enum<?> e = (Enum<?>) o;
        S p = service.cast(e);
        providers.put(cn + e.ordinal(), p);
        list.add(p);
    }
    subiter = list.iterator();
    return subiter.next();
}

I also added some code such that if subiter exists and has next, it is iterated before moving on to the next class name.

My question is: Is there a better way?

In case the end use is not clear, this is now what's possible with the above modifications:

interface ImageType {
    String name();
}

@AutoService(ImageType.class)
enum StandardImageType implements ImageType {
    IMAGE,
    VECTOR,
    RASTER,
    HANDWRITING,
    ICON,
    LOGO,
    SEAL,
    RULE,
    BARCODE
}

Solution

  • With the introduction of Java modules, an alternative to instantiating via default constructor has been added to service providers. But it only work when the provider is in a named module.

    The provider class may declare a public static T provider() method where T is the service type. Then, the provider implementation class doesn’t even need to implement or extend T itself.

    Since neither, arrays nor a generic type like List<ImageType> can be used as service type, we would need another type for potentially encapsulating multiple actual instances, e.g.

    package somemodule;
    
    import java.util.function.Supplier;
    
    public interface ImageType {
        String name();
        interface ImageTypes extends Supplier<ImageType[]> {}
    }
    

    and

    package somemodule;
    
    public enum StandardImageType implements ImageType {
        IMAGE,
        VECTOR,
        RASTER,
        HANDWRITING,
        ICON,
        LOGO,
        SEAL,
        RULE,
        BARCODE;
    
        public static ImageTypes provider() {
            return StandardImageType::values;
        }
    }
    

    and a module declaration like

    module SomeModule {
        uses somemodule.ImageType.ImageTypes;
        provides somemodule.ImageType.ImageTypes with somemodule.StandardImageType;
    }
    

    which allows to write, e.g.

    List<ImageType> all = ServiceLoader.load(ImageType.ImageTypes.class)
            .stream().flatMap(p -> Arrays.stream(p.get().get()))
            .collect(Collectors.toList());
    

    somewhere within the module (or any other module with a uses somemodule.ImageType.ImageTypes; declaration).