【Tomcat源码阅读分享】—(5)Tomcat中的ClassLoader

java中的类加载器
在我看来,java的类加载器,其实就是将.class文件,变成java中的java.lang.Class对象的工具,其中包含查找文件,加载字节码,转换字节码的过程。
在java中,有三种自带的类加载器:
  • 启动类加载器(Bootstrap ClassLoader)
    主要加载java的一些核心库(路径为:/jre/lib),这个类加载器与其他的类加载器有些不同,它由C/C++实现,所以不是java.lang.ClassLoader的子类,我们在java代码中是用不了这个加载器的。
  • 扩展加载器(Extention ClassLoader)
    加载/jre/lib/ext里的类,它的父类是启动类加载器(Bootstrap ClassLoader),所以它由启动类加载器加载,但是由于启动类加载器是由C/C++实现的,如果你尝试用getParent获取它的父类加载器,会得到null值。
  • 应用程序类加载器/系统类加载器(Application ClassLoader/System ClassLoader)
    此类加载器由启动类加载,它的父加载器为扩展加载器,负责加载(CLASSPATH)指定的类,可以通过ClassLoader.getSystemClassLoader()获取。
除了自带的类加载器,用户还可以自定义类加载器
用户通过继承java.lang.ClassLoader,重写findClass和loadClass方法即可。
各个类加载器的关系图:

【Tomcat源码阅读分享】—(5)Tomcat中的ClassLoader
文章图片
类加载器图
双亲委派模型
在这里先举一个例子,假如我们在一个java项目中新建一个java.lang的包,然后再在这个包下新建一个String的类,如下图,从代码上看,完全没问题。

【Tomcat源码阅读分享】—(5)Tomcat中的ClassLoader
文章图片
但是仔细一想,如果我某个类需要使用String类,new出来,究竟使用哪个类呢?虚拟机该怎么加载呢?
为了解决这一问题,java实现了双亲委派这一模型,这种模型规定:
除了顶层的启动类加载器外,其他的类加载器都要有自己的父类加载器,假如有一个类要加载进来,一个类加载器不会马上尝试自己将其加载,而是委派给父类加载器,父类加载器收到后又尝试委派给其父类加载器,以此类推,直到委派给启动类加载器,这样一层一层往上委派。只有父类加载器反馈自己没法完成这个加载时,子加载器才会尝试自己加载。——汪建《Tomcat内核设计剖析》
再回到我们刚刚的例子,现在有两个String类,一个是在/jre/lib下的rt.jar包下,由启动类加载器加载,一个是我们自己写的,由应用程序类加载器加载。
如果我们请求载入String类,我们自己编辑的java程序,默认由Application加载器加载,如果有双亲委派机制,Application加载器不会先尝试自己加载这个类,先请求父类加载器,一直到启动类加载器,这是发现启动类加载器下面的jar包中已经有String这个类了,直接加载成功返回,这样既解决了加载选择的问题,也防止了jdk自带的程序被用户恶意破坏。
虽然jdk默认使用双亲委派机制,但有时候我们为了自己的需求,也可以破坏这一机制,那就是使用自定义类加载器,除了重写findClass方法之外,还需重写loadClass方法,在loadClass方法中先自己加载,不找super.loadClass方法。
tomcat中的类加载器解析
在前面Tomcat启动过程简述中,提到了三个类加载器,commonLoader、catalinaLoader和sharedLoader,除了这几个类加载器以外,其实还有WebappClassLoader。
那么Tomcat为什么要定义这么多类加载器呢,其实是为了解决一下几个问题:
  • 有一个统一的目录,让每个apps目录下的web项目共享类库
  • 每个web项目之间如果有相同的jar依赖,不能冲突,保持各项目私有jar库互相隔离
  • 支持热部署的功能
    有了需求以后,再来探究tomcat是怎么处理这一机制的。
