二、Tomcat打破双亲委派机制

1. Tomcat 如果使用默认的双亲委派类加载机制行不行?

        首先我们思考一下:Tomcat是一个web容器,那么它要解决什么问题?

        1)、一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。

        2)、部署在同一个web容器中相同的类库相同的版本可以共享。否则如果服务器有10个应用程序,那么要有10份相同的类库加载器进虚拟机。

        3)、web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来

        4)、web容器要支持jsp的修改,我们知道,JSP文件最终也是要编译成class文件才能在虚拟机中运行,但是程序运行后修改jsp已经是已经司空见惯的事情,web容器需要支持jsp修改后不用重启。

        现在再看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行?

        答案是不行的。为什么呢?

        第一个问题:如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加载器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。

        第二个问题:默认的类加载器是能够实现的,因为他的职责就是保证唯一性。

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

        第四个问题:怎么实现jsp文件的热加载,jsp文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改好了,就直接卸载这个jsp类加载器,重新创建类加载器,重新加载jsp文件。

2. Tomcat是如何打破双亲委派机制的?

        tomcat可以加载各种各样类型的war包,相互之间没有影响。因为tomcat打破了双亲委派机制。

        2.1 Tomcat自定义加载器详解

        

        如上图,上面的橙色部分还是和原来一样,采用双亲委派机制,而黄色部分是tomcat第一部分自定义的类加载器,这部分主要加载tomcat包中的类,这一部分依然采用的是双亲委派机制,而绿色部分是tomcat第二部分自定义类加载器,正是这一部分,打破了类的双亲委派机制。

        黄色部分加载器,在tomcat7及以前是tomcat自定义的三个类加载器,分别加载不同文件夹下的jar包,而到了tomcat7及以后,tomcat将这三个文件夹合并了,合并成了一个lib包,也就是现在看到的lib包。

我们来看看这三个类加载器的三个主要功能:

  • commonClassLoader:tomcat最基本的类加载器,加载路径中的class可以被tomcat容器本身和各个webapp访问;
  • catalinaClassLoader:tomcat容器中私有的类加载器,加载路径中的class对于webapp不可见的部分。
  • sharedClassLoader:各个webapps共享的类加载器,加载路径中的class对于所有的webapp都可见,但是对于tomcat容器不可见。
  • WebappClassLoader:各个webapp私有的类加载器,加载路径中的class只对当前webapp可见,比如加载war包里相关的类,每个war包应用都是自己的
  • WebappClassLoader:实现相互隔离,比如不同的war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本。

 如上图中的委派关系中可以看出:

        CommonClassLoader能加载的类都是可以被被CatalinaClassLoader和SharedClassLoader使用,从而实现了共有的类库的共用,而被CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。

        WebAppClassLoader可以使用SharedClassLoader类加载器的类,但各个WebAppClassLoader实例之间相互隔离。

        而jasperLoader的加载器范围仅仅是这个JSP文件编译出来的那一个.calss文件,他出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的JSP类加载器来实现JSP文件的热加载功能。

omcat 这种类加载机制违背了java 推荐的双亲委派模型了吗?答案是:违背了。

绿色部分是java项目在打war包的时候, tomcat自动生成的类加载器, 也就是说 , 每一个项目打成一个war包, tomcat都会自动生成一个类加载器, 专门用来加载这个war包. 而这个类加载器打破了双亲委派机制. 我们可以想象一下, 假如这个webapp类加载器没有打破双亲委派机制会怎么样?

之前也说过,如果没有打破, 他就会委托父类加载器去加载, 一旦加载到了, 子类加载器就没有机会在加载了. 那么, spring4和spring5的项目想共存, 那是不可能的了.

所以, 这一部分他打破了双亲委派机制

这样一来, webapp类加载器不需要在让上级去加载, 他自己就可以加载对应war里的class文件. 当然了, 其他的基础项目文件, 还是要委托上级加载的.

下面我们来实现一个自定义的tomcat类加载器

3. 自定义tomcat的war包类加载器

如何打破双亲委派机制, 我们在上面已经写过一个demo了.

