Tomcat - 模拟Tomcat的webappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离

简介: Tomcat - 模拟Tomcat的webappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离

20200613095116593.png

Tomcat要解决什么问题?


作为一个Web容器,Tomcat要解决什么问题 , Tomcat 如果使用默认的双亲委派类加载机制能不能行?


我们知道Tomcat可以部署多个应用,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离 .


举个例子 假设APP1 使用的是 Spring4 , APP2 使用的是Spring5 , 毫无疑问 Spring4 和 Spring 5 肯定有 类的全路径一样的类吧,如果使用双亲委派 ,父加载器加载谁?


部署在同一个web容器中相同的类库相同的版本可以共享, 比如jdk的核心jar包,否则,如果服务器有n个应用程序,那么要有n份相同的类库加载进虚拟机。


web容器 自己依赖的类库 (tomcat lib目录下),不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。


2020061300050537.png



web容器要支持jsp的修改, jsp 文件最终也是要编译成class文件才能在虚拟机中运行, web容器需要支持 jsp 修改后不用重启 ,就是热加载的功能。


结合上面的4个问题,我们看下双亲委派能不能支持?


第一个问题,如果使用默认的类加载器机制,肯定是无法加载两个相同类库的不同版本的,如果使用双亲委派,让父加载器去加载 ,不管你是什么版本的,只要你的全限定类名一样,那肯定只有一份,APP 隔离 无法满足


第二个问题,默认的类加载器是能够实现的,很好理解嘛, 就是双亲委派的功能,保证唯一性。


第三个问题和第一个问题一样。


第四个问题, 要怎么实现jsp文件的热加载呢? jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?可以直接卸载掉这jsp文件的类加载器 .当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。 源码详见: org.apache.jasper.servlet.JasperLoader


Tomcat违反了双亲委派机制?


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


也不尽然,核心的Java的加载还是遵从双亲委派 。

Tomcat中 各个web应用自己的类加载器(WebAppClassLoader)会优先加载,打破了双亲委派机制。加载不到时再交给commonClassLoader走双亲委托 .


模拟Tomcat的webappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离


我们基于JVM - 实现自定义的ClassLoader就是这么简单


package com.gof.facadePattern;
import java.io.FileInputStream;
import java.lang.reflect.Method;
/**
 * @author 小工匠
 * @version v1.0
 * @create 2020-06-11 23:09
 * @motto show me the code ,change the word
 * @blog https://artisan.blog.csdn.net/
 * @description
 **/
public class MyClassLoaderTest {
    static class MyClassLoader extends ClassLoader {
        private String classPath;
        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }
        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }
        protected Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    if ("com.gof.facadePattern.Boss1".equals(name)){
                        c = findClass(name);
                    }else{
                        // 交由父加载器去加载
                        c = this.getParent().loadClass(name);
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
    }
    public static void main(String args[]) throws Exception {
        //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:/artisan");
        //D盘创建 artisan/com/gof/facadePattern 目录,将Boss类的复制类Boss1.class丢入该目录
        Class clazz = classLoader.loadClass("com.gof.facadePattern.Boss1");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader() );
        System.out.println();
        MyClassLoader classLoader1 = new MyClassLoader("D:/artisan1");
        //D盘创建 artisan1/com/gof/facadePattern 目录,将Boss类的复制类Boss1.class丢入该目录
        Class clazz1 = classLoader1.loadClass("com.gof.facadePattern.Boss1");
        Object obj1 = clazz1.newInstance();
        Method method1 = clazz1.getDeclaredMethod("sout", null);
        method1.invoke(obj1, null);
        System.out.println(clazz1.getClassLoader() );
    }
}

20200613142701369.png


为了好区分 我们把Boss1 的类 ,sout方法的输出稍微调整下,以示区别。

应用中的Boss1 无需删除

20200614003245655.png


同时模拟第二个应用, 在D盘创建 artisan1/com/gof/facadePattern 目录,将Boss类的复制类Boss1.class丢入该目录


基于以上前置条件,得出如下结论


