目录
简单工厂模式下,假设核心类和工厂方法由启动类加载器加载,应用类由应用类加载器加载,因为双亲委派模式下上层的类加载器无法访问下层ClassLoader加载的类,所以会导致启动类加载器加载的工厂方法无法创建应用类加载器加载的类的实例。在工厂模式中解决的方式就是把工厂方法抽象出来,具体生成实例的逻辑交由子类来实现,简单工厂模式变成抽象工厂模式,这个在上一篇日志中总结到,那么回到双亲委派问题,JVM解决的方式是在启动类加载器代码中新建一个ClassLoader来完成对下层应用类加载器加载的类的实例化,具体来看看它是怎么做的。
上下文加载器
回到JVM里,工厂模式对应的就是核心类提供个外部类的接口,SPI(Service Provider Interface),这个接口就是外部类需要自己实现业务逻辑的接口,拿DocumentBuilderFactory类举例,该类是一个抽象类,可以实现对XML文件的解析,由启动类加载器加载,所以是一个核心类,类中有一个newInstance方法,负责创建并返回一个DocumentBuilderFactory实例,具体实现在FactoryFind.find()方法中:
public static DocumentBuilderFactory newInstance() {
try {
return (DocumentBuliderFactory) FactoryFinder.find(
"javax.xml.parsers.DocumentBuilderFactory",
"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");
} catch (FactoryFinder.ConfigurationError e) {
throw new FactoryConfigurationError(e.getException(), e.getMessage());
}
}
find()方法里传入的字符串“javax.xml.parsers.DocumentBuilderFactory”就是查找类名factoryId,因为子类的实例并不由启动类加载器加载,所以需要通过这个factoryId找到需要生成的实例其对应的类名:
Object provider = findJarServiceProvider(factoryId);
这部分在FactoryFind.find()方法中实现,作用是根据类名生成实例:
private static object findJarServiceProvider(String factoryId)
throw ConfigurationError {
String serviceId = "META-INF/service/" + factoryId;
ClassLoader cl = ss.getContextClassLoader();
InputStream is = ss.getResourceAsStream(cl, serviceId);
BufferedReader rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
String factoryClassName = rd.readLine();
return newInstance(factoryClassName, cl, false, useBSClsLoader);
}
可以看到,findJarServiceProvider方法中通过读取路径“META-INF/service”下的factoryId类名文件获得需要的类,然后根据这个类生成相应的实例对象,具体生成方式就是下面那句,ClassLoader cl,创建一个上下文加载器,通过这个上下文加载器来完成应用类的实例创建,最后return时将这个ClassLoader传入newInstance()方法,也就是说,完成实例加载和创建的代码虽然在启动类加载器中,但并不由它来加载,而是里面创建一个上下文加载器来完成对应用类的加载,以次解决启动类加载器无法访问其他ClassLoader加载的类的问题。其实在默认情况下上下文加载器用的就是应用类加载器,启动类加载器中就是通过这段代码实现对应用类加载器加载类的访问的。
突破双亲委派模式
说到上下文加载器,这里就顺便总结下,通过它可以实现突破双亲委派模式,我们知道JVM默认情况下是开启双亲委派的,这其中有前面日志中说到的因为安全问题,在该模式下越是核心的类由越上层的加载器加载,最核心的类由启动类加载器负责,这些核心类通常是经常被我们开发者程序调用的接口等,这没什么问题,但如果这些核心类里面有方法需要调用到开发者程序的代码的话,那就出问题了,核心类由于是顶层ClassLoader加载的类,它们无法访问下层,通常是应用层类,我之前看的一个例子就是JNDI,如果不使用JNDI的话,在Java开发时需要链接数据库的话,就要硬编码加载JDBC驱动,使用URL连接到数据库等,如果我们用的是MySql数据库,就加载MySql驱动程序,如果换到其他数据库比如Oracle,就要该驱动程序,而加载驱动程序的代码是写死在Java程序代码里的。使用JNDI后就可以定义一个数据源存储JDBC需要用到的参数,包括数据库URL,需要加载的驱动程序等,以次引用数据源,避免了JDBC硬编码的不足。回到主题,JNDI需要进行查找,查找外部如数据库厂商实现的驱动程序等数据源,所以需要访问到外部类,但JNDI又是由启动类加载器加载的,问题就又来了,启动类加载器加载的类无法访问其他ClassLoader加载的类,怎么办,为了解决这个问题,所以引入了上面说的上下文加载器,JNDI使用这个上下文ClassLoader来加载外部需要的代码,实际的操作很复杂,需要重载ClassLoader,通过这种方式可以改变类加载的顺序,即突破了双亲委派模式。
JDK 9双亲委派模型
还记得上两篇日志中用代码一层一层输出某个类类的双亲类,发现在JDK 9版本下应用类加载器的双亲类不是扩展类加载器而是变成了平台类加载器platformClassLoader,可以知道在JDK 9及之后,双亲委派模型发生了改变:
这种模型也突破了原本的双亲委派模式,让判断哪个类是由哪个加载器加载时,不用一级一级往上找,因为可以发现平台类加载器和应用类加载器之间被“打通“了,较上层的平台类加载器可以访问由下层应用类ClassLoader加载的类,突破了原来双亲委派模式下的单向访问。