我们再看这三个类加载器的初始化代码:
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); } }private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception {String value = https://www.it610.com/article/CatalinaProperties.getProperty(name +".loader"); if ((value =https://www.it610.com/article/= null) || (value.equals(""))) return parent; value = https://www.it610.com/article/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)); } } return ClassLoaderFactory.createClassLoader(repositories, parent); }

在initClassLoaders方法中,可以看出,commonLoader父加载器为应用加载器,catalinaLoader 和sharedLoader 的类加载器的父加载器为commonLoader。createClassLoader方法中,第一句,意思是去conf/catalina.properties中找对应的配置:
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"server.loader=shared.loader=

在Tomcat中,它希望这三个加载器对应的加载目录中的类库可以有以下约束和功能:
  • 放置在common.loader配置的目录:类库可被Tomcat和所有的Web应用程序共同使用。
  • 放置在server.loader配置的目录:类库可被Tomcat使用,对所有的Web应用程序都不可见。
  • 放置在shared.loader配置的目录:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
【【Tomcat源码阅读分享】—(5)Tomcat中的ClassLoader】事实上,tomcat团队为了简化部署过程,除了common对应的配置中有值,其他的都配置为空的,所以都指向commonLoader这一个类加载器。
创建完这三个类加载器以后,马上执行
Thread.currentThread().setContextClassLoader(catalinaLoader);

这句话是什么意思呢?
使用线程上下文类加载器,可以打破双亲委派机制,实现让一个加载器请求其他非父类加载器去加载它需要的类。比如:
我们通过前面的内容已经知道,commonLoader的父类加载器为应用加载器,假设我们再common目录下导入Spring依赖的jar包,去管理webapps目录下的项目,显然Spring相关的类是commonLoader加载的,但是webapps项目下的WEB-INF/lib下的包不在commonLoader的范围内,是由WebAppClassLoader(后文会讲解)加载的,如果按照双亲委派模型去加载,是加载不成功的。
为了解决这一问题,首先要知道,如果没有显式得声明是由哪个类加载器加载的类,比如我们代码中的new 一个类,默认由当前线程的加载器加载,如果没有用Thread.currentThread().setContextClassLoader进行设置,默认由ContextClassLoader加载,这个加载器属于系统类加载器。因此对于上面的问题,我们只需要执行Thread.currentThread().setContextClassLoader(WebAppClassLoader),Spring访问webapps下的类时,用WebAppClassLoader加载就可以了。
设置了线程类加载器以后,执行如下代码:
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);

这一段通过反射,调用了Catalina类的setParentClassLoader方法,且看这个方法:
public void setParentClassLoader(ClassLoader parentClassLoader) { this.parentClassLoader = parentClassLoader; }

通过这两段代码可以看到,将sharedLoader赋值给了Catalina的parentClassLoader 属性。然后我们查看Catalina的getParentClassLoader方法的调用栈:

【Tomcat源码阅读分享】—(5)Tomcat中的ClassLoader
文章图片

【Tomcat源码阅读分享】—(5)Tomcat中的ClassLoader
文章图片
【Tomcat源码阅读分享】—(5)Tomcat中的ClassLoader
文章图片

【Tomcat源码阅读分享】—(5)Tomcat中的ClassLoader
文章图片

通过查看调用栈,我们看到,在StandardContext类中调用了这个方法,StandardContext组件在tomcat中,相当于部署在tomcat上的一个web项目,我们继续跟如何设置和启动WebApp类加载器的:
由tomcat启动流程可以知道,组件中的startInternal方法,是由LifecycleBase的子类调用start方法时,调用了自身实现的startInternal钩子方法。
在StandardContext的startInternal方法中,有这么一段:
if (getLoader() == null) { WebappLoader webappLoader = new WebappLoader(getParentClassLoader()); webappLoader.setDelegate(getDelegate()); setLoader(webappLoader); }

