Tomcat类加载机制探析


title: Tomcat类加载机制探析
comments: false
toc: true
categories:

  • Web服务器
    tags:
  • Tomcat
  • 类加载
  • 双亲委派
    date: 2020-12-23 23:03:58

Java类加载机制,双亲委托模型相必大家已经熟的不能再熟了。本文就从Tomcat的源码级别上来探究一下类加载机制的秘密。

首先咱们还是老调重弹,看一下网上已经泛滥的一张Tomcat类加载关系图 Tomcat类加载器关系图

属于JavaSE的BootstrapClassLoader、ExtClassLoader、AppClassLoader(这里主要加载Tomcat启动的Bootstrap类)在本文不再赘述;

  • CommonClassLoader加载common.loader属性下的jar;一般是CATALINA_HOME/lib目录下

  • CatalinaClassLoader加载server.loader属性下的jar;默认未配置路径,返回其父加载器即CommonClassLoader

  • SharedClassloader加载share.loader属性下的jar;默认未配置路径,返回其父加载器即CommonClassLoader

    由于WebAppClassLoader需要等Tomcat的各个组件初始化完成之后才加载对应的server.xml配置文件,解析对应Host下的docBase目录寻找WEB-INF下的类文件,并且会将该根目录下的每个直接子目录当作一个web项目加载,为了确保各个项目之间的相互独立,每个项目都是单独的WebAppClassLoader加载的,咱们后文再讲。

CommonClassLoader、CatalinaClassLoader、SharedClassloader

先从源码角度来看看CommonClassLoader、CatalinaClassLoader、SharedClassloader这三者是如何绑定关系的?

org.apache.catalina.startup.Bootstrap#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);
    }
}

调用了createClassLoader方法来创建对象,第二个参数是指定它的父级类加载器。可以看到catalinaLoader、sharedLoader均指明commonLoader为它的父级类加载器,这说明catalinaLoader、sharedLoader是同级类加载器,印证了上图。

再看看createClassLoader方法怎么做的:

// 创建基础类型类加载器
private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {
    
    

    // 根据“name+.loader”从系统配置中读取需要加载的jar路径
    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
        }

        // 封装路径,指定其jar的类型:jar包、目录等
        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));
        }
    }
    // 通过类加载工厂创建,repositories的值为当前Tomcat所在目录下配置的jar子目录,比如 Tomcat_Home/lib/
    return ClassLoaderFactory.createClassLoader(repositories, parent);
}

ClassLoaderFactory.createClassLoader是一个典型的工厂模式,屏蔽了类加载器对象初始化的细节:

public static ClassLoader createClassLoader(List<Repository> repositories,
                                            final ClassLoader parent)
    throws Exception {
    
    

    if (log.isDebugEnabled())
        log.debug("Creating new class loader");

    // Construct the "class path" for this class loader
    Set<URL> set = new LinkedHashSet<>();

    if (repositories != null) {
    
    
        // 根据jar包类型解析其Url,并添加到Set<URL> set
        for (Repository repository : repositories)  {
    
    
            if (repository.getType() == RepositoryType.URL) {
    
    
                URL url = buildClassLoaderUrl(repository.getLocation());
                set.add(url);
            } else if (repository.getType() == RepositoryType.DIR) {
    
    
                File directory = new File(repository.getLocation());
                directory = directory.getCanonicalFile();
                if (!validateFile(directory, RepositoryType.DIR)) {
    
    
                    continue;
                }
                URL url = buildClassLoaderUrl(directory);
               
                set.add(url);
            } else if (repository.getType() == RepositoryType.JAR) {
    
    
                File file=new File(repository.getLocation());
                file = file.getCanonicalFile();
                if (!validateFile(file, RepositoryType.JAR)) {
    
    
                    continue;
                }
                URL url = buildClassLoaderUrl(file);
                
                set.add(url);
            } else if (repository.getType() == RepositoryType.GLOB) {
    
    
                File directory=new File(repository.getLocation());
                directory = directory.getCanonicalFile();
                if (!validateFile(directory, RepositoryType.GLOB)) {
    
    
                    continue;
                }
               
                String filenames[] = directory.list();
                if (filenames == null) {
    
    
                    continue;
                }
                for (int j = 0; j < filenames.length; j++) {
    
    
                    String filename = filenames[j].toLowerCase(Locale.ENGLISH);
                    if (!filename.endsWith(".jar"))
                        continue;
                    File file = new File(directory, filenames[j]);
                    file = file.getCanonicalFile();
                    if (!validateFile(file, RepositoryType.JAR)) {
    
    
                        continue;
                    }
                    if (log.isDebugEnabled())
                        log.debug("    Including glob jar file "
                            + file.getAbsolutePath());
                    URL url = buildClassLoaderUrl(file);
                    set.add(url);
                }
            }
        }
    }

    // Construct the class loader itself
    final URL[] array = set.toArray(new URL[set.size()]);

    // 直接通过URLClassLoader创建ClassLoader
    return AccessController.doPrivileged(
            new PrivilegedAction<URLClassLoader>() {
    
    
                @Override
                public URLClassLoader run() {
    
    
                    if (parent == null)
                        return new URLClassLoader(array);
                    else
                        return new URLClassLoader(array, parent);
                }
            });
}

