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?
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.
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.
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.
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.
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)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With