这里将sharedLoader设置为了此StandardContext对象的类加载器了,继续跟入setLoader方法:
@Override public void setLoader(Loader loader) {Lock writeLock = loaderLock.writeLock(); writeLock.lock(); Loader oldLoader = null; try { // Change components if necessary oldLoader = this.loader; if (oldLoader == loader) return; this.loader = loader; // Stop the old component if necessary if (getState().isAvailable() && (oldLoader != null) && (oldLoader instanceof Lifecycle)) { try { ((Lifecycle) oldLoader).stop(); } catch (LifecycleException e) { log.error("StandardContext.setLoader: stop: ", e); } }// Start the new component if necessary if (loader != null) loader.setContext(this); if (getState().isAvailable() && (loader != null) && (loader instanceof Lifecycle)) { try { ((Lifecycle) loader).start(); } catch (LifecycleException e) { log.error("StandardContext.setLoader: start: ", e); } } } finally { writeLock.unlock(); }// Report this property change to interested listeners support.firePropertyChange("loader", oldLoader, loader); }

再看getLoader方法:
@Override public Loader getLoader() { Lock readLock = loaderLock.readLock(); readLock.lock(); try { return loader; } finally { readLock.unlock(); } }

可见这里巧妙地用了读写锁loaderLock设置和获取类加载器对象。
在设置过程中,如果有旧的不同的类加载器,先停掉,再开始当前的类加载器,进入 ((Lifecycle) loader).start(); 方法,进行生命周期管理的状态设置等,在start方法中,会调用其startInternal方法,如下:
/** * Start associated {@link ClassLoader} and implement the requirements * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}. * * @exception LifecycleException if this component detects a fatal error *that prevents this component from being used */ @Override protected void startInternal() throws LifecycleException {if (log.isDebugEnabled()) log.debug(sm.getString("webappLoader.starting")); if (context.getResources() == null) { log.info("No resources for " + context); setState(LifecycleState.STARTING); return; }// Construct a class loader based on our current repositories list try {classLoader = createClassLoader(); classLoader.setResources(context.getResources()); classLoader.setDelegate(this.delegate); // Configure our repositories setClassPath(); setPermissions(); ((Lifecycle) classLoader).start(); String contextName = context.getName(); if (!contextName.startsWith("/")) { contextName = "/" + contextName; } ObjectName cloname = new ObjectName(context.getDomain() + ":type=" + classLoader.getClass().getSimpleName() + ",host=" + context.getParent().getName() + ",context=" + contextName); Registry.getRegistry(null, null) .registerComponent(classLoader, cloname, null); } catch (Throwable t) { t = ExceptionUtils.unwrapInvocationTargetException(t); ExceptionUtils.handleThrowable(t); log.error( "LifecycleException ", t ); throw new LifecycleException("start: ", t); }setState(LifecycleState.STARTING); }

截取这一段:
classLoader = createClassLoader(); classLoader.setResources(context.getResources()); classLoader.setDelegate(this.delegate);

/** * 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; }

这里其实就是通过反射,生成一个WebappClassLoader加载器对象,并将之前传入的sharedLoader作为父类加载器。
接下来我们查看WebappClassLoader的start方法,由于它是WebappClassLoaderBase的子类,直接会调用WebappClassLoaderBase的start方法,如下:
/** * Start the class loader. * * @exception LifecycleException if a lifecycle error occurs */ @Override public void start() throws LifecycleException {state = LifecycleState.STARTING_PREP; WebResource classes = resources.getResource("/WEB-INF/classes"); if (classes.isDirectory() && classes.canRead()) { localRepositories.add(classes.getURL()); } WebResource[] jars = resources.listResources("/WEB-INF/lib"); for (WebResource jar : jars) { if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) { localRepositories.add(jar.getURL()); jarModificationTimes.put( jar.getName(), Long.valueOf(jar.getLastModified())); } }state = LifecycleState.STARTED; }

可以清楚地看到,WebappClassLoader加载了项目下/WEB-INF/classes和/WEB-INF/lib目录。
由于Jsp加载过程涉及到语法分析,生成java文件的过程,这里不再说明,以后章节专门来看。
至此,我们可以总结出tomcat中的类加载器为:
【Tomcat源码阅读分享】—(5)Tomcat中的ClassLoader
文章图片
tomcat类加载器关系图
ClassLoaderFactory——tomcat中的类加载器工厂
Tomcat使用java提供的URLClassLoader将创建类加载器的细节封装在了ClassLoaderFactory中,使其能够很方便地创建自己的自定义类加载器。
我们先看创建类加载器的定义:

【Tomcat源码阅读分享】—(5)Tomcat中的ClassLoader
文章图片

通过传入内部类Repository的结合和父加载器,便可以返回一个自定义类加载器。
且看两个内部类的定义:
public static enum RepositoryType { DIR, GLOB, JAR, URL }public static class Repository { private final String location; private final RepositoryType type; public Repository(String location, RepositoryType type) { this.location = location; this.type = type; }public String getLocation() { return location; }public RepositoryType getType() { return type; } }

RepositoryType 枚举类用来表示资源类型,单词意思很显然,包括了
DIR目录下的class和jar,GLOB目录下的jar资源,JAR单个jar包,URL从URL获取的资源。
类加载器工厂在创建类加载器的过程中,主要是将所有传入的Repository 对象转换成URL对象,放入到一个HashSet中,然后通过AccessController绕过权限检查,调用URLClassLoader的构造方法进行实例化类加载器。以后我们生成类加载器,也可以直接搬过来用了,哈哈。
结束语
Tomcat中的类加载器结构清晰,设计巧妙,由于篇幅原因,很多细节未讲到,通过这一篇的引导,希望可以在看Tomcat类加载器的时候,有一定的方向,以便以后更加深入的了解这一体系。

    推荐阅读