Java虚拟机--Tomcat类加载

Tomcat类加载

与很多服务器应用一样,Tomcat也实现了类加载器。

在Java虚拟机类加载的基础上,Tomcat进行了稍许改动,以适应自身业务的需求。

当Tomcat启动后,它会创建一组类加载器,这些类加载器也会形成双亲委派模型中的父子关系,父类加载器在子类加载器之上:

   Bootstrap
      |
   System
      |
   Common
   /     \
Webapp1   Webapp2 ...

结合双亲委派模型来看,Tomcat的类加载结构,如图所示:

Java虚拟机--Tomcat类加载_第1张图片
image

Tomcat在原有双亲委派模型的基础上,增加了Common类加载器、Catalina类加载器、Shared类加载,以及WebApp类加载器。

如果将两者结合起来看,那么Bootstrap对应的是Bootstrap ClassLoader和Extension ClassLoader,System对应的是Application ClassLoader,Common对应的是Common ClassLoader、Catalina ClassLoader和Shared ClassLoader,WebApp1/WebApp2..对应的是WebApp ClassLoader。

Bootstrap ClassLoader:C++编写,无具体实现类,无法通过方法获得,负责加载/lib目录下的文件。

Extension ClassLoader:Java程序编写,sun.misc.Launcher.ExtClassLoader实现,负责加载/lib/ext目录下的文件。

Application ClassLoader:Java语言编写,sun.misc.Launcher.AppClassLoader实现,负责加载Tomcat目录下lib文件夹下的bootstrap.jar、commons-daemon.jar、tomcat-juli.jar。

Common ClassLoader:Java语言编写,org.apache.catalina.loader.StandardClassLoader实现(在tomcat源码中),负责加载Tomcat目录conf文件夹下的catalina.properties文件中的common.loader属性所指定的目录。

Catalina ClassLoader:Java语言编写,org.apache.catalina.loader.StandardClassLoader实现(在tomcat源码中),负责加载Tomcat目录conf文件夹下的catalina.properties文件中的server.loader属性所指定的目录。

Shared ClassLoader:Java语言编写,org.apache.catalina.loader.StandardClassLoader实现(在tomcat源码中),负责加载Tomcat目录conf文件夹下的catalina.properties文件中的shared.loader属性所指定的目录。

WebApp ClassLoader:Java语言编写,org.apache.catalina.loader.WebappClassLoader实现(在tomcat源码中),负责加载Tomcat目录webapps文件下中应用里的/WEB-INF/lib和/WEB-INF/class.

Tomcat目录结构中,有三组目录(/common/,/server/和shared/)可以存放公用Java类库,以及第四组Web应用程序目录/WEB-INF/

放置在common目录中:类库可被Tomcat和所有的Web应用程序共同使用。

放置在server目录中:类库可被Tomcat使用,但对所有的Web应用程序都不可见。

放置在shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。

放置在/WebApp/WEB-INF目录中:类库仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
Java虚拟机--Tomcat类加载_第2张图片
tomcat7.0目录结构

值得注意的是,在Tomcat的6.0版本之后,只有在tomcat/conf/catalina.properties文件中给server.loader属性和share.loader属性赋值后,才会去建立Catalina ClassLoader和Shared ClassLoader两个类加载器的实例,否则在用到这两个类加载器的地方都会用Common ClassLoader的实例代替(org.apache.catalina.loader.StandardClassLoader),而/common、/server和/shared目录在Tomcat6.0之后就不存在了,取而代之的是/lib目录。

Java虚拟机--Tomcat类加载_第3张图片
默认类加载器配置

Tomcat类加载源码

说完了,Tomcat类加载的结构。本小节,具体说下Tomcat的类加载机制,想要说清楚,那必须还得从Tomcat源码来入手。

找到org.apache.catalina.startup.Bootstrap类,发现该类中有main方法,没错这就是Tomcat的起点。

public static void main(String args[]) {
    if (daemon == null) {
        // 创建Bootstrap对象:
        Bootstrap bootstrap = new Bootstrap();
        try {
            // bootstrap初始化
            bootstrap.init();
        } catch (Throwable t) {
            handleThrowable(t);
            t.printStackTrace();
            return;
        }
        daemon = bootstrap;
    } else {
        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")) {

            // command为start,代表着启动:
            daemon.setAwait(true);
            daemon.load(args);
            daemon.start();
        } 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) {
        .....省略
    }
}

在main方法中,我们首先创建了一个Bootstrap对象,并调用其init()方法。

在初始化结束后,判断虚拟机传入的指令,通常为“start”,开始加载、启动流程,调用了daemon.load(args)、daemon.start()方法来实现。

