Tomcat-正统的类加载器架构
-
对于一个web服务器如tomcat 需要实现如下功能:
-
1 不同web应用所使用的类库需要相互隔离,不能互相影响,比如webapp1使用spring3.1依赖库,webapp2使用了spring3.2的依赖库,那么这两个应用依赖类库必须隔离。
-
2 不同web应用所使用的基础类库可以共享,比如两个webapp都要使用java.lang的类库,那么需要进行共享,如果每个web应用都加载一份基础类库太浪费内存空间。
-
3 web应用程序的类库不能影响到tomcat本身的代码。tomcat本身的代码类库也不能影响到web应用程序的类库。
-
4 支持热替换,比如修改jsp文件代码,不需要重启应用就可以在浏览器看到变化。
-
为了支持以上功能,单独一个classpath路径无法满足要求,需要提供多个classpath供用户存放第三方类库。每一个classpath路径会对应一个自定义类加载器去加载放置在里面的类库。
-
tomcat的存放类库的目录有
/common目录:tomcat服务器和web应用共同使用的类库。
/server目录:只能由tomcat服务器使用,对于web应用不可见。
/shared目录:被所有web应用程序共同使用,对tomcat服务器不可见。
/webapp/web-info:当前web应用私有,对tomcat和其他web应用不可见。
为了支持这套规则,tomcat自定义了很多类加载器,这些类加载器使用经典双亲委派模型来实现。 -
tomcat用到的类加载器以及委派关系如下
-
前三个是JDK默认的类加载器,后面几个是jdk自定义的类加载器。
-
common类加载器用来加载/common目录的类,cataline类加载器用来加载/server目录的类,shared类加载器用来加载/shared目录的类,webapp类加载器用来加载/webapp/web-info目录的类。除此之外,每一个jsp文件对应一个jsp类加载器。
-
根据这种委派关系,common类加载器加载的类库可以被整个tomcat环境使用,cataline类加载器对应类库只能被tomcat程序本身使用,share类加载器对应类库可以被所有web应用访问,webapp类加载器是每个web应用对应一个,可以被当前web应用访问。每个jsp类加载器只能对应一个jsp文件,可以实现jsp文件热替换。
-
tomcat默认没有实现cataline和share两个目录,即只有 common — webapp — jsp 这个委派流程,如果需要更复杂委派流程,可以修改配置文件,使用更复杂的委派机制。
-
通过这种机制,如果common类库的代码需要访问webapp目录类库代码,可以使用线程上下文类加载器来进行类加载访问。
-
但是在tomcat7好像破坏了双亲委派机制,先加载webapp的类库,再加载tomcat的类库,tomcat往上的类库还是双亲委派。
另外springboot对类加载行为也有一些变化,总之在web应用中的线程上下文类加载器还是webappclassloader。
破坏双亲委派模型的情况1 SPI机制
- 根据双亲委派模型,只能用户类调用核心类库如果反向调用就会出现错误,比如核心类库的类a调用classpath目录下的类b时,由于类a是由启动类加载器加载的,那么根据托管机制类a调用类b时类b只能由启动类加载器进行加载,但是启动类加载器在lib目录下无法加载到类b就会报错。
- 但是在一些情况下可能出现核心类库代码调用用户自定义类的情况,此时必须要破坏双亲委派模型。比如SPI机制,在该机制下模块的接口类在核心类库编写,而模块的实现类放在用户的classpath目录下,那么就必然存在核心类库的接口类调用用户目录的实现类这种情况。
- 比如接口类Persion在核心类库,实现类Persionimpl在用户类库,那么当我们在核心类库代码中加载Persionimpl类时就会出现问题,因为用户类库对于核心类库是不可见的,核心类库委托启动类加载器无法加载用户类库的实现类。为了解决这个问题,jvm提供了一个工具类ServiceLoader,根据这个工具类就可以实现从接口类在用户目录寻找实现类的功能,但是ServiceLoader类本身也是核心类库,仅仅在ServiceLoader类中通过反射加载Persionimpl也是无法加载到的。其实ServiceLoader类内部使用的是线程上下文类加载器来加载实现类的。如下,如果没有传入自定义类加载器就会默认使用线程上下文类加载器。
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
- 线程上下文类加载器可以通过Thread类的一个方法进行设置,如果当前线程没有设置就会继承父线程的线程上下文类加载器,如果全局环境都没有设置,那么线程上下文类加载器默认就是应用程序类加载器。这样我们就可以在高层类获取线程上下文类加载器来加载用户类,相当于启动类加载器向下委托线程上下文类加载器来加载用户类。这样就破坏了双亲委派机制向上委托的原则。
- 可以看出线程上下文类加载器的本质是,一个类加载器对象可以在一个线程环境中共享,这样只要在一个线程环境中,无论是哪个层级的类库都可以访问到线程上下文类加载器,我们也可以把自己定义的类加载器设置为线程上下文类加载器。
ServiceLoader通过线程上下文类加载器可以访问用户类库了,但是ServiceLoader怎么知道一个接口类的实现类的类名是什么呢?其实这里使用了一个配置约定,默认ServiceLoader会从classpath目录的 META-INFO/services目录寻找接口和实现类的对应关系配置,通过这个配置文件就知道应该加载接口的哪些实现类。
public final class ServiceLoader<S> implements Iterable<S>
//配置文件的路径
private static final String PREFIX = "META-INF/services/";
//加载的服务类或接口
private final Class<S> service;
//已加载的服务类集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
//类加载器
private final ClassLoader loader;
//内部类,真正加载服务类
private LazyIterator lookupIterator;
}
-
总之高层实例化底层的类时,必须通过线程上下文类加载器以及接口-实现类配置文件帮助高层的Classloader找到并加载该类。
-
典型应用就是JDBC机制,DirverManager属于核心类由启动类加载器加载,具体的mysql连接jar包属于用户类存放在用户类库,由应用程序类加载器加载。mysql连接类会实现Driver接口创建 com.mysql.jdbc.Driver 实现类。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//...
}
- 在mysql连接jar包中有 META-INF/services目录配置了 java.sql.Driver接口 和 com.mysql.jdbc.Driver实现类对应关系
- 获取mysql连接对象的代码如下
// Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test?&useSSL=false&characterEncoding=utf-8",
"root",
"123456");
- 在DriverManager类的静态代码块调用了loadInitialDrivers方法
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
- 在loadInitialDrivers方法中调用了ServiceLoader类实现了SPI机制进行服务实现类加载。
private static void loadInitialDrivers() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
driversIterator.next();
}
}
});
}
- 上一步已经找到了MySQL中的com.mysql.cj.jdbc.Driver全限定类名,当调用next方法时,就会创建这个类的实例。它就完成了一件事,向DriverManager注册自身的实例。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
//注册
//调用DriverManager类的注册方法
//往registeredDrivers集合中加入实例
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
- 紧接着就可以创建connect连接对象进行使用了。
- 整体流程就是DriverManager类调用ServiceLoader类,ServiceLoader类内部使用了线程上下文类加载器加载用户目录Driver实现类进行使用。
- 其实只要自己遵循JDBC规范和SPI规范,自己也可以实现出类似的功能。
- 既然这些jdbc驱动jar包扮演核心类的功能,那干嘛不直接把它放在核心类库中,而是要作为用户代码存在呢?我们可以想想,jdbc只是一个接口,它的实现类有很多很多,每种jdbc又有不同的版本,如果放在核心类库太麻烦了,每一次变动都要修改核心类库的代码。
破坏双亲委派模型的情况2 OSGI的类加载架构
-
OSGI是一个基于Java的动态模块化规范,可以进行模块热替换,热部署,比如程序升级时不需要重启整个应用,只需要替换指定模块代码即可。基于OSGI标准的实现技术有 jBoss。
-
在程序运行期间当我们对一个模块的代码进行热替换时,相当于要把旧的jar包替换为新的jar包,但是由于类加载器的缓存机制,即使换了新的jar包也不会生效,因为程序已经把老的jar包的类缓存了。因此,为了实现热替换,我们必须为每一个模块绑定一个自定义类加载器,再每一次替换模块代码时把类加载器也替换掉,这样新的jar包所有代码才会被系统重新加载。
-
OSGI标准为了实现模块之间的灵活依赖与调用,把原来树状的类加载委派关系改进成为了网状的类加载委派关系,与SPI机制通过线程上下文类加载器打破双亲委派不同,它直接从代码层面打破了双亲委派模型。
-
比如有 A B C 三个模块,模块B依赖 模块 A C 和基础类库,模块C依赖了模块A。那么这三个模块类加载委派关系如下
-
OSGI的类加载规则如下
-
但是OSGI标准也为程序带来了更大的复杂度,可能产生线程死锁和内存泄漏的风险。我们知道进行类加载时,为了防止多个线程同时加载一个类,会对类进行加锁。
-
如果模块A依赖模块B,而模块B又依赖模块A,那么当a加载b同时b加载a时,很容易产生死锁问题,a的类加载器加载b的类时首先会将a的类加载器锁定,然后委派b的类加载器会去锁定b的类加载器,此时b的类加载器刚好加载a的类,那么会出现两个线程相互持有锁,互相等待对方释放锁的死锁情况。
字节码生成技术
- 字节码生成技术就是在程序运行时手动生成class二进制字节码,比如 CGlib,web服务器中jsp编译器,动态代理技术等。
- 动态代理的优势不在于省去了编写代理类那一点工作量,而是实现了可以在原始类和接口还未知的时候,就确定了代理类的代理行为。将原始类和代理类脱离直接联系就可以灵活应用于不同应用场景中。
类加载机制实操
- 从指定目录加载class文件
URLClassLoader myClassLoader1 = new URLClassLoader(new URL[] {
new URL("file:D:/test1/") });
Class<?> myClass1 = myClassLoader1.loadClass("com.demo.UDF");
System.out.println(myClass1.newInstance());
- 从指定jar包加载class文件
URLClassLoader myClassLoader1 = new URLClassLoader(new URL[] {
new URL("file:D:/test/1.jar") });
Class<?> myClass1 = myClassLoader1.loadClass("com.demo.Persion");
- 如果用同一个类加载器(必须是自定义类加载器这一层级)加载两次persion,实际只会加载一次。
URLClassLoader myClassLoader1 = new URLClassLoader(new URL[] {
new URL(