Sled
Sled

Reputation: 18939

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?

Upvotes: 7

Views: 533

Answers (3)

spacepickle
spacepickle

Reputation: 2778

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)

Upvotes: 2

Stevers
Stevers

Reputation: 535

It looks like you're missing the MANFIEST.MF file that directs Spring on how to load the nested jars. Here's an example hierarchy from Spring's documentation. You can read up on how to configure it by going here.

The MANIFEST.MF should contain this (for the structure below):

Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.mycompany.project.MyApplication

Start-Class is your entry point into your application Main-Class is the Loader you need to load the nested jars.

Example structure:

example.war
 |
 +-META-INF
 |  +-MANIFEST.MF
 +-org
 |  +-springframework
 |     +-boot
 |        +-loader
 |           +-<spring boot loader classes>
 +-WEB-INF
    +-classes
    |  +-com
    |     +-mycompany
    |        +-project
    |           +-YourClasses.class
    +-lib
    |  +-dependency1.jar
    |  +-dependency2.jar
    +-lib-provided
       +-servlet-api.jar
       +-dependency3.jar

Upvotes: 0

Siva
Siva

Reputation: 306

Its possible that you are getting driver value from configuration, e.g.

my.driver = 'com.microsoft.sqlserver.jdbc.SQLServerDriver'

And that configuration is returning value with single quotes. Please check your configuration files.

Upvotes: 2

Related Questions