我们通过上面的示例模拟出了同一个JVM内, 分别使用不同的类加载器(new 出来的)去加载不同classpath下的类,而避免了走双亲委派,去模拟tomcat的类加载机制


通过结论可以得出在同一个JVM内,两个相同包名和类名的类对象可以共存,是因为他们的类加载器不一样。


所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器是否相同


Tomcat加载机制小结


20200604090610632.png


当tomcat启动时,会创建几种类加载器:


Bootstrap 引导类加载器 : 加载JVM启动所需的类,以及标准扩展类(位于jre/lib/ext下)


System 系统类加载器 : 加载tomcat启动的类,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下


20200613093125911.png

4. webapp 应用类加载器: 每个应用在部署后,都会创建一个唯一的类加载器。该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。


Common 通用类加载器:加载tomcat使用以及应用通用的一些类,位于CATALINA_HOME/lib下,比如servlet-api.jar

20200613093211921.png



总之 当应用需要到某个类时,则会按照下面的顺序进行类加载:


1 使用bootstrap引导类加载器加载 (JVM 的东西 )


2 使用system系统类加载器加载 (tomcat的启动类Bootstrap包)


3 使用WebAppClassLoader 加载 WEB-INF/classes (应用自定义的class)


4 使用WebAppClassLoader 加载在WEB-INF/lib (应用的依赖包)


5 使用common类加载器在CATALINA_HOME/lib中加载 (tomcat的依赖包,公共的,被各个应用共享的)


相关文章
|
5月前
|
Java 应用服务中间件
idea tomcat 404 无法自动打开本地项目war包路径
idea tomcat 404 无法自动打开本地项目war包路径
72 0
|
4月前
|
Java 应用服务中间件 API
SpringBoot项目 Tomcat部署war程序时启动成功但是访问404异常处理
SpringBoot项目 Tomcat部署war程序时启动成功但是访问404异常处理
84 0
|
5月前
|
缓存 JavaScript 应用服务中间件
Nginx+Tomcat代理环境下JS无法完全加载问题
Nginx+Tomcat代理环境下JS无法完全加载问题
|
4月前
|
Java 应用服务中间件 容器
SpringBoot配置外部Tomcat并打war包
SpringBoot配置外部Tomcat并打war包
70 0
|
1天前
|
自然语言处理 Java 应用服务中间件
从零手写实现 tomcat-09-servlet 处理类
该文是一个关于手写实现 Apache Tomcat 简化版的系列教程摘要。作者希望通过亲自实现一个简单的 Tomcat,来深入理解其工作原理。系列教程包括了从入门介绍到解析处理 WAR 包、与 SpringBoot 集成等多个步骤。文章介绍了 Servlet 的概念,将其比作餐厅服务员,负责处理网络请求和响应。文中还详细阐述了 Servlet 的处理流程,并通过实例解释了如何实现一个基础的 Servlet。最后,提到了如何根据请求 URL 进行调度和处理,并给出了迷你版 Tomcat(Mini-Cat)的开源地址。
|
4月前
|
Java 应用服务中间件
SpringBoot 项目war包部署 配置外置tomcat方法
SpringBoot 项目war包部署 配置外置tomcat方法
71 0
|
3月前
|
存储 安全 Java
从HTTP到Tomcat:揭秘Web应用的底层协议与高性能容器
从HTTP到Tomcat:揭秘Web应用的底层协议与高性能容器
|
3月前
|
Java 应用服务中间件 Maven
Tomcat部署SpringBoot war包
Tomcat部署SpringBoot war包
29 0
|
4月前
|
jenkins Java 应用服务中间件
Jenkins【部署 01】两种方式+两种环境部署最新版本 Jenkins v2.303.2 WAR包(直接使用 java -jar+使用Tomcat的Web端部署)
Jenkins【部署 01】两种方式+两种环境部署最新版本 Jenkins v2.303.2 WAR包(直接使用 java -jar+使用Tomcat的Web端部署)
78 0
|
5月前
|
Java 应用服务中间件 Android开发
IDEA Eclipse项目如何导入tomcat里面的jar包
IDEA Eclipse项目如何导入tomcat里面的jar包
65 0