学习Tomcat

久未见 提交于 2020-04-08 10:50:58

Servlet规范定义的类加载顺序

在Servlet规范中有对web应用程序类的加载方式作出建议,重要的有两点:

  1. 容器要加载某个类时,类加载器首先应该加载本地web应用程序中“WEB-INF/classes”路径中的类,然后再到“WEB-INF/lib”依赖库中加载类,最后在容器级别的lib中加载类;
  2. 同时,类加载器要保证应用程序不会覆盖Java核心类,即java.*javax.*命名空间中的类,也就是说web应用程序如果定义了一个和java核心类名字相同的类则是无效的。

Tomcat的应用程序类加载器

Tomcat内部有多种类型的类加载器,其中WebappClassLoader是应用程序类加载器,tomcat启动时对于它管理的每个web应用程序都会创建一个单独WebappClassLoader实例,在tomcat内部的StandardContex类中的启动方法中找到。

根据servlet规范,容器要首先在本地应用程序库中加载请求的类,同时要避免应用程序中的类覆盖Java平台类库中的核心类,WebappClassLoader加载器为了实现这些要求没有遵循java的父加载器委托的模型,它重写了ClassLoader的loadClass方法,重写的loadClass方法的源代码细节如下:

// 代码省略了一些与类加载逻辑关系不大的细节,通过分析这段代码的逻辑来了解是否复合servlet规范的建议。  

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
  
  synchronized (getClassLoadingLock(name)) {
    // ...
    Class<?> clazz = null;

    // ...

    // (0) 检查是否已经从本地库中加载过
    clazz = findLoadedClass0(name);
    if (clazz != null) {
      if (log.isDebugEnabled())
        log.debug("  Returning class from cache");
      if (resolve)
        resolveClass(clazz);
      return clazz;
    }

    // (0.1) 检查是否此加载器是否已经加载过这个类
    clazz = findLoadedClass(name);
    if (clazz != null) {
      if (log.isDebugEnabled())
        log.debug("  Returning class from cache");
      if (resolve)
        resolveClass(clazz);
      return clazz;
    }

    // (0.2) 
    String resourceName = binaryNameToPath(name, false);
    // getJavaseClassLoader方法返回的是引用javaseClassLoader,这个引用变量
    // 引用的是java内建的加载:bootstrapClassLoader,或者systemClassLoader,或者extClassLoader。
    // 这里首先使用java内建的加载器加载,防止应用程序中用户定义的类和覆盖java核心类,
    // 如果不先用内建加载器加载,而首先加载本地程序库中的类,如果应用程序类和java核心类
    // 同名,则java核心类无法被加载。
    ClassLoader javaseLoader = getJavaseClassLoader();
    boolean tryLoadingFromJavaseLoader;
    try {
      URL url;
      if (securityManager != null) {
        PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
        url = AccessController.doPrivileged(dp);
      } else {
        url = javaseLoader.getResource(resourceName);
      }
      tryLoadingFromJavaseLoader = (url != null);
    } catch (Throwable t) {
      ExceptionUtils.handleThrowable(t);
      tryLoadingFromJavaseLoader = true;
    }

    if (tryLoadingFromJavaseLoader) {
      try {
        clazz = javaseLoader.loadClass(name);
        if (clazz != null) {
          if (resolve)
            resolveClass(clazz);
          return clazz;
        }
      } catch (ClassNotFoundException e) {
        // Ignore
      }
    }
    
    // 通过上面的三个步骤后,说明name类没有被加载过,并且也不是java的核心类,
    // 接下来按照servlet的规范到本地应用程序库中加载。

    // (0.5) Permission to access this class when using a SecurityManager
    if (securityManager != null) {
      // 检查访问权限 ...
    }

    // filter方法是检查类名是否属于javax命名空间下的核心类或者是tomcat的核心类
    // delegate是可以通过外部控制的变量
    boolean delegateLoad = delegate || filter(name, true);

    // (1) 这里的if块说明两点:
    // 第一是tomcat也支持使用java的上层委托加载机制,只要把delegate字段设置为true即可,
    // delegate默认是false,说明tomcat默认并没有开启委托加载机制。
    // 第二是对于javax命名空间的核心类和tomcat本身的核心类使用委托加载机制。
    if (delegateLoad) {
      if (log.isDebugEnabled())
        log.debug("  Delegating to parent classloader1 " + parent);
      try {
        // forName采用的就是委托加载机制
        clazz = Class.forName(name, false, parent); 
        if (clazz != null) {
          if (log.isDebugEnabled())
            log.debug("  Loading class from parent");
          if (resolve)
            resolveClass(clazz);
          return clazz;
        }
      } catch (ClassNotFoundException e) {
        // Ignore
      }
    }

    // (2) 没有使用委托加载机制,到本地应用程序库中加载
    if (log.isDebugEnabled())
      log.debug("  Searching local repositories");
    try {
      clazz = findClass(name);
      if (clazz != null) {
        if (log.isDebugEnabled())
          log.debug("  Loading class from local repository");
        if (resolve)
          resolveClass(clazz);
        return clazz;
      }
    } catch (ClassNotFoundException e) {
      // Ignore
    }

    // (3) 没有使用委托机制加载过,并且本地应用程序库中也没有加找到类,最后再委托到父加载器加载
    if (!delegateLoad) {
      if (log.isDebugEnabled())
        log.debug("  Delegating to parent classloader at end: " + parent);
      try {
        clazz = Class.forName(name, false, parent);
        if (clazz != null) {
          if (log.isDebugEnabled())
            log.debug("  Loading class from parent");
          if (resolve)
            resolveClass(clazz);
          return clazz;
        }
      } catch (ClassNotFoundException e) {
        // Ignore
      }
    }
  }

  throw new ClassNotFoundException(name);
}

