javajarjava-ioserviceloader

Opening service declaration file inside a Jar


List<URL> resources = new ArrayList<>();
try {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

Enumeration<URL> importedResources = classLoader.getResources("META-INF/services");
while (importedResources.hasMoreElements()) {
    resources.add(importedResources.nextElement());
}

Map<String, String> fileContents = Maps.newHashMap();

for (URL resource : resources) {
    InputStream stream = classLoader.getResourceAsStream(resource.getFile());
    Properties props = new Properties(); // for debug
    props.load(stream); // for debug
    BufferedReader reader = new BufferedReader(new InputStreamReader(resource.openStream()));
    String line;
    while ((line = reader.readLine()) != null) { // Line is always null except for the declarations in my own project
        String fileName = line;
        if (!fileName.endsWith("/")) {
            URL fileUrl = new URL(resource + "\\" + fileName);
            BufferedReader fileReader = new BufferedReader(new InputStreamReader(fileUrl.openStream()));
            StringBuilder content = new StringBuilder();
            String fileLine;
            while ((fileLine = fileReader.readLine()) != null) {
                content.append(fileLine);
            }
            fileReader.close();
            fileContents.put(fileName, content.toString());
        }
    }
}
System.out.println(fileContents);

} catch (IOException e) {
throw new RuntimeException(e);
}

I have been trying to read the service declaration files in my project (then saving them as a Map with Interface name as key and provider names as value), including the ones from imported Jars, but got stuck at reading files from inside JARS. Rhe URL resources will load correctly and importedResources does contain the URI to the files but when trying to readLine it will return null.

Project is Maven, URLs look something like this:

jar:file:/C:/Users/xxx/.m2/repository/org/glassfish/hk2/hk2-locator/2.5.0-b42/hk2-locator-2.5.0-b42.jar!/META-INF/services

Any suggestions? or is there a different approach instead?


Solution

  • classLoader.getResources("META-INF/services");

    This does not work. classloaders don't "do" directories, nor do they "do" a "list" command. This is why META-INF/services exists in the first place! Instead of 'give me a list of e.g. all class files on the classpath' which is simply not an abstraction offered by classloaders, you do have the operation "give me all variants of resource X, i.e. if that resource exists more than once I want them all", and that is used by SPI (META-INF/services): Compiled code contains a well-known file name that lists class names. This solves the 'cannot run list commands' dilemma: There is no need to list anything, just get the services file for the interface you are interested in, and then load each class listed in that file.

    This situation is ordinarily fixed by using SPI. You want some sort of bizarro meta-SPI where you want a list of all SPI services for which some file exists. This simply isn't a thing. You can't do what you want.

    You COULD hack it - determine which jars and dirs-on-disk are part of the classpath and scan those. This requires a ton of code, you need to ask for a well known resource (not a dir, an actual file), toString() the URL you get, and then tear that to pieces to know what to do. This doesn't fully fit the rules - a classloader doesn't have to be jar/disk based at all, it could generate resources on the fly, query them from a network, or load them from a database. Given that it's a pluggable architecture and plugins don't (and cannot) implement a "list" command, such hacks are incomplete.

    And thus, not a good idea.

    Whatever made you think: "I know! I'll just list all files in the META-INF/services directory!" - that was the wrong answer to whatever question you had that led you to try that answer.

    Note that .getResource("META-INF/services") does sometimes work. But the specs do not guarantee it, and, indeed, as you found, on most platform/classpath-source combinations it doesn't. The 'bug' is that it sometimes works at all, it's not really supposed to (the spec doesn't define that it should work. It doesn't exactly demand that it never does either).