背景

我们项目在类加载时一般都遵循双亲委派模型,但在Tomcat项目源码中却打破了双亲委派机制的模型,在每个Webapp应用启动加载时创建独立WebappClassLoader类加载器,这样做是出于哪些重要的考虑?于此同时Tomcat也创建了多种类型的全局类加载器,并且它们负责加载特定的配置路径,它为什么要这样设计,是出于什么考虑?今天我们通过源码就要弄清楚这两个问题。


类加载与类加载器

类加载是将class文件中的二进制字节码(流)读入JVM的过程。它非常灵活,可以从本地磁盘、缓存甚至网络直接读取二进制流。

类加载的过程很明确首先通过类的完全限定名,查找到该类的二进制流,然后将二进制流的代表的静态结构转换成虚拟机内存方法区的运行时数据结构,最后在内存中生成java.lang.Class对象并作为该类访问的入口。

JVM 设计者把类加载这个动作放到 java 虚拟机外部去实现,以便让应用程序决定如何获取所需要的类,这样做非常灵活,而实现这种加载的工具就叫类加载器。对于任何一个类都需要加载它的类加载器和该类的对象来确定其在JVM中的唯一性。

Tomcat源码分析 - 遵循双亲委派模型与打破双亲委派共存,Tomcat类加载器源码分析_第1张图片

双亲委派模型

工作过程为:当一个类加载器收到类请求时,首先不考虑自己尝试加载这个类,而是将请求直接委派给它的父加载器去完成;每个层级的加载器都采用这种方式,因此默认情况下可能被传递到启动类加载器,只有在父加载器无法完成请求时,这个类加载器才自己去加载,这种模式就叫双(其实应该叫做“单”)亲委派。

这种模式要求除启动类加载器外,都应该有父类加载器,子类加载器不通过继承父类加载器实现,而是采用组合关系复用父加载器的代码实现。

该模式的优点是:让java类和它的类加载器具有带优先级的层次关系,同时保证了程序的稳定性和安全性。

遵循双亲委派模式的类加载,在JDK中的依赖关系是

Tomcat源码分析 - 遵循双亲委派模型与打破双亲委派共存,Tomcat类加载器源码分析_第2张图片

Tomcat类加载的考虑

一、隔离性

Web应用项目包之间相互隔离,避免库相互依赖和应用包相互影响。比如:在一个端口配置两个应用包,这两个应用包之间除了某些三方依赖包版本不一样外,其他都一样。这时如果采用同一个类加载器,会不会出问题?答案是肯定的(包的三方依赖包会被覆盖,而导致其中一个应用无法正常启动)。

二、灵活性

因为考虑了隔离性,所以每一个应用的类加载器相互独立。如果某个应用重新部署时,只有该应用的类加载器会重新加载,而不会影响其它的应用。可以针对单个应用单独升级或部署。

三、性能

因为每个Web应用都有自己的类加载器,所以不会去加载与本应用无关的依赖库包,显然类加载性能要高于所有应用都共享一个类加载器的方案。


Tomcat类加载器结构

因为有以上三点的考虑后,Tomcat的类加载器结构为

Tomcat源码分析 - 遵循双亲委派模型与打破双亲委派共存,Tomcat类加载器源码分析_第3张图片

Tomcat类加载器源码分析

Bootstrap启动类,入口main()方法

Tomcat源码分析 - 遵循双亲委派模型与打破双亲委派共存,Tomcat类加载器源码分析_第4张图片

Bootstrap启动类,init()初始化方法

public void init() throws Exception {
   /**
    * 定义和初始化各种类加载器
    * common-classloader 公共类加载器
    * catalina-classloader 主类加载器
    * shared-classloader 共享类加载器
    */
   
initClassLoaders();
   
/**
    * 应尽早设置上下文类加载器避免发生ClassNotFound的异常
    */
   
Thread.currentThread().setContextClassLoader(catalinaLoader);
   
/**
    * 设置安全类加载器为catalina-classloader
    */
   
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();

   
/**
    * 反射设置Cataline类的默认父类加载器为shared-classloader
    */
   
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;
}

Bootstrap启动类initClassLoaders()方法

/**
* 初始化和创建各种加载器实现
* 通过解析/conf/catalina.properties文件
*/
private void initClassLoaders() {
   try {
       /**
        * 创建common-classloader公共类加载器
        */
       
commonLoader = createClassLoader("common", null);
       if
( commonLoader == null ) {
           /**
            * 若common.loader属性未配置加载路径,则采用默认的AppClassLoader加载器
            */
           
// no config file, default to this loader - we might be in a 'single' env.
           
commonLoader=this.getClass().getClassLoader();
       
}
       /**
        * 创建catalina-classloader服务主类加载器
        */
       
catalinaLoader = createClassLoader("server", commonLoader);
       
/**
        * 创建shared-classloader共享类加载器
        */
       
sharedLoader = createClassLoader("shared", commonLoader);
   
} catch (Throwable t) {
       handleThrowable(t);
       
log.error("Class loader creation threw exception", t);
       
System.exit(1);
   
}
}

/conf/catalina-properties配置文件

Tomcat源码分析 - 遵循双亲委派模型与打破双亲委派共存,Tomcat类加载器源码分析_第5张图片

创建类加载器公共封装createClassLoader();