可以看到通过ClassLoaderFactory.createClassLoader(List repositories, final ClassLoader parent)此方法创建的ClassLoader其本身是一个URLClassLoader,通过人为手工的方式分别指定了其父级类加载。

上文提到catalinaLoader会加载Tomcat本身的类,其主要目的是进行类加载隔离,与SharedClassloader区分开,这样咱们开发人员编写的web项目就不能直接访问到Tomcat的类,造成安全问题了。 那么又是怎么实现的呢?咱们接着看Tomcat的启动前的初始方法org.apache.catalina.startup.Bootstrap#init()

public void init() throws Exception {
    
    
	// 初始化commonLoader、catalinaLoader、sharedLoader3个类加载器
    initClassLoaders();
	// 直接设置当前线程的类加载器为catalinaLoader
    Thread.currentThread().setContextClassLoader(catalinaLoader);

    SecurityClassLoad.securityClassLoad(catalinaLoader);

    // 使用当前线程的catalinaLoader来加载Catalina类
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    // 以下整段表示:Catalina初始化后得到的对象startupInstance调用setParentClassLoader方法,将sharedLoader设置为父加载器
    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;
}

从源码中可以看到步骤:

  • 用catalinaLoader加载了"org.apache.catalina.startup.Catalina"类;
  • 立即通过setParentClassLoader方法指定其父加载器为sharedLoader,这样Catalina对象也可访问sharedLoader下的类了;

如何为每一个webApp项目定义一个独立的WebAppClassLoader?

咱们从源码角度来分析一下。

Tomcat的各容器调用流程不是本文关注的重点,直接查看webApp的类加载器初始化的代码位置org.apache.catalina.loader.WebappLoader#startInternal()

// 每一个项目context均会执行此方法
protected void startInternal() throws LifecycleException {
    
    

    // 省略部分代码

    // Construct a class loader based on our current repositories list
    try {
    
    
		// 创建WebApp的类加载器
        classLoader = createClassLoader();
        // 获取一个webapp项目的类加载路径:WEB-INF/classes、WEB-INF/lib/
        classLoader.setResources(context.getResources());
        // delegate是否遵循类加载的双亲委派模型,默认为false
        classLoader.setDelegate(this.delegate);

		// 执行类加载
        ((Lifecycle) classLoader).start();

        // 省略

    } catch (Throwable t) {
    
    
        // 省略
    }

    // 省略
}


private String loaderClass = ParallelWebappClassLoader.class.getName();
/**
  * Create associated classLoader.
  */
private WebappClassLoaderBase createClassLoader()
    throws Exception {
    
    

    Class<?> clazz = Class.forName(loaderClass);
    WebappClassLoaderBase classLoader = null;

    if (parentClassLoader == null) {
    
    
        parentClassLoader = context.getParentClassLoader();
    }
    Class<?>[] argTypes = {
    
     ClassLoader.class };
    Object[] args = {
    
     parentClassLoader };
    Constructor<?> constr = clazz.getConstructor(argTypes);
    classLoader = (WebappClassLoaderBase) constr.newInstance(args);

    return classLoader;
}

