Java 9 is modularized precluding accessing via reflection base Java components. This renders most methods of extending classpath and java.library.path programmatically invalid. How to do it right? And how to make it compatible with java.sql.DriverManager and javax.activation?
Below is the result of hours of research to how to programmatically extend either the classpath or java.library.path in a "authorized" fashion without reflection or trying to access methods or fields that are not public. I will also show how to bypass the java.sql.DriverManager as its "is class authorized" check will fail if the JDBC driver was created with a different ClassLoader than the calling class' ClassLoader. This has been tested on Java 8 and Java 9 and is working in both environments (the URLClassLoader code should work going back to 1.1).
This is the base ClassLoader that will be used throughout the application:
public class MiscTools
{
private static class SpclClassLoader extends URLClassLoader
{
static
{
ClassLoader.registerAsParallelCapable();
}
private final Set<Path> userLibPaths = new CopyOnWriteArraySet<>();
private SpclClassLoader()
{
super(new URL[0]);
}
@Override
protected void addURL(URL url)
{
super.addURL(url);
}
protected void addLibPath(String newpath)
{
userLibPaths.add(Paths.get(newpath).toAbsolutePath());
}
@Override
protected String findLibrary(String libname)
{
String nativeName = System.mapLibraryName(libname);
return userLibPaths.stream().map(tpath -> tpath.resolve(nativeName)).filter(Files::exists).map(Path::toString).findFirst().orElse(super.findLibrary(libname)); }
}
private final static SpclClassLoader ucl = new SpclClassLoader();
/**
* Adds a jar file or directory to the classpath. From Utils4J.
*
* @param newpaths JAR filename(s) or directory(s) to add
* @return URLClassLoader after newpaths added if newpaths != null
*/
public static ClassLoader addToClasspath(String... newpaths)
{
if (newpaths != null)
try
{
for (String newpath : newpaths)
if (newpath != null && !newpath.trim().isEmpty())
ucl.addURL(Paths.get(newpath.trim()).toUri().toURL());
}
catch (IllegalArgumentException | MalformedURLException e)
{
RuntimeException re = new RuntimeException(e);
re.setStackTrace(e.getStackTrace());
throw re;
}
return ucl;
}
/**
* Adds to library path in ClassLoader returned by addToClassPath
*
* @param newpaths Path(s) to directory(s) holding OS library files
*/
public static void addToLibraryPath(String... newpaths)
{
for (String newpath : Objects.requireNonNull(newpaths))
ucl.addLibPath(newpath);
}
}
Early in main() place the following code to handle things like javax.activation.
Thread.currentThread().setContextClassLoader(MiscTools.addToClasspath());
All threads (including those that are created by java.util.concurrent.Executors) inherit the context ClassLoader.
For classes that are loaded from the extended classpath, use the following code:
try
{
Class.forName(classname, true, MiscTools.addToClasspath(cptoadd);
}
catch (ClassNotFoundException IllegalArgumentException | SecurityException e)
{
classlogger.log(Level.WARNING, "Error loading ".concat(props.getProperty("Class")), e);
}
Finally, how to bypass java.sql.DriverManager which checks to see if the calling class of DriverManager.getDriver() ClassLoader is the same ClassLoader used to load the JDBC driver (it won't be if the calling class was loaded by the application ClassLoader but the driver was loaded using the SpclClassLoader).
private final static CopyOnWriteArraySet<Driver> loadedDrivers = new CopyOnWriteArraySet<>();
private static Driver isLoaded(String drivername, String... classpath) throws ClassNotFoundException
{
Driver tdriver = loadedDrivers.stream().filter(d -> d.getClass().getName().equals(drivername)).findFirst().orElseGet(() ->
{
try
{
Driver itdriver = (Driver) Class.forName(drivername, true, addToClasspath(classpath)).newInstance();
loadedDrivers.add(itdriver);
return itdriver;
}
catch (ClassNotFoundException | IllegalAccessException | InstantiationException e)
{
return null;
}
});
if (tdriver == null)
throw new java.lang.ClassNotFoundException(drivername + " not found.");
return tdriver;
}
isLoader ensures we don't load a bunch of the same driver with all of the extra overhead while providing the requested driver. The downside is it requires knowing the class name for the JDBC class (everyone publishes this) instead of just a URL search that DriverManager does but DriverManager requires the JDBC class gets loaded at startup to not having to do the Class.forName function.
Hopefully this will help others avoid the large number of hours I spent refining this approach for an application I wrote that is used across many platforms and in many configurations requiring both the ability to load a class based on the classpath provided in a properties file and to extend the library.path to use native libraries that are not in the default library.path (also delineated in a properties file).