当前位置: 首页 > news >正文

双亲委派模型与Tomcat类加载架构

双亲委派模型

类加载器的划分

  • 启动类加载器(Bootstrap ClassLoader)
  • 扩展类加载器(Extension ClassLoader)
  • 应用程序类加载器(Application Classloader)

启动类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
扩展类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器由sun.misc.Launcher$App-ClassLoader实现。应用程序类加载器也称之为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。应用程序中如果没有自定义类加载器,那一般情况下应用程序类加载器就是程序中默认的类加载器。

双亲委派模型的工作过程

双亲委派模型要求除了顶层的启动类加载器外,其余的父类加载器都应当有自己的父类加载器。这里的父类加载器之前的父子关系一般不会以继承的关系来实现,而都使用组合关系来复用父加载器的代码。
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
双亲委派模型的代码都集中在java.lang.ClassLoader的loadClass()方法中:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

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;
    }
}
复制代码

双亲委派模型的作用

Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序在程序的各种类加载器环境中都是同一个类。

破坏双亲委派模型

Java有许多核心库提供的SPI接口,允许第三方为这些接口提供实现,常见的有JDBC、JNDI等。问题是SPI接口是核心库的一部分,是由启动类加载器来加载的,而SPI接口的实现类是由应用程序类加载器来加载的。根据双亲委派模型,Bootstrap ClassLoader无法委派Application Classloader来加载类(有关Java的SPI可以参考Dubbo SPI中2.1节的Java SPI例子)。为了解决这个问题,我们引入一个线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
我们来看下JDBC的驱动管理类DriverManager的代码:
初始化jdbc驱动类

/**
 * Load the initial JDBC drivers by checking the System property
 * jdbc.properties and then use the {@code ServiceLoader} mechanism
 */
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}
复制代码
private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    // If the driver is packaged as a Service Provider, load it.
    // Get all the drivers through the classloader
    // exposed as a java.sql.Driver.class service.
    // ServiceLoader.load() replaces the sun.misc.Providers()

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            /* Load these drivers, so that they can be instantiated.
             * It may be the case that the driver class may not be there
             * i.e. there may be a packaged driver with the service class
             * as implementation of java.sql.Driver but the actual class
             * may be missing. In that case a java.util.ServiceConfigurationError
             * will be thrown at runtime by the VM trying to locate
             * and load the service.
             *
             * Adding a try catch block to catch those runtime errors
             * if driver not available in classpath but it's
             * packaged as service and that service is there in classpath.
             */
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}
复制代码

注意上面的ServiceLoader的load方法,这里就用到了线程上下文类加载器。

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
复制代码

Tomcat的类加载架构

Tomcat作为一个web容器需要解决下面几个问题:

  1. 部署在同一服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离,因为两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用。
  2. 部署在同一服务上的两个Web应用程序所使用的Java类库可以互相共享。
  3. 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。一般来说,服务器所使用的类库应该与应用程序的类库互相独立。
  4. 支持JSP应用的Web服务器,大多数需要支持Hotswap功能。

Tomcat类加载架构如下图:

CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader分别加载/common/*、/server/*、/shared/*(Tomcat6已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和JSP类加载器通常会存在多个实例,每个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个JSP类加载器。

  1. CommonClassLoader加载路径中的class可以被Tomcat和所有的Web应用程序共同使用
  2. CatalinaClassLoader加载路径中的class可被Tomcat使用,对所有的Web应用程序都不可见
  3. SharedClassLoader加载路径中的Class可被所有的Web应用程序共同使用,但对Tomcat自己不可见
  4. WebappClassLoader加载路径中的Class只对当前webapp可见,对Tomcat和其他Web应用程序不可见

从委派关系可以看出:

  1. 各个WebappClassLoader实例之间相互隔离
  2. CommonClassLoader和SharedClassLoader实现了公有类库的共用
  3. CatalinaClassLoader和SharedClassLoader自己能加载的类与对方相互隔离
  4. JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个Class

先从Tomcat的启动方法bootstrap.main方法入手

/**
 * Main method and entry point when starting Tomcat via the provided
 * scripts.
 *
 * @param args Command line arguments to be processed
 */
