Search code examples
spring-bootjarclassloader

Why can't Jars in Jars see the contents of other Jars in Jars if they are in the same Jar?


tl;dr: The classes in our Spring Boot jar seem to see classes within the bundled jars, but their contents don't seem to be able to. Why?


Our main product is a web app, but all the business logic is centralized in a core mac-guffin-api.jar. mac-guffin-api.jar is not a Spring Boot project, but has a Spring Java config file called net.initech.api.Configuration that initializes all the services and repositories etc. We use MS SQL Server as our backend with the sqljdbc42:jar driver.

We needed to write an ETL that needed to reuse the same business logic from API project so we created a Spring Boot Spring Batch project that imports mac-guffin-api.jar as a Maven dependency. The ETL's configuration (net.initech.etl.Configuration)import's APIs configuration without problem (I can see it from the console logging) but when the API configuration goes to create the database connection it cannot find the driver.

Caused by: java.lang.ClassNotFoundException: 'com.microsoft.sqlserver.jdbc.SQLServerDriver'
    at java.net.URLClassLoader.findClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:94)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Unknown Source)
    at org.apache.tomcat.jdbc.pool.PooledConnection.connectUsingDriver(PooledConnection.java:246)
    ... 113 more

However, I can clearly see that the JAR containing the driver is present. The contents of the ETL jar are (Nb: mac-guffin-api.jar and sqljdbc42-4.2.jar are not unpacked, they are jars in the ETL jar ) :

mac-guffin-etl.jar
|
+- org.springframework.boot.loader...
|
+- BOOT-INF
   |
   +- classes
   |  |
   |  +- com.initech.etl.Main.class
   |  |
   |  +- com.initech.etl.Configuration.class
   |
   +- lib
      |
      +- mac-guffin-api.jar
      |  |
      |  +- com.initech.api.Configuration.class
      |
      +- sqljdbc42-4.2.jar
         |
         +- com.microsoft.sqlserver.jdbc.SQLServerDriver.class

So apparently the class ETL's configuration class can see the content's of the included JARs (or at least the contents of API jar), but they API jar does not seem to be able to see the com.microsoft.sqlserver.jdbc.SQLServerDriver.class in the fellow SQL Server JDBC jar.

I'm even able to do a Class.forName( "com.microsoft.sqlserver.jdbc.SQLServerDriver.class" ) from before the instantiation of the Spring context and it doesn't have a problem.

Is this is a limitation of the class loader? Is this because the API project is not Spring Boot? Is it because of a missing configuration parameter? What is going on here?


Solution

  • Somewhere in your configuration, you have ended up with the classname that is being used as the value:

    'com.microsoft.sqlserver.jdbc.SQLServerDriver'
    

    with single quotes around it. Normally the class name being loaded is printed without quotes, double or single.

    This would explain why you are able to load the class but the API jar is not. Check you configuration/build files for where the driver name is set.

    DEMO

    The only way I can get a message like yours:

    Caused by: java.lang.ClassNotFoundException: 'com.microsoft.sqlserver.jdbc.SQLServerDriver'
    

    and not:

    Caused by: java.lang.ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver
    

    Is to deliberately ask to load a class with single quotes in the name. For example:

    import java.lang.*;
    
    public class myclass {
    
            public static void test(String thename) {
                    System.out.println("trying " + thename);
                    try {
                            myclass test = (myclass) myclass.class
                                    .getClassLoader()
                                    .loadClass(thename)
                                    .newInstance();
                            System.out.println(test.toString());
                    } catch (Exception e){
                            System.out.println("failed to load " + thename);
                            e.printStackTrace();
                    }
            }
    
            public static void main(String[] args) {
                    test("my.package.itwontexist");
                    test("'my.package.itwontexist'");
            }
    }
    

    outputs:

    trying my.package.itwontexist
    failed to load my.package.itwontexist
    java.lang.ClassNotFoundException: my.package.itwontexist
        at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        at myclass.test(myclass.java:10)
        at myclass.main(myclass.java:20)
    trying 'my.package.itwontexist'
    failed to load 'my.package.itwontexist'
    java.lang.ClassNotFoundException: 'my.package.itwontexist'
        at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        at myclass.test(myclass.java:10)
        at myclass.main(myclass.java:21)