Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 210
Sled Avatar asked Apr 17 '17 20:04

Sled


People also ask

Can jar include other jars?

Adding jars to a single jar is done using the jar command. Suppose you have jarA, jarB and jarC. For your deployment you would need a manifest file too. The manifest would specify the external jars' full path.

Can JAR files be nested?

The Executable Jar File Structure. Application classes should be placed in a nested BOOT-INF/classes directory. Dependencies should be placed in a nested BOOT-INF/lib directory.

What is a BOOT jar?

Spring boot executable jar is defined as a collection of class files that bundles the different classes written to accomplish the task the application is designed for.


2 Answers

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)
like image 60
spacepickle Avatar answered Oct 09 '22 06:10

spacepickle


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.

like image 26
Siva Avatar answered Oct 09 '22 06:10

Siva