打破双亲委派的两种场景

前言

  Java中的双亲委派机制可以保证Java的运行安全,保证Java中的核心类可以被正确安全加载。那有没有打破双亲委派机制的Java应用呢?其实是有的,比如运行Java Web应用的Tomcat容器以及Java专门用来操作数据库的APIJDBC,它们就都打破了双亲委派机制了。为什么它们如此特殊呢?

Tomcat打破双亲委派

  通常使用Java开发Web应用时,最后我们都会将源码打包成一个war包或者jar包,然后扔进Tomcat容器里去运行。通常情况下,一台服务器上只会部署一个Tomcat容器,现在因为公司资源有限,我想在Tomcat里部署多个应用可不可以?肯定是可以的,只要server.port设置不同,就可以在Tomcat里部署多个应用。那现在问题来了,不同应用里可能使用了同一个jar包的不同版本,就比如常见的lombok,就有很多个不同的版本。如果Tomcat还是使用Java默认的双亲委派机制(注:Tomcat也是Java写的),那就只会加载一个lombok?那Tomcat到底加载哪个?还有就是如mysql-connector-java这种比较特殊的,如果mysql是5.7版本的,只能使用mysql-connector-java的特定版本来加载。此时如果Tomcat还是遵从双亲委派就无法支持这种场景

Tomcat的类加载机制

image.png

上图就是Tomcat中的类加载机制。可以看到,最顶层的三个类加载器还是Java中默认的类加载器。Common ClassLoaderCatalina ClassLoaderShared ClassLoaderWebapp ClassLoaderJasper Loader则是Tomcat内部自定义的类加载器。

  • Common ClassLoader加载的类都可以被Catalina ClassLoaderShare ClassLoader使用,从而实现公有类库的公用

  • Catalina ClassLoaderShare ClassLoader各自加载的类则与对方相互隔离,实现了Tomcat自身依赖与Web应用依赖的隔离

  • WebApp ClassLoader加载每个应用/WebApp/WEB-INF/*目录下的资源,WebApp ClassLoader可以使用Share ClassLoader加载的类,但各个
    WebApp ClassLoader之间相互隔离

  • Jasper Loader主要是用来加载JSP文件的。JSP其实也是Java文件,其编译后也会生成一个.class文件

  • Jasper Loader的加载范围仅仅是这个JSP文件所编译出来的那个.class文件,一对一的设计是为了随时丢弃它,以实现热加载功能

  • Tomcat会在后台启动一个线程来监听JSP文件变化,如果这个JSP文件变化了则找到这个JSP对应的Servlet的类加载器对象引用,重新生成新的Jasper Loader类加载器对象赋值,然后加载新的JSP对应的Servlet类,原先的那个类加载器对象会在下次GC被回收

      综上,Tomcat打破了双亲委派机制让每个WebApp ClassLoader加载自己目录下的class文件没有向上传递给父加载器Tomcat通过这样一套类加载机制,实现了各个Web应用之间,相同依赖版本的统一加载,不同依赖版本的分别加载以及随时热加载JSP文件的功能。

JDBC打破双亲委派

  Java开发难免少不了与数据库打交道。而JDBCJava中与数据库连接的APIJDBCDriver是定义在rt.jar包中的,但是它的实现通常是由不同的数据库厂商来完成的,如mysql-connetor-java。同时rt.jar包中的DriverManager类会加载每个Driver接口的实现类并管理它们。那么问题来了,根据类加载机制,某个类需要引用其它类的时候,虚拟机将会用这个类的classloader去加载被引用的类。既然DriverManager是由BootStrap ClassLoader加载,那么各个Driver的实现类理应也由BootStrap ClassLoader来加载。但很显然,Boostrap Classloader显然是无法加载到mysql-connetor-java的,二者明显不在一个目录里,直接报ClassNotFound。因此只能在DriverManager里强行指定下层的classloader来加载Driver实现类,而这就会打破双亲委派模型

JDBC实现方式

  在JDBC 4.0之后,借助于JavaSPI机制,可以实现自动加载。通过查看DriverManager类的代码可以看到,当我们使用DriverManager的时候就会触发static代码块,进而会加载META-INF/services/java.sql.Driver指定的类。

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
    AccessController.doPrivileged(new PrivilegedAction<Void>() { // 1. AccessController,Java安全模型
        public Void run() {
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); // 2. 核心,ServiceLoader就是JDK提供的SPI的实现方式
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next(); // 3. 遍历的过程会触发每个Driver实现类的加载
                }
            } catch(Throwable t) {
              // Do nothing
            }
            return null;
        }
    });
}

  ServiceLoader#load方法中,通过获取Thread.currentThread()context class loader来根据配置加载对应的类。

public static <S> ServiceLoader<S> load(Class<S> service) {

    // Thread.currentThread().getContextClassLoader() 默认是系统类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

  Thread.currentThread().getContextClassLoader()默认返回应用类加载器,这个可以从sun.misc.Launch类中看到。

public class Launcher {

public Launcher() {
    ...
    try {
                               //应用类加载器
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
    Thread.currentThread().setContextClassLoader(this.loader);
    ...
  }


}

总结

  分析了两种打破Java双亲委派的情况,希望大家看完总结消化一下。

posted @ 2022-04-26 23:22  Reecelin  阅读(417)  评论(0编辑  收藏  举报