双亲委派机制
JVM 中加载类,需要循序双亲委派机制。即 子加载器 委托父加载器 加载类, 当且仅当父加载器加载不了时,才由 子加载器 加载类。
JVM 中有以下 默认的 3 个类加载。 Bootstrap、Extention、Application。Bootstrap 是最顶层的类加载
- Bootstrap ClassLoader:负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
- Extention ClassLoader:负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
- Application ClassLoader:负责加载当前应用的classpath下的所有类
- 用户自定义 ClassLoader: 用户自定义的类加载器,可加载指定路径的class文件
举个例子:现在加载 User.class
- Application ClassLoader 询问 Extension ClassLoader 能不能加载
- Extension ClassLoader 询问 Bootstrap ClassLoader 能不能加载
- 当 Bootstrap 返回不能加载时,Extension 会看自己能不能加载
- 当 Extension 返回不能加载时, Application 则自己加载。
优点
确保类只有 1 个,且避免外部注入不安全的类。
缺陷
当 基础类需要加载 用户类时,则会出现无法加载的情况。
举个例子: java.sql.DriverManager
DriverManager 中,需要加载各个厂商实现的驱动类。但是由于 DriverManager 是被 Bootstrap 类加载加载的。因此,该类当前的类加载就是 Bootstrap, 而 Bootstrap 是无法加载 各个厂商实现的 驱动类的。除非用 Application ClassLoader 加载 驱动类。
如果在 DriverManager 中使用 Application ClassLoader 加载驱动类,则破坏了双亲委派机制。因为 Bootstrap(父加载器) 委托 Application(子加载器)加载类。
破坏双亲委派机制
为什么要破坏双亲委派机制
- 需要 基础类加载用户类
- 需要实现热部署、热加载
在 JAVA 中要破坏双亲委派机制有 2 种常用的方法。
- SPI。经典代表为 DriverManager 加载
java.sql.Driver - 继承 ClassLoader 类,并重写 loadClass 方法。经典代表为 tomcat 实现的热部署
ClassLoader#loadClass
public class ClassLoaderTest extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
// 因为传入的不是 类的全限定名称,而是 XX.class
// 因此测试的时候,要加载的类需要与 ClassLoaderTest 同一个目录下
InputStream is = this.getClass().getResourceAsStream(fileName);
// 当前 class 获取不到资源,交由 父加载加载
if (is == null) {
return super.loadClass(name);
}
Class cls;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len;
while ((len = is.read(buf)) >= 0) {
baos.write(buf, 0, len);
}
buf = baos.toByteArray();
int i = name.lastIndexOf(46);
if (i != -1) {
String pkgname = name.substring(0, i);
Package pkg = this.getPackage(pkgname);
if (pkg == null) {
this.definePackage(pkgname, (String) null, (String) null, (String) null, (String) null, (String) null, (String) null, (URL) null);
}
}
cls = this.defineClass(name, buf, 0, buf.length);
} catch (IOException ex) {
throw new ClassNotFoundException(name, ex);
} finally {
try {
is.close();
} catch (IOException ex) {
}
}
return cls;
}
}
SPI
JAVA SPI 遵循以下步骤
- 定义接口
- 定义实现类实现 第一步定义的接口
- 在 resources 的
META-INF/service下 用该接口的全限定类名新建文件。 例如org.csp.learn.java.spi.Hello - 将实现类的全限定类名写入
org.csp.learn.java.spi.Hello。
org.csp.learn.java.spi.impl.CatHelloImpl
org.csp.learn.java.spi.impl.DogHelloImpl
- 通过 ServiceLoader API 加载类
ServiceLoader<Hello> load = ServiceLoader.load(Hello.class);
Iterator<Hello> iterator = load.iterator();
while (iterator.hasNext()) {
Hello item = iterator.next();
}
更详细的代码请查阅 Github