public void init() throws Exception {
    setCatalinaHome();
    setCatalinaBase();
    // Tomcat类加载器初始化
    initClassLoaders();

    // 设置线程上下文类加载器:
    Thread.currentThread().setContextClassLoader(catalinaLoader);
    
    SecurityClassLoad.securityClassLoad(catalinaLoader);

    if (log.isDebugEnabled())
        log.debug("Loading startup class");

    // 创建Catalina对象:
    Class startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.newInstance();
    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);
    
    // 将Catalina对象中的 parentClassLoader属性设置为sharedLoader类加载器
    method.invoke(startupInstance, paramValues);

    catalinaDaemon = startupInstance;
}

在Bootstrap初始化方法中,有两行值得注意,(1)initClassLoaders:初始化Tomcat类加载器、(2)Thread.currentThread().setContextClassLoader(catalinaLoader):将catalinaLoader设置为线程上下文类加载器(打破了双亲委派模型);

紧接着,又通过反射的方式,创建了Catalina对象,并对Catalina对象中的parentClassLoader属性重新赋值,赋值为sharedLoader类加载器,也就是common ClassLoader。注意,此处赋值的目的是为了后续创建WebApp ClassLoader使用。

private void initClassLoaders() {
    try {
        // Common类加载器  
        commonLoader = createClassLoader("common", null);
        if( commonLoader == null ) {
            commonLoader=this.getClass().getClassLoader();
        }
        // Catalina类加载器  
        catalinaLoader = createClassLoader("server", commonLoader);
        // Shared类加载器  
        sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}

在初始化Tomcat类加载器方法中,我们总共创建了3大类加载器,分别为commonLoader、catalinaLoader、sharedLoader。默认情况下,在Tomcat6.0之后,commonLoader、catalinaLoader、sharedLoader对象相同,并且均为org.apache.catalina.loader.StandardClassLoader实例。

Tomcat类加载器初始化时,很明确是指出catalina ClassLoader和shared ClassLoader类加载器的父类加载器为common ClassLoader类加载器,而common ClassLoader的父类加载器传值为null,继续调用createClassLoader()方法:

private ClassLoader createClassLoader(String name, ClassLoader parent)throws Exception {
    // 从conf/catalina.properties中获取具体的key:
    String value = CatalinaProperties.getProperty(name + ".loader");
    if ((value == null) || (value.equals("")))
        return parent;

    // 将common.loader中的值,替换成服务器上的绝对路径,并以;相隔
    value = replace(value);

    List repositories = new ArrayList();

    // 遍历value中的值:
    StringTokenizer tokenizer = new StringTokenizer(value, ",");
    while (tokenizer.hasMoreElements()) {
        String repository = tokenizer.nextToken().trim();
        if (repository.length() == 0) {
            continue;
        }
        ...省略

        // 将value中的值,组装成Repository对象,并存入到List集合中
        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);
}

在创建ClassLoader方法中,我们解析catalina.properties文件中的common.loader属性,默认值为${catalina.base}/lib,${catalina.base}/lib/.jar,${catalina.home}/lib,${catalina.home}/lib/.jar**,将默认值中的${}进行替换,最终得到了服务器上的绝对路径,例如:(catalina.base,catalina.home是环境变量中的值,可通过System.getProperty("xxx")获得)

F:\framework_code\apache-tomcat-7.0.88-src/lib,
F:\framework_code\apache-tomcat-7.0.88-src/lib/*.jar,
F:\framework_code\apache-tomcat-7.0.88-src/lib,
F:\framework_code\apache-tomcat-7.0.88-src/lib/*.jar

在得到这些绝对路径后,进行Repository对象组装,并添加到ArrayList集合中来。最后,调用ClassLoaderFactory.createClassLoader(repositories, parent)生成具体的ClassLoader实现类。

在创建common ClassLoader中:commonLoader = createClassLoader("common", null),传递父类加载器为parent = null,当实际创建类加载器对象时,会以系统类加载器(App ClassLoader)作为common ClassLoader的父类加载器。

说完了Common ClassLoader、Catalina ClassLoader、Shared ClassLoader的实例化,我们接下来在来看看WebApp ClassLoader在Tomcat中是如何初始化的!!!

之前,在创建Catalina对象时候,我们说到了对Catalina对象中的parentClassLoader属性重新赋值,是为了创建WebApp ClassLoader时使用。具体如何,我们来看下源码!!

由于Tomcat代码太多,本次不展示代码的流转过程,直接来看源码!

org.apache.catalina.loader.WebappClassLoader创建源码,存在于org.apache.catalina.loader.WebappLoader类的createClassLoader()方法中:

private WebappClassLoaderBase createClassLoader() throws Exception {

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

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

    return classLoader;
}

loaderClass是WebappLoader的实例变量,其值为org.apache.catalina.loader.WebappClassLoader,通过反射调用了WebappClassLoader的构造函数,然后传递了sharedLoader(也就是common ClassLoader)作为其父亲加载器。

来看下org.apache.catalina.loader.WebappClassLoader的构造器:

public WebappClassLoader(ClassLoader parent) {
    super(parent);
}

由于WebappClassLoader继承org.apache.catalina.loader.WebappClassLoaderBase类,所以来看看WebappClassLoaderBase类的构造:

public WebappClassLoaderBase(ClassLoader parent) {

    super(new URL[0], parent);

    ClassLoader p = getParent();
    if (p == null) {
        p = getSystemClassLoader();
    }
    this.parent = p;

    ClassLoader j = String.class.getClassLoader();
    if (j == null) {
        j = getSystemClassLoader();
        while (j.getParent() != null) {
            j = j.getParent();
        }
    }
    this.j2seClassLoader = j;

    securityManager = System.getSecurityManager();
    if (securityManager != null) {
        refreshPolicy();
    }
}

将父类加载传入,赋值给parent属性,为sharedLoader(也就是common ClassLoader)。此外,继续对j2seClassLoader属性赋值,通过遍历获取到了sun.misc.Launcher.ExtClassLoader类加载。

至此,整个Tomcat类加载器初始化流程结束。

工作中遇到的类加载问题

上面我们说了Tomcat的类加载机制。

其中,关于Tomcat的类加载器加载路径,笔者提到了跟/conf/catalina.properties文件有关。

在该文件中,可以自定义common.loader属性,修改此属性后,其他目录下的.jar包、.class文件可以通过Tomcat的common ClassLoader加载进来。

在笔者工作的环境中,Tomcat的此属性就被进行了修改,将应用中maven所有依赖的jar包都放到了common.loader所配置的目录下,这样做的结果就是:common ClassLoader将加载应用中依赖的jar包,而应用本身的WebappClassLoader失去了原本存在的意义。不过,这么做倒没什么关系,只是在代码开发时,会出现莫名的异常。

Java虚拟机--Tomcat类加载_第4张图片
image

截图中,就是笔者所在公司的项目结构,使用了maven的模块化功能。

将hessian打成war包,进行Tomcat部署,hessian中如果没有servlet的话,基本上没有业务代码。

facade是对外的接口层,core是底层实现。

hessian依赖facade、core模块。

编写测试代码:

在hessian模块下创建servlet类,并调用core模块中的任意一个类。

public class TestClassLoaderServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) 
        throws ServletException, IOException {
        super.service(req, resp);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
        throws ServletException, IOException {
        super.doGet(req, resp);
        TestUtil.test();
    }
}

在core模块中,类加载hessian模块下的创建servlet类,使用Class.forName("xxxx")即可。

public static void test(){
    try{
        Class clazz =  Class.forName("com.jiaboyan.hessian.TestClassLoaderServlet");
        logger.info("要加载的类为:",clazz);
    }catch (Throwable th){
        logger.error("异常",th);
    }
}

在本地对hessian进行打包,结果如下:

Java虚拟机--Tomcat类加载_第5张图片
image

项目中的依赖,以及core模块、facade模块都会被打到hessian.war/WEB-INF/lib下。

本地tomcat启动,可以在idea中启动,也可以放到Tomcat目录中去启动也行。

输出结果如下:

要加载的类为:class com.jiaboyan.hessian.TestClassLoaderServlet

一切都是如此的正常。

此时,你的新功能开发完了,自测完了,你需要部署到测试服务器中。利用jenkins进行部署,公司的jenkins进行了修改,在部署构建过程中,会将war包中/WEB-INF/lib下的所有jar包,拷到Tomcat/commonlib目录下,并删除/WEB-INF/lib下的所有jar包。

而我们公司的Tomcat又对/conf/catalina.properties中的common.loader进行了修改:

common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar,${catalina.home}/commonlib/*.jar,${catalina.home}/commoncfg

通过common.loader的配置可知,common ClassLoader会加载commonlib目录下的.jar包。

如果此时你发现了问题,那么说明你已经理解了Tomcat的类加载机制。

由此可见,对于core模块、facade模块下的代码,均有common ClassLoader进行加载,而hessian模块中的代码是由WebApp ClassLoader进行加载。并且,common ClassLoader是WebApp ClassLoader的父类加载器,父类加载无法加载子类加载器的资源,当测试服务器中代码执行时候,common ClassLoader无法加载WebApp ClassLoader所拥有的类,结果就是类加载异常。

Java虚拟机--Tomcat类加载_第6张图片
image

你可能感兴趣的:(Java虚拟机--Tomcat类加载)