/**
* 创建类加载器
*
@param name 加载器名称
*
@param parent 父类加载器
*
@return classloader
*
@throws Exception
*/
private ClassLoader createClassLoader(String name, ClassLoader parent)
   throws Exception {
   /**
    * 构建节点名称,从catalina-properties文件解析加载路径
    */
   
String value = CatalinaProperties.getProperty(name + ".loader");
   
/**
    * 当节点未配置加载器路径时,统一采用common-classloader作为类加载器
    */
   
if ((value == null) || (value.equals("")))
       return parent;

   
value = replace(value);

   
List 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));
       
}
   }
   /**
    * 通过工厂类创建类UrlClassLoader类型加载器
    */
   
return ClassLoaderFactory.createClassLoader(repositories, parent);
}

ClassLoaderFactory工厂类创建类加载器

Tomcat源码分析 - 遵循双亲委派模型与打破双亲委派共存,Tomcat类加载器源码分析_第6张图片

WebappClassLoader源码解析

在StandardContext应用程序包上下文类启动时,Tomcat通过解析web.xml创建独立的WebappClassLoader,每个应用包创建一个独立的类加载器。入口方法为startInternal()启动方法实现。

Tomcat源码分析 - 遵循双亲委派模型与打破双亲委派共存,Tomcat类加载器源码分析_第7张图片


通过LifecycleBase生命周期骨架类,开启web.xml配置文件解析

Tomcat源码分析 - 遵循双亲委派模型与打破双亲委派共存,Tomcat类加载器源码分析_第8张图片

WebappClassLoaderBase抽象基类,打破双亲委派重写loadClass()方法;

@Override
public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {

   synchronized (getClassLoadingLock(name)) {
       if (log.isDebugEnabled())
           log.debug("loadClass(" + name + ", " + resolve + ")");
       
Class clazz = null;

       
// Log access to stopped class loader
       
checkStateForClassLoading(name);

       
// (0) Check our previously loaded local class cache
       
clazz = findLoadedClass0(name);
       if
(clazz != null) {
           if (log.isDebugEnabled())
               log.debug("  Returning class from cache");
           if
(resolve)
               resolveClass(clazz);
           return
clazz;
       
}

       // (0.1) Check our previously loaded class cache
       
clazz = findLoadedClass(name);
       if
(clazz != null) {
           if (log.isDebugEnabled())
               log.debug("  Returning class from cache");
           if
(resolve)
               resolveClass(clazz);
           return
clazz;
       
}

       // (0.2) Try loading the class with the system class loader, to prevent
       //       the webapp from overriding Java SE classes. This implements
       //       SRV.10.7.2
       
String resourceName = binaryNameToPath(name, false);

       
ClassLoader javaseLoader = getJavaseClassLoader();
       boolean
tryLoadingFromJavaseLoader;
       try
{
           // Use getResource as it won't trigger an expensive
           // ClassNotFoundException if the resource is not available from
           // the Java SE class loader. However (see
           // https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for
           // details) when running under a security manager in rare cases
           // this call may trigger a ClassCircularityError.
           // See https://bz.apache.org/bugzilla/show_bug.cgi?id=61424 for
           // details of how this may trigger a StackOverflowError
           // Given these reported errors, catch Throwable to ensure any
           // other edge cases are also caught
           
URL url;
           if
(securityManager != null) {
               PrivilegedAction dp = new PrivilegedJavaseGetResource(resourceName);
               
url = AccessController.doPrivileged(dp);
           
} else {
               url = javaseLoader.getResource(resourceName);
           
}
           tryLoadingFromJavaseLoader = (url != null);
       
} catch (Throwable t) {
           // Swallow all exceptions apart from those that must be re-thrown
           
ExceptionUtils.handleThrowable(t);
           
// The getResource() trick won't work for this class. We have to
           // try loading it directly and accept that we might get a
           // ClassNotFoundException.
           
tryLoadingFromJavaseLoader = true;
       
}

       if (tryLoadingFromJavaseLoader) {
           try {
               clazz = javaseLoader.loadClass(name);
               if
(clazz != null) {
                   if (resolve)
                       resolveClass(clazz);
                   return
clazz;
               
}
           } catch (ClassNotFoundException e) {
               // Ignore
           
}
       }

       // (0.5) Permission to access this class when using a SecurityManager
       
if (securityManager != null) {
           int i = name.lastIndexOf('.');
           if
(i >= 0) {
               try {
                   securityManager.checkPackageAccess(name.substring(0,i));
               
} catch (SecurityException se) {
                   String error = "Security Violation, attempt to use " +
                       "Restricted Class: " + name;
                   
log.info(error, se);
                   throw new
ClassNotFoundException(error, se);
               
}
           }
       }

       boolean delegateLoad = delegate || filter(name, true);

       
// (1) Delegate to our parent if requested
       
if (delegateLoad) {
           if (log.isDebugEnabled())
               log.debug("  Delegating to parent classloader1 " + 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
           
}
       }

       // (2) Search local repositories
       
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) Delegate to parent unconditionally
       
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);
}

总结

以上就是Tomcat类加载相关源代码的解析过程,在实际的开发中我们可以利用Tomcat类加载的机制优化我们的项目部署。比如:在多个子应用包一起部署的时,我们可以把这些应用中公共的依赖包放入shared.loader配置的加载路径下从而减少类加载器重复加载依赖包,优化类加载性能;我们也可以将一些全局性较强且安全验证、授权等相关组件包放入server.loader加载路径下,它们在多个应用中起作用但对于应用开发人员不需要关注此类包的细节,从而起到安全隔离和应用之间解耦的效果。更多Tomcat源码的技术点分享,请继续关注!