Java 9是模块化的,不允许通过基于反射的Java组件进行访问。这使得大多数以编程方式扩展类路径和java.library.path的方法无效。如何做对?如何使其与java.sql.DriverManager和javax.activation兼容?
以下是数小时研究的结果,研究如何以"授权"的方式以编程方式扩展类路径或java.library.path,而不需要反思或尝试访问非公共的方法或字段。我还将展示如何绕过java.sql.DriverManager,因为如果JDBC驱动程序是用与调用类"ClassLoader"不同的ClassLoader创建的,则其"是否为类授权"检查将失败。这已经在Java 8和Java 9上进行了测试,并且在这两种环境中都可以工作(URLClassLoader代码应该可以追溯到1.1)
这是将在整个应用程序中使用的基本ClassLoader:
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);
}
}
在main()的早期,放置以下代码来处理javax.activation.之类的事情
Thread.currentThread().setContextClassLoader(MiscTools.addToClasspath());
所有线程(包括由java.util.concurrent.Executors创建的线程)都继承上下文ClassLoader。
对于从扩展类路径加载的类,请使用以下代码:
try
{
Class.forName(classname, true, MiscTools.addToClasspath(cptoadd);
}
catch (ClassNotFoundException IllegalArgumentException | SecurityException e)
{
classlogger.log(Level.WARNING, "Error loading ".concat(props.getProperty("Class")), e);
}
最后,如何绕过java.sql.DriverManager,后者检查DriverManager.getDriver()ClassLoader的调用类是否与用于加载JDBC驱动程序的ClassLoader相同(如果调用类是由应用程序ClassLoader加载的,但驱动程序是使用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确保我们在提供请求的驱动程序时,不会加载一堆相同的驱动程序,而带来所有额外的开销。缺点是它需要知道JDBC类的类名(每个人都发布这个),而不是像DriverManager那样只进行URL搜索,但DriverManager要求JDBC类在启动时加载,而不必执行class.forName函数。
希望这将帮助其他人避免我为一个在许多平台和许多配置中使用的应用程序花费大量时间来改进这种方法,该应用程序需要基于属性文件中提供的类路径加载类,并扩展library.path以使用不在默认library.paath中的本地库(也在属性文件中描述)。