public static void main(String args[]) {

    synchronized (daemonLock) {
        if (daemon == null) {
            // Don't set daemon until init() has completed
            Bootstrap bootstrap = new Bootstrap();
            try {
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            daemon = bootstrap;
        } else {
            // When running as a service the call to stop will be on a new
            // thread so make sure the correct class loader is used to
            // prevent a range of class not found exceptions.
            Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
        }
    }

    try {
        String command = "start";
        if (args.length > 0) {
            command = args[args.length - 1];
        }

        if (command.equals("startd")) {
            args[args.length - 1] = "start";
            daemon.load(args);
            daemon.start();
        } else if (command.equals("stopd")) {
            args[args.length - 1] = "stop";
            daemon.stop();
        } else if (command.equals("start")) {
            daemon.setAwait(true);
            daemon.load(args);
            daemon.start();
            if (null == daemon.getServer()) {
                System.exit(1);
            }
        } else if (command.equals("stop")) {
            daemon.stopServer(args);
        } else if (command.equals("configtest")) {
            daemon.load(args);
            if (null == daemon.getServer()) {
                System.exit(1);
            }
            System.exit(0);
        } else {
            log.warn("Bootstrap: command \"" + command + "\" does not exist.");
        }
    } catch (Throwable t) {
        // Unwrap the Exception for clearer error reporting
        if (t instanceof InvocationTargetException &&
                t.getCause() != null) {
            t = t.getCause();
        }
        handleThrowable(t);
        t.printStackTrace();
        System.exit(1);
    }

}
复制代码

看下init方法,init方法中执行Thread.currentThread().setContextClassLoader方法,将当前线程上下文类加载器设为catalinaLoader。

/**
 * Initialize daemon.
 * @throws Exception Fatal initialization error
 */
public void init() throws Exception {

    initClassLoaders();

    Thread.currentThread().setContextClassLoader(catalinaLoader);

    SecurityClassLoad.securityClassLoad(catalinaLoader);

    // Load our startup class and call its process() method
    if (log.isDebugEnabled())
        log.debug("Loading startup class");
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    // Set the shared extensions class loader
    if (log.isDebugEnabled())
        log.debug("Setting startup class properties");
    String methodName = "setParentClassLoader";
    Class<?> paramTypes[] = new Class[1];
    paramTypes[0] = Class.forName("java.lang.ClassLoader");
    Object paramValues[] = new Object[1];
    paramValues[0] = sharedLoader;
    Method method =
        startupInstance.getClass().getMethod(methodName, paramTypes);
    method.invoke(startupInstance, paramValues);

    catalinaDaemon = startupInstance;

}
复制代码

看下initClassLoaders方法。

private void initClassLoaders() {
    try {
        commonLoader = createClassLoader("common", null);
        if( commonLoader == null ) {
            // no config file, default to this loader - we might be in a 'single' env.
            commonLoader=this.getClass().getClassLoader();
        }
        catalinaLoader = createClassLoader("server", commonLoader);
        sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}
复制代码

可以看到commonLoader、catalinaLoader、sharedLoader都是在initClassLoaders方法中初始化的。对于Tomcat6.x版本,只有指定了tomcat/conf/catalina.properties配置文件的server.loader和shared.loader项后才会真正建立CatalinaClassLoader和SharedClassLoader的实例,否则会用到这两个类加载器的地方都会用CommonClassLoader的实例代替,而默认的配置文件没有设置这两个loader项,所以Tomcat6.x把/common、/server和/shared三个目录默认合并到一起变成一个/lib目录,这个目录里的类库相当于以前/common目录中类库的作用。

common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
复制代码
private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {

    String value = CatalinaProperties.getProperty(name + ".loader");
    if ((value == null) || (value.equals("")))
        return parent;

    value = replace(value);

    List<Repository> repositories = new ArrayList<>();

    String[] repositoryPaths = getPaths(value);

    for (String repository : repositoryPaths) {
        // Check for a JAR URL repository
        try {
            @SuppressWarnings("unused")
            URL url = new URL(repository);
            repositories.add(new Repository(repository, RepositoryType.URL));
            continue;
        } catch (MalformedURLException e) {
            // Ignore
        }

        // Local repository
        if (repository.endsWith("*.jar")) {
            repository = repository.substring
                (0, repository.length() - "*.jar".length());
            repositories.add(new Repository(repository, RepositoryType.GLOB));
        } else if (repository.endsWith(".jar")) {
            repositories.add(new Repository(repository, RepositoryType.JAR));
        } else {
            repositories.add(new Repository(repository, RepositoryType.DIR));
        }
    }

    return ClassLoaderFactory.createClassLoader(repositories, parent);
}
复制代码

相关文章:

  • JavaScript事件详解
  • 明文存密码成惯例?Facebook 6 亿用户密码可被 2 万员工直接看
  • 用grunt搭建自动化的web前端开发环境-完整教程
  • 强化学习遭遇瓶颈!分层RL将成为突破的希望
  • [改善Java代码]不同的场景使用不同的泛型通配符
  • Java 混淆那些事(二):认识 ProGuard GUI
  • yii2权限控制rbac之rule详细讲解
  • Solr简单介绍
  • 可视区域懒加载
  • 如何自学编程?学习方法在这里!
  • asp.net MD5 加密
  • js base
  • 「译」Node.js Streams 基础
  • SAP到SAP的升级 --- SAP行业新的金矿
  • EZOJ #257
  • [PHP内核探索]PHP中的哈希表
  • Angular6错误 Service: No provider for Renderer2
  • DataBase in Android
  • echarts的各种常用效果展示
  • gf框架之分页模块(五) - 自定义分页
  • Javascript 原型链
  • Js基础知识(一) - 变量
  • learning koa2.x
  • nginx 配置多 域名 + 多 https
  • 动态规划入门(以爬楼梯为例)
  • 发布国内首个无服务器容器服务,运维效率从未如此高效
  • 如何用Ubuntu和Xen来设置Kubernetes?
  • 设计模式(12)迭代器模式(讲解+应用)
  • 手机端车牌号码键盘的vue组件
  • 小程序button引导用户授权
  • MyCAT水平分库
  • # 日期待t_最值得等的SUV奥迪Q9:空间比MPV还大,或搭4.0T,香
  • #NOIP 2014# day.1 T3 飞扬的小鸟 bird
  • #vue3 实现前端下载excel文件模板功能
  • #我与Java虚拟机的故事#连载19:等我技术变强了,我会去看你的 ​
  • $con= MySQL有关填空题_2015年计算机二级考试《MySQL》提高练习题(10)
  • $redis-setphp_redis Set命令,php操作Redis Set函数介绍
  • (二)基于wpr_simulation 的Ros机器人运动控制,gazebo仿真
  • (二)什么是Vite——Vite 和 Webpack 区别(冷启动)
  • (翻译)terry crowley: 写给程序员
  • (附源码)ssm基于web技术的医务志愿者管理系统 毕业设计 100910
  • (附源码)ssm考试题库管理系统 毕业设计 069043
  • (附源码)计算机毕业设计高校学生选课系统
  • (六)软件测试分工
  • (十三)Java springcloud B2B2C o2o多用户商城 springcloud架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)...
  • (学习日记)2024.02.29:UCOSIII第二节
  • (转)memcache、redis缓存
  • (转)大道至简,职场上做人做事做管理
  • .NET Core 版本不支持的问题
  • .net mvc actionresult 返回字符串_.NET架构师知识普及
  • .net websocket 获取http登录的用户_如何解密浏览器的登录密码?获取浏览器内用户信息?...
  • .NET 使用 ILMerge 合并多个程序集,避免引入额外的依赖
  • .net使用excel的cells对象没有value方法——学习.net的Excel工作表问题
  • /usr/bin/perl:bad interpreter:No such file or directory 的解决办法
  • @RequestParam,@RequestBody和@PathVariable 区别