当每一个HOST执行org.apache.catalina.startup.HostConfig#deployApps()方法发布所有的项目时,其工作目录下的每个webapp项目均会执行org.apache.catalina.loader.WebappLoader#startInternal()方法,并在其中使用createClassLoader()创建类加载器。可以从createClassLoader()方法看到 WebappClassLoaderBase的真正实现类为ParallelWebappClassLoader。

ParallelWebappClassLoader类继承于WebappClassLoaderBase,类结构如下:

ParallelWebappClassLoader

WebappClassLoaderBase继承于URLClassLoader,由URLClassLoader可指定加载位置的Path来进行类加载。

还记得上面的 this.delegate 字段吗?默认为false,表示不使用双亲委派机制,我们再看看是如何破坏双亲委派机制的。方法位置:org.apache.catalina.loader.WebappClassLoaderBase#loadClass(java.lang.String, boolean)

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    
    

    synchronized (getClassLoadingLock(name)) {
    
    
        
        Class<?> clazz = null;

        // 省略一些检查代码

        // (0) 从本地字节码缓存中查找
        clazz = findLoadedClass0(name);
        if (clazz != null) {
    
    
            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) 尝试使用系统类加载器加载该类,以防止webapp覆盖Java SE类。This implements SRV.10.7.2
        String resourceName = binaryNameToPath(name, false);

        ClassLoader javaseLoader = getJavaseClassLoader();
        boolean tryLoadingFromJavaseLoader;
        try {
    
    
            //使用getResource,因为如果Java SE类加载器无法提供该资源,它将不会触发昂贵的ClassNotFoundException。
            //但是,在极少数情况下在安全管理器下运行时(有关详细信息,请参见https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 //),此调用可能触发ClassCircularityError。
            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;
        }
		// 尝试通过JavaSE类加载器加载
        if (tryLoadingFromJavaseLoader) {
    
    
            try {
    
    
                clazz = javaseLoader.loadClass(name);
                if (clazz != null) {
    
    
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
    
    
                // Ignore
            }
        }

        // (0.5) 使用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);
                }
            }
        }
		
        //默认为不使用双亲委派,调用filter进行过滤是否可以委派加载此方法,如果是JSP,EL表达式,Tomcat等一些类则可以委派
        boolean delegateLoad = delegate || filter(name, true);

        // (1) 如果需要,委托给我们的父加载器
        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) 从本地搜索查找类即自我加载
        try {
    
    
            clazz = findClass(name);
            if (clazz != null) {
    
    
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
    
    
            // Ignore
        }

        // (3) 实在找不到,无条件委托给父加载器
        if (!delegateLoad) {
    
    
            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
            }
        }
    }
	// 所有方法均加载失败,返回ClassNotFoundException
    throw new ClassNotFoundException(name);
}

从源码中,可以提取为以下步骤:

  • (0) 从本地字节码缓存中查找
  • (0.1) 从已经加载过的类中查找
  • (0.2) 尝试使用系统类加载器加载该类,以防止webapp覆盖Java SE类。
  • (0.3) 系统类加载器不适合加载此类
  • (0.4) 尝试通过JavaSE类加载器加载
  • (0.5) 使用SecurityManager时访问此类的权限
  • (1) 判断是否双亲委派(一般为false),如果需要,委托给我们的父加载器
  • (2) 使用自己加载器进行类加载,注意此步骤如果执行则破坏了双亲委派原则,因为没有让父类加载器进行加载。
  • (3) 实在找不到,无条件委托给父加载器
  • 所有方法均加载失败,返回ClassNotFoundException

总结

Java的双亲委派机制无疑是一个非常优秀的设计,它有以下优点:

  1. 采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
  2. 增加系统安全,java核心API中定义类型不会被随意替换。

但是在Tomcat这种需要多项目部署时,其反而可能会有一些弊端,比如刚好部署了A,B两个字节码完全相同的系统,如果是传统的双亲委派机制,则后加载的系统的类不会加载成功,如果有类静态变量则会被2个系统共享,引起系统异常。

猜你喜欢

转载自blog.csdn.net/ksisn/article/details/111940586
今日推荐