WebappClassLoader实现细节的关键点总结如下:

  1. 为了满足前面说的servlet加载规范的第二点要求,在loadClass方法的(0.2)步,tomcat首先使用了java内建的加载器加载类,这个类加载器大多数情况下是bootstrap。
  2. 为了满足servlet规范第一点要求,tomcat首先去/WEB-INF/classes,/WEB-INF/lib以及其他自定义的URL路径下加载,在这些路径下如果找不到类,然后才委托到父加载器加载,这和java的父类加载器委托机制相反,loadClass方法中的(1)(2)(3)这几个步骤就是实现的这点要求。
  3. tomcat的类加载机制也可以完全兼容父类加载器委托机制,即首先使用父类加载器加载,父加载器找不到类的话,然后再到本地应用程序库中加载类,这只需要把delegate字段值设置为true即可。

Tomcat的类加载器层次

Tomcat内部实现的类加载器有很多种,上面分析的WebappClassLoader就是属于web应用程序的类加载器,它负责加载web应用程序中的业务类及其他资源文件。Tomcat在启动时默认情况下会创建出下面这几种类加载器,并且它们的层级关系如下:
   bootstrap
    /|\
   system
    /|\
   common
   /   \
 webapp1 webapp2 ......

这些类加载器的不同之处其实就是它们查找类的路径不相同

  • bootstrap就是虚拟机内部的加载器,它负责加载Java平台核心类。
  • system加载器就是Java平台内建的system加载器,负载加载CLASSPATH路径下的类,CLASSPATH默认是当前工作目录,但是tomcat重新定义了CLASSPATH,在启动脚本catalina.sh中把CLASSPATH的值设置为下面这几个路径:
    • $CATALINA_HOME/bin/bootstrap.jar,tomcat服务器实例的启动类包含在里面。
    • $CATALINA_BASE/bin/tomcat-juli.jar或者CATALINA_HOME/bin/juli.jar
    • $CATALINA_HOME/bin/commons-daemon.jar,这个jar没有在catalina.sh脚本中显示设置为CLASSPATH的一个值,而是通过bootstrap.jar中引用的。
  • common加载器负责加载通过“$CATALINA_HOME/conf/catalina.properties”文件中的common.loader属性定义的类:
    common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
    
    这个定义实际上包含的类如下:
    • $CATALINA_BASE/lib路径下所有的class文件和其他资源
    • $CATALINA_BASE/lib路径下所有的jar文件
    • $CATALINA_HOME/lib路径下所有的class文件和其他资源
    • $CATALINA_HOME/lib路径下所有的jar文件
  • webapp加载器则是每个web应用程序都会创建的,专门负责加载web应用程序本地库/WEB-INF/classes/路径下的class和资源文件,也包括/WEB-INF/lib/路径下的所有jar中打包的class文件以及资源文件,也可以加载其他明确指定的URL路径的资源,webapp loader只会在这几个地方加载类和资源。

更细致的类加载器层次

实际上tomcat的类加载器的层次结构还可以设定的更细致一些,可以配置另外两种加载器:server loadershared loader,这两种类加载器和其他类加载器的层次关系现在如下:
   bootstrap
    /|\
   system
    /|\
   common
   /   \
 server  shared
      /   \
    webapp1 webapp2 ......

server loader只对tomcat自己内部的类可见,shared loader对所有的web应用程序的类都是可见的,可以用来加载所有web应用程序所共用代码。实际上server loader和shared loader加载的路径只是对common loader的细分,因此common loader可以用来管理tomcat内部和外部web应用程序所共同依赖的类。

如果要开启这两个类加载器,需要分别为“$CATALINA_HOME/conf/catalina.properties”文件中的server.loadershared.loader这两个属性设置路径。

多层次结构加载器的好处

Tomcat的这种多类型多层次结构的类加载器实现机制有一个很好的好处是:既能实现类的隔离,又能实现类的共享。

通常的部署模式我们一般是一个tomcat容器中运行一个web应用程序,但是如果我们想要在一个tomcat容器中部署多个web应用程序,可能出现两个问题,首先如果两个web程序中有两个相同名字的类,但是它们的实现代码是完全不同的,类加载器如何区分两个类;另一种情况是两个web应用程序中依赖于同一个第三方库,但是依赖的版本不一致,这时如何正确的加载第三方库中的类。这两种情况其实就是要隔离不同的应用程序,tomcat就是通过为每一个web应用程序都创建一个独立的WebappClassLoader来实现的,不同的WebappClassLoader只会加载自己程序里面的类。

共享则是通过父加载器来实现的,共享类能减少内存区空间的占用,例如我们在一个容器中部署了多个web应用程序,假如它们都依赖于相同版本的Spring框架,那么其实可以共用Spring的类,而不必每个应用程序都独自加载一遍Spring的类,这个时候shared loader就是用来解决这个问题的,我们可以把Spring库放到shared loader查找的路径下,所有应用程序要加载Spring的类时都使用同一个shared loader来加载,因此最终方法区只会存在一份Spring类。上面我们分析过应用程序类加载器WebappClassLoader是能够向上委托到shared loader的,当它不能在本地应用程序库中找到类时,会委托到父加载器加载。

与shared loader类似,common loader也是一个用来加载共用类的加载器,只是common loader加载的类对tomcat自己和应用程序都是共享的。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!