Search code examples
javaclassloaderserviceloader

Child first class loader and Service Provider Interface (SPI)


I found a custom class loader, which loads classes by child-first principle. And it works fine, but I faced with the following issue. When I try to load classes that use SPI I get the exception:

Exception in thread "main" java.util.ServiceConfigurationError: test.spi.SayMyNameProvider: test.spi.ImplProvider not a subtype

I created simple SPI project with modules: spi-api, spi-impl and spi-app.

And it works when I use URLClassLoader, however whenever I use ChildFirstClassLoader I get the exception mentioned above:

public class TestMain {
    public static void main(String[] args) throws MalformedURLException {

        //!!! comment ChildFirstClassLoader and uncomment URLClassLoader to get the correct behavior 
        ChildFirstClassLoader classLoader = getCustomClassLoader();
        //URLClassLoader classLoader = getUrlClassLoader();

        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(classLoader);

        try {
            List<SayMyNameProvider> providers = Speaker.providers();
            for (SayMyNameProvider provider : providers) {
                SayMyNameManager sayMyNameManager = provider.create();
                sayMyNameManager.sayIt("main");
            }
            System.out.println("done");

        } finally {
            Thread.currentThread().setContextClassLoader(contextClassLoader);
        }
    }

    private static ChildFirstClassLoader getCustomClassLoader() throws MalformedURLException {
        URL[] urls = getUrls();

        return new ChildFirstClassLoader(urls);
    }

    private static URLClassLoader getUrlClassLoader() throws MalformedURLException {
        URL[] urls = getUrls();

        return new URLClassLoader(urls);
    }

    private static URL[] getUrls() throws MalformedURLException {
        File spiImpl = Paths.get("spi-impl", "target", "spi-impl-1.0.0-SNAPSHOT.jar").toFile();
        File spiApi = Paths.get("spi-api", "target", "spi-api-1.0.0-SNAPSHOT.jar").toFile();

        URL[] urls = new URL[2];
        urls[0] = spiImpl.toURI().toURL();
        urls[1] = spiApi.toURI().toURL();
        return urls;
    }
}

Maybe someone has already faced this problem before and knows how to solve it. I would be grateful for any help or advice.


Solution

  • So, after 3 days I finally got an answer to my question. And it says that I am stupid :) because in the article at the very end the author provided the example with correct behavior and it works in my case. However, it doesn`t work in my other test with slf4j and logback dependencies. But to my surprise, the code without system class loader works. In the nutshell, I try to use different versions of slf4j and logback.

    Pom.xml:

    <dependencies>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
    </dependencies>
    

    TestMain.class

    public class TestMain {
        private static Logger log = LoggerFactory.getLogger(TestMain.class);
    
        public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, MalformedURLException {
            log.info("Hello");
    
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
    
            ChildFirstClassLoader2 classLoader = new ChildFirstClassLoader2(getUrls());
    
            Thread.currentThread().setContextClassLoader(classLoader);
    
            try {
                Class<?> testClass = classLoader.loadClass("petrovskyi.TestClass");
                Object o = testClass.getDeclaredConstructor().newInstance();
                Method test = testClass.getMethod("test");
                test.setAccessible(true);
                test.invoke(o);
    
            } finally {
                Thread.currentThread().setContextClassLoader(contextClassLoader);
            }
        }
    
        private static URL[] getUrls() throws MalformedURLException {
            File libDir = Paths.get("src","main", "resources", "testClasses").toFile();
    
            URL[] urls;
    
            List<URL> urlsList = new ArrayList<>();
    
            URL classUrl = libDir.toURI().toURL();
            urlsList.add(classUrl);
    
            try (Stream<Path> walk = Files.walk(libDir.toPath())) {
                List<File> result = walk.map(Path::toFile)
                        .filter(x -> x.getName().endsWith(".jar"))
                        .collect(Collectors.toList());
    
                for (File jarFile : result) {
                    urlsList.add(jarFile.toURI().toURL());
                }
            } catch (IOException e) {
                throw new RuntimeException("Error while walking through " + libDir + " to find jar files", e);
            }
    
            urls = urlsList.toArray(new URL[0]);
            return urls;
        }
    }
    

    Above I try to get all jars from some directory. The jars are the next:

    • logback-classic-1.3.0-alpha5.jar
    • logback-core-1.3.0-alpha5.jar
    • slf4j-api-2.0.0-alpha1.jar

    TestClass.class

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.slf4j.event.Level;
    import org.slf4j.spi.DefaultLoggingEventBuilder;
    import org.slf4j.spi.LoggingEventBuilder;
    
     public class TestClass {
            private static Logger log = LoggerFactory.getLogger(TestClass.class);
    
            public TestClass() {
            }
    
            public void test() {
                LoggingEventBuilder loggingEventBuilder = new DefaultLoggingEventBuilder(log, Level.ERROR);
                loggingEventBuilder.log(" =========== Hello, World! ===========");
                log.info("Test from test class");
            }
        }
    

    The class above uses LoggingEventBuilder that is not present in slf4j version mentioned in pom.xml

    ChildFirstClassLoader2.class

    public class ChildFirstClassLoader2 extends URLClassLoader {
    
        public ChildFirstClassLoader2(URL[] urls) {
            super(URLs);
        }
    
        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            Class<?> loadedClass = findLoadedClass(name);
            if (loadedClass == null) {
    
                try {
                    if (loadedClass == null) {
                        loadedClass = findClass(name);
                    }
    
                } catch (ClassNotFoundException e) {
                    loadedClass = super.loadClass(name, resolve);
                }
            }
    
            if (resolve) {
                resolveClass(loadedClass);
            }
            return loadedClass;
        }
    
    
        @Override
        public Enumeration<URL> getResources(String name) throws IOException {
            List<URL> allRes = new LinkedList<>();
    
            Enumeration<URL> thisRes = findResources(name);
            if (thisRes != null) {
                while (thisRes.hasMoreElements()) {
                    allRes.add(thisRes.nextElement());
                }
            }
    
            Enumeration<URL> parentRes = super.findResources(name);
            if (parentRes != null) {
                while (parentRes.hasMoreElements()) {
                    allRes.add(parentRes.nextElement());
                }
            }
    
            return new Enumeration<URL>() {
                Iterator<URL> it = allRes.iterator();
    
                @Override
                public boolean hasMoreElements() {
                    return it.hasNext();
                }
    
                @Override
                public URL nextElement() {
                    return it.next();
                }
            };
        }
    
        @Override
        public URL getResource(String name) {
            URL res = null;
    
            if (res == null) {
                res = findResource(name);
            }
            if (res == null) {
                res = super.getResource(name);
            }
            return res;
        }
    }
    

    The output:

    2020-04-10 12:01:39,928 [main] INFO TestMain - Hello
    2020-04-10 12:01:40,095 [main] ERROR TestClass -  =========== Hello, World! ===========
    2020-04-10 12:01:40,097 [main] INFO TestClass - Test from test class