写在前面
本文隶属于专栏《100个问题搞定Java虚拟机》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!
本专栏目录结构和文献引用请见100个问题搞定Java虚拟机
解答
1. 重写 ClassLoader 的 loadClass()方法
2. Java SPI,比如 JDBC,JNDI等
3. OSGi
补充
重写 ClassLoader 的 loadClass()方法
由于双亲委派模型在JDK1.2之后オ被引入,而类加载器和抽象类java.lang.Classloader则在JDK1.0时代就已经存在,
面对已经存在的用户自定义类加载器的实现代码,Java设计者引人双亲委派模型时不得不做出一些妥协。
为了向前兼容,JDK1.2之后的 java.lang.Classloader 添加了一个新的 protected 方法 findClass,
在此之前,用户去继承 java.lang.ClassLoader 的唯一目的就是为了重写 loadClass方法,
因为虚拟机在进行类加载的时候会调用加载器的私有方法 loadClassInternal ,而这个方法的唯一逻辑就是去调用自己的 loadClass 。
双亲委派的具体逻辑就实现在这个方法之中,JDK1.2 之后已不提倡用户再去覆盖 loadClass 方法,
而应当把自己的类加载逻辑写到 findClass 方法中,在 loadClass 方法的逻辑里如果父类加载失败,则会调用自己的 findClass 方法来完成加载
这样就可以保证新写出来的类加载器是符合双亲委派规则的。
ClassLoader#loadClass 源码(JDK8)
/**
* 加载具有指定二进制名称的类。
* 此方法搜索类的方式与loadClass(String,boolean)方法相同。
* Java虚拟机调用它来解析类引用。调用此方法相当于调用loadClass(name,false)。
*
* @param name–类的二进制名称
*
* @return 结果类对象
*
* @throws ClassNotFoundException–如果找不到类
*/
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
/**
*加载具有指定二进制名称的类。
* 此方法的默认实现按以下顺序搜索类:
* 1. 调用findLoadedClass(String)检查类是否已经加载。
* 2. 在父类加载器上调用loadClass方法。如果父级为null,则使用虚拟机内置的类加载器。
* 3. 调用findClass(String)方法来查找类。
*
* 如果使用上述步骤找到了该类,并且resolve标志为true,则此方法将调用结果类对象上的resolveClass(Class)方法。
*
* ClassLoader的子类被鼓励重写findClass(String),而不是这个方法。
* 除非重写,否则在整个类加载过程中,此方法将同步getClassLoadingLock方法的结果。
*
* @param name–类的二进制名称
*
* @param resolve-如果为true,则解析类
*
* @return 结果类对象
*
* @throws ClassNotFoundException-如果类找不到
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果类找不到的话,非空的父类加载器会抛出ClassNotFoundException
}
if (c == null) {
// 如果仍然找不到的话,会调用findClass为了找到类
long t1 = System.nanoTime();
c = findClass(name);
// 这是已经定义的类加载器,用来记录统计信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
/**
* 查找具有指定二进制名称的类。
*
* 此方法应该由遵循用于加载类的委托模型的类装入器实现重写,并将在检查请求类的父类装入器之后由loadClass方法调用。
*
* 默认实现抛出ClassNotFoundException。
* @param name–类的二进制名称
*
* @return 结果类对象
*
* @throws ClassNotFoundException–如果找不到类
*
* @since 1.2
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
/**
* 返回类加载操作的锁对象。
*
* 为了向后兼容,此方法的默认实现如下所示。
*
* 如果该类加载器对象被注册为具有并行功能,则该方法将返回与指定类名关联的专用对象。
* 否则,该方法返回这个ClassLoader对象。
*
* @param className–要加载的类的名称
*
* @return 类加载操作的锁
*
* 抛出: NullPointerException–如果注册为支持并行且className为null
*
* @see #loadClass(String, boolean)
*
* @since 1.7
*/
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
/**
* 如果Java虚拟机已将此加载程序记录为具有给定二进制名称的类的初始加载程序,则返回具有给定二进制名称的类。
* 否则返回null。
*
* @param name–类的二进制名称
*
* @return 类对象,如果尚未加载该类,则为null
*
* @since 1.1
*/
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name)) return null;
return findLoadedClass0(name);
}
// 这个方法被 Java 虚拟机调用用来加载类
private Class<?> loadClassInternal(String name) throws ClassNotFoundException{
// 为了向后兼容,如果当前的类加载器不支持并行功能,那么需要显示对当前对象加锁
if (parallelLockMap == null) {
synchronized (this) {
return loadClass(name);
}
} else {
return loadClass(name);
}
}
Java SPI,比如 JDBC,JNDI等
双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API。
但世事往往没有绝对的完美,如果基础类又要调用回用户的代码,那该怎么办?
这并非是不可能的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),
但JNDI的目的就是对资源进行集中管理和査找,它需要调用由独立厂商实现并部署在应用程序的 Classpath 下的JNDI接口提供者(SPI, Service Provider Interface)的代码,
但启动类加载器不可能“认识”这些代码啊!那该怎么办?
线程上下文类加载器(Thread Context Class Loader)
这个类加载器可以通过 java.lang.Thread#setContextClassLoader 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承。
如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作。
这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原财,但这也是无可奈何的事情。
Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
java.lang.Thread#setContextClassLoader源码(JDK8)
/**
* 设置此线程的上下文类加载器。
*
* 可以在创建线程时设置上下文类加载器,并允许线程的创建者通过getContextClassLoader提供适当的类加载器,以便在加载类和资源时在线程中运行代码。
*
* 如果存在安全管理器,则使用RuntimePermission(“setContextClassLoader”)权限调用其checkPermission方法,以查看是否允许设置上下文类加载器。
*
* @param cl–此线程的上下文类加载器,或null,表示系统类加载器(否则,表示引导类加载器)
*
* @throws SecurityException-如果当前线程无法设置上下文类加载器
*
* @since 1.2
*/
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
OSGi
由于用户对程序动态性的追求,比如代码热替换(Hotswap)、模块热部署(Hot Deployment)等,
不用停机也不用重启对于个人计算机来说,重启一次其实没什么大不了的,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故,
这种情况下热部署就对软件开发者,尤其是企业级软件开发者具有很大的吸引力。
目前OSGi已经成为了业界“事实上”的Java模块化标准,而OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。
每一个程序模块(OSGi中称为 Bundle)都有个自己的类加载器,当需要更换一个 Bundle时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。