类加载器的代理模式
经典的双亲委派模式
1、启动类加载器BootstrapClassLoader
2、扩展类加载器Extension ClassLoader
3、系统类加载器App ClassLoader
如果一个类加载器收到了一个类加载请求,它不会自己去尝试加载这个类,而是把这个请求转交给父类加载器去完成。每一个层次的类加载器都是如此。因此所有的类加载请求都应该传递到最顶层的启动类加载器中,只有到父类加载器反馈自己无法完成这个加载请求(在它的搜索范围没有找到这个类)时,子类加载器才会尝试自己去加载。委派的好处就是避免有些类被重复加载。
线程上下文加载器
java在设计的时候,将很多功能分割出去,以方便第三方为自己开发组件,这被成为服务提供者接口(SPI)。常见的SPI有jdbc,jndi,jaxp等。这些接口存储在java核心类库中。如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。而实现则是由程序员引入的jar包完成的。作为核心库的一部分,这些SPI的接口应该由系统引导类加载器(Bootstrap ClassLoader)完成加载,系统类加载器显然不能取外部jar包加载它门的实现,也不能代理给App ClassLoader,因为大小辈分乱了。
线程上下文类加载器是从jdk1.2开始引入的。通过java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器App ClassLoader。在线程中运行的代码可以通过此类加载器来加载类和资源。
使用线程上下文类加载器,可以在执行线程中抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类。
具体例子
上面那一段我自己都看不懂。我们具体看看如何做。
当程序运行到
Class.forName("com.mysql.jdbc.Driver")
会执行该类的静态代码块
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
DriverManager类会把这个Driver放在自己的静态变量里
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
如果这次是DriverManager第一次被加载,则DriverManager也会进行自己的初始化
- 初始化JVM启动参数制定的默认JDBC实现的driver类。如果没有,则什么都不会做。
- 通过SPI机制寻找所有Driver的SPI实现类,利用当前线程上下文加载器加载。
当是实际取连接的时候,执行下面代码:
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//将classloader优先置为调用者的类加载器,如果为null则置为本线程的上下文加载器。
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
//多线程同步
synchronized(DriverManager.class) {
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
//轮训已经注册的driver,尝试连接
for(DriverInfo aDriver : registeredDrivers) {
Connection con = aDriver.driver.connect(url, info);
}
}
总结:
class com.mysql.jdbc.NonRegisteringDriver,class com.mysql.jdbc.Driver都是系统加载器加载的。
当需要加载器时,会使用当前类的类加载器。这很合理,因为一般核心类只会调用核心类。但是SPI破坏了这一规则,被核心类使用,但是却只能被系统加载器加载。所以,需要使用线程上下文加载器,不然核心类调用SPI时会找不到类。
具体实现
线程在加载类的时候,首先从自己的父加载器一层层向上委托(注意不是自己加载器的父类),如果没有才在自己的目录中寻找。
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
//父加载器
private final ClassLoader parent;
//ClassLoader.hava
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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 thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
如果Extension ClassLoader仍然cover不住这个类加载,那么则调用native方法进行
java.lang.ClassLoader#findBootstrapClass
tomcat
有一个最基本的刚需,一个tomcat也是用java写的,上面运行的工程也是java写的。如果依赖的是一个类的不同版本,就傻了。。
一张经典的图
- commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
- catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
- sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
每个web应用对应一个webApp类加载器。负责加载本应用的资源。每个webApp的classloader,会优先尝试自己加载。加载不到才会向上级加载器请求。
可以看到tomcat谁一个标准的双亲委派模型。