那么, 现在我有两个war包, 分处于不同的文件夹, tomcat如何使用各自的类加载器加载自己包下的class类呢?

我们来举个例子, 比如: 在我的本地目录下有两个文件夹, tomcat-test和tomcat-test1. 用这两个文件夹来模拟两个项目,在他们下面都有一个com/jvm/User1.class

 虽然类名和类路径都是一样的,但是他们的内容是不同的

这个时候,如果tomcat要同时加载这两个目录下的User1.class文件, 我们如何操作呢?

其实,非常简单, 按照上面的思路, tomcat只需要为每一个文件夹生成一个新的类加载器就可以了.

代码如下

/**
 * 自定义类加载器
 */
public class MyClassLoaderTest{
    static class MyClassLoader extends ClassLoader {
        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        public Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

        private byte[] loadByte(String name) throws Exception {
            //需要读取类的路径
            name = name.replaceAll("\\.", "/");
            //根据路径查找这个类
            FileInputStream file = new FileInputStream(classPath + "/" + name + ".class");
            int len = file.available();
            byte[] bytes = new byte[len];
            file.read(bytes);
            file.close();
            return bytes;
        }

        /**
         * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
         * @param name
         * @param resolve
         * @return
         * @throws ClassNotFoundException
         */
        protected Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                Class<?> c = findLoadedClass(name);
                    if (c == null) {
                        long t1 = System.nanoTime();
                        if (!name.startsWith("com.jvm")){
                            c = this.getParent().loadClass(name);
                        }else {
                            c = findClass(name);
                        }
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    }
    public static void main(String[] args) throws Exception {
        //初始化自定义类加载器会先初始化父类ClassLoader,
        // 其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("E:/tomcat-test");
        //E盘创建 tomcat-test/com/jvm 几级目录,将User类的复制类User1.class丢入该目录
        Class<?> clazz = classLoader.loadClass("com.jvm.User1");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj,null);
        System.out.println(clazz.getClassLoader().getClass().getName());

        System.out.println();
        MyClassLoader classLoader = new MyClassLoader("E:/tomcat-test1");
        //E盘创建 tomcat-test1/com/jvm 几级目录,将User类的复制类User1.class丢入该目录
        Class<?> clazz = classLoader.loadClass("com.jvm.User1");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj,null);
        System.out.println(clazz.getClassLoader().getClass().getName());

    }
}

 运行结果:

=======自己的加载器加载类调用方法=======
com.tuling.jvm.MyClassLoaderTest$MyClassLoader@266474c2 

=======另外一个User1版本:自己的加载器加载类调用方法=======
com.tuling.jvm.MyClassLoaderTest$MyClassLoader@66d3c61

注意:同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一 样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类 加载器也是同一个才能认为他们是同一个。

 附下User类的代码:

package com.jvm;

public class User1 {

    private int id;
    private String name;

    public User1() {
    }
    
    public User1(int id, String name) { 
        super();
        this.id = id;
        this.name = name; 
    }
    public int getId() { 
        return id; 
    }
    
    public void setId(int id) { 
        this.id = id; 
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void sout() {
        System.out.println("=======另外一个User1版本:自己的加载器加载类调用方法=======");
    }
}

4. 思考: tomcat自定义的类加载器中, 有一个jsp类加载器,jsp是可以实现热部署的, 那么他是如何实现的呢?

jsp其实是一个servlet容器, 由tomcat加载. tomcat会为每一个jsp生成一个类加载器. 这样每个类加载器都加载自己的jsp, 不会加载别人的. 当jsp文件内容修改时, tomcat会有一个监听程序来监听jsp的改动. 比如文件夹的修改时间, 一旦时间变了, 就重新加载文件夹中的内容.

具体tomcat是怎么实现的呢? tomcat自定义了一个thread, 用来监听不同文件夹中文件的内容是否修改, 如何监听呢? 就看文件夹的update time有没有变化, 如果有变化了, 那么就会重新加载.

jsp热部署也不是立刻就会看到效果,其他他也是有延迟的,这个延迟就是重新加载的过程。

  • 6
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值