目錄
引子
在之前的一篇文章談談JAVA中物件和類別、this、super和static關鍵字中,我們知道Java 是如何建立物件的
类加载器 与 双亲委派机制
ClassLoader
自定义类加载器
编写一个自定义的类加载器
为什么我们这边要打破双亲委派机制
自定义类加载器时,如何打破双亲委派机制
SPI机制 与 线程上下文类加载器
JDBC
Tomcat
SpringBoot Starter
首頁 Java java教程 Java類別載入器與雙親委派機制怎麼應用

Java類別載入器與雙親委派機制怎麼應用

Apr 18, 2023 pm 05:19 PM
java

引子

大家想必都有過平常開發springboot 專案的時候稍微改動一點程式碼,就得重啟,就很煩

網路上一般介紹2種方式 spring- boot-devtools,或透過JRebel外掛程式來實現"熱部署"

熱部署就是當應用程式正在運行時,修改應用程式不需要重新啟動應用程式。

其中 spring-boot-devtools其實是自動重啟,主要是節省了我們手動點擊重啟的時間,不算是真正意義上的熱部署。 JRebel插件啥都好,就是需要收費

但如果平常我們在調試debug的情況下,只是在方法塊內程式碼修改了一下,我們還得重啟項目,就很浪費時間。這時候我們其實可以直接build ,不重啟項目,可以 實現熱部署。

我們先來寫一個範例來示範:

@RestController
public class TestController {
    @RequestMapping(value = "/test",method = {RequestMethod.GET, RequestMethod.POST})
    public void testclass() {
        String name = "zj";
        int weight = 100;
        System.out.println("name:"+ name);
        System.out.println("weight: "+weight);
    }
}
登入後複製

結果:

name:zj weight: 100

##修改程式碼,然後直接build項目,不重啟項目,我們再請求這個測試接口:

String name = "ming";
int weight = 300;
登入後複製

神奇的一幕出現了,結果為:

name:ming weight: 300

當我們修改.java文件,只要重新產生對應的.class文件,就能影響到程式運作結果, 無需重啟,Why? 背後JVM的操作原理且看本文証娓道來。

了解.class檔案

首先我們得先了解什麼是.class檔案

舉個簡單的例子,建立一個Person類別:

public class Person {
    /**
     * 状态 or 属性
     */
    String name;//姓名
    String sex;//性别
    int height;//身高
    int weight;//体重
    
    /**
     * 行为
     */
    public void sleep(){
     System.out.println(this.name+"--"+ "睡觉");
 }
    public void eat(){
        System.out.println("吃饭");
    }
    public void Dance(){
        System.out.println("跳舞");
    }
}
登入後複製

我們執行javac指令,產生Person.class檔

然後我們透過

vim 16進位 開啟它

#打开file文件
vim Person.class 

#在命令模式下输入.. 以16进制显示
 :%!xxd
 
#在命令模式下输入.. 切换回默认显示
:%!xxd -r
登入後複製

Java類別載入器與雙親委派機制怎麼應用##不同的作業系統,不同的CPU 有不同的指令集,JAVA能做到平台無關性,依賴的就是Java 虛擬機器。 .java原始碼是給人類讀的,而.class字節碼是給JVM虛擬機讀的,計算機只能識別 0 和 1組成的二進製文件,所以虛擬機就是我們編寫的代碼和計算機之間的橋樑。

虛擬機器將我們寫的.java 原始程式檔案編譯為字節碼格式的.class 文件,字節碼是各種虛擬機器與所有平台統一使用的程式儲存格式,class檔案主要使用於解決平台無關性的中間檔案

Java類別載入器與雙親委派機制怎麼應用類別載入的過程

在之前的一篇文章談談JAVA中物件和類別、this、super和static關鍵字中,我們知道Java 是如何建立物件的

 Person zhang = new Person();
登入後複製

雖然我們寫的時候是簡單的一句,但是JVM內部的實作過程是複雜的:

    將硬碟上指定位置的Person.class檔案載入進記憶體
  • 執行main方法時,在堆疊記憶體中開闢了main方法的空間(壓棧-進棧),然後在main方法的棧區分配了一個變數zhang。
  • 執行new,在堆記憶體中開闢一個實體類別的空間,分配了一個記憶體首位址值
  • 呼叫該實體類別對應的構造函數,進行初始化(如果沒有建構函數,Java會補上一個預設構造函數)。
  • 將實體類別的 首位址賦值給zhang,變數zhang就引用了該實體。 (指向了該物件)

Java類別載入器與雙親委派機制怎麼應用類別載入程序

其中上圖步驟1 Classloader(類別載入器) 將class文件載入到記憶體中具體分為3個步驟:載入、連線、初始化

類別的生命週期一般有如下圖有7個階段,其中階段1-5為類別載入過程,驗證、準備、解析統稱為連接

Java類別載入器與雙親委派機制怎麼應用類別的生命週期

#1.載入

##載入

階段:指的是將類別對應的.class檔案中的二進位位元組流讀入到記憶體中,將這個位元組流轉換為方法區的執行時間資料結構,然後在堆區建立一個java.lang.Class對象,作為對方法區中這些資料的存取入口

相對於類別載入的其他階段而言,載入階段(準確地說,是載入階段獲取類別的二進位位元組流的動作)是我們最可以控制的階段,因為開發人員既可以使用系統提供的類別載入器來完成加載,也可以自訂類別載入器來完成載入。這個我們文章後面再詳細講

2.驗證

驗證階段:校驗字節碼檔案正確性。此階段的目的是為了確保Class檔案的位元組流中所包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器本身的安全。

這部分對開發者而言是無法介入的,以下內容了解即可

驗證階段大致會完成4個階段的檢驗動作:

文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

3.准备

准备阶段:为类变量(static 修饰的变量)分配内存,并将其初始化为默认值

注意此阶段仅仅是为类变量 即静态变量分配内存,并将其初始化为默认值

举个例子,在这个准备阶段

static int value = 3;//类变量 初始化,设为默认值 0,不是 3哦 !!!
int num = 4;//类成员变量,在这个阶段不初始化;在 new类,调用对应类的构造函数才进行初始化
final static valFin = 5;//这个比较特殊,在这个阶段也不会分配内存!!!
登入後複製

注意: valFin 是被final static修饰的常量在 **编译 **的时候已分配好了,所以在准备阶段 此时的值为5,所以在这个阶段也不会初始化!

4.解析

解析阶段:是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

符号引用就是一组符号来描述目标,可以是任何字面量。

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

这个阶段了解一下即可

5.初始化

直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。

初始化阶段 是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控 制。

Java程序对类的使用方式可分为两种:主动使用被动使用。一般来说只有当对类的首次主动使用的时候才会导致类的初始化,所以主动使用又叫做类加载过程中“初始化”开始的时机。

类实例初始化方式,主要是以下几种:

1、创建类的实例,也就是new的方式

2、访问某个类或接口的静态变量,或者对该静态变量赋值

3、调用类的静态方法

4、反射(如Class.forName("com.test.Person")

5、初始化某个类的子类,则其父类也会被初始化

6、Java虚拟机启动时被标明为启动类的类(JavaTest),还有就是Main方法的类会 首先被初始化

这边就不展开说了,大家记住即可

6.使用

当JVM完成初始化阶段之后,JVM便开始从入口方法开始执行用户的程序代码

7.卸载

当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象,最后负责运行的JVM也退出内存

在如下几种情况下,Java虚拟机将结束生命周期

执行了System.exit()方法

程序正常执行结束

程序在执行过程中遇到了异常或错误而异常终止

由于操作系统出现错误而导致Java虚拟机进程终止

类加载器 与 双亲委派机制

上文类加载过程中,是需要类加载器的参与,类加载器在Java中非常重要,它使得 Java 类可以被动态加载到 Java 虚拟机中并执行

那什么是类加载器?通过一个类的全限定名来获取描述此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例

Java虚拟机支持类加载器的种类:主要包括3中:引导类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用类加载器(系统类加载器,AppClassLoader),另外我们还可以自定义加载器-用户自定义类加载器

Java類別載入器與雙親委派機制怎麼應用

  • 引导类加载器(Bootstrap ClassLoader):BootStrapClassLoader是由c++实现的。引导类加载器加载java运行过程中的核心类库JRE\lib\rt.jar,sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar 以及存放 在JRE\classes里的类,也就是JDK提供的类等常见的比如:Object、Stirng、List

  • 扩展类加载器(Extension ClassLoader):它用来加载/jre/lib/ext目录以及java.ext.dirs系统变量指定的类路径下的类。

  • 应用类加载器(AppClassLoader):它主要加载应用程序ClassPath下的类(包含jar包中的类)。它是java应用程序默认的类加载器。其实就是加载我们一般开发使用的类

  • 用户自定义类加载器:用户根据自定义需求,自由的定制加载的逻辑,只需继承应用类加载器AppClassLoader,负责加载用户自定义路径下的class字节码文件

  • 线程上下文类加载器:除了以上列举的三种类加载器,其实还有一种比较特殊的类型就是线程上下文类加载器。ThreadContextClassLoader可以是上述类加载器的任意一种,这个我们下文再细说

我们来看一个例子:

public class TestClassLoader {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = TestClassLoader.class.getClassLoader();
        System.out.println(classLoader);
        System.out.println(classLoader.getParent());//获取其父类加载器
        System.out.println(classLoader.getParent().getParent());//获取父类的父类加载器
    }
}
登入後複製

结果:

sun.misc.Launcher
ExtClassLoader@5caf905d null

结果显示分别打印应用类加载器、扩展类加载器和引导类加载器

由于 引导类加载器 是由c++实现的,所以并不存在一个Java的类,因此会打印出null

我们还可以看到结果里面打印了 sun.misc.Launcher,这个是什么东东?

其实Launcher是JRE中用于启动程序入口main()的类,我们看下Launcher的源码:

public class Launcher {
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader(); //加载扩展类类加载器
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);//加载应用程序类加载器,并设置parent为extClassLoader
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

        Thread.currentThread().setContextClassLoader(loader); //设置AppClassLoader为线程上下文类加载器
    }

    /*
     * Returns the class loader used to launch the main application.
     */
    public ClassLoader getClassLoader() {
        return loader;
    }
    /*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {}

/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {}
登入後複製

其中loader = AppClassLoader.getAppClassLoader(extcl);的核心方法源码如下:

private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;//设置parent
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            assertionLock = this;
        }
    }
登入後複製

通过以上源码我们可以知晓:

  • Launcher的ClassLoaderBootstrapClassLoader,在Launcher创建的同时,还会同时创建ExtClassLoader,AppClassLoader(并设置其parent为extClassLoader)。其中代码中 "sun.boot.class.path"是BootstrapClassLoader加载的jar包路径。

  • 这几种类加载器 都遵循 双亲委派机制

双亲委派机制说的其实就是,当一个类加载器收到一个类加载请求时,会去判断有没有加载过,如果加载过直接返回,否则该类加载器会把请求先委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。

双亲委派模式优势:

  • 避免类的重复加载, 当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次, 这样保证了每个类只被加载一次。

  • 保护程序安全,防止核心API被随意篡改,比如 java核心api中定义类型不会被随意替换

我们这里看一个例子:

我们新建一个自己的类“String”放在src/java/lang目录下

public class String {
    static {
        System.out.println("自定义 String类");
    }
}
登入後複製

新建StringTest类:

public class StringTest {
    public static void main(String[] args) {
        String str=new java.lang.String();
        System.out.println("start test-------");
    }
}
登入後複製

结果:

start test-------

可以看出,程序并没有运行我们自定义的“String”类,而是直接返回了String.class。像String,Integer等类 是JAVA中的核心类,是不允许随意篡改的!

ClassLoader

ClassLoader 是一个抽象类,负责加载类,像 ExtClassLoader,AppClassLoader 都是由该类派生出来,实现不同的类装载机制。这块的源码太多了,就不贴了

我们来看下 它的核心方法loadClass(),传入需要加载的类名,它会帮你加载:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 一开始先 检查是否已经加载该类
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 如果未加载过类,则遵循 双亲委派机制,来加载类
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                //如果父类是null就是BootstrapClassLoader,使用 启动类类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                long t1 = System.nanoTime();
                // 如果还是没有加载成功,调用findClass(),让当前类加载器加载
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

// 继承的子类得重写该方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
登入後複製

loadClass()源码 展示了,一般加载.class文件大致流程:

  • 先去缓存中 检查是否已经加载该类,有就直接返回,避免重复加载;没有就下一步

  • 遵循 双亲委派机制,来加载.class文件

  • 上面两步都失败了,调用findClass()方法,让当前类加载器加载

注意:由于ClassLoader类是抽象类,而抽象类是无法通过new创建对象的,所以它最核心的findClass()方法,没有具体实现,只抛了一个异常,而且是protected的,这是应用了模板方法模式,具体的findClass()方法丢给子类实现, 所以继承的子类得重写该方法。

自定义类加载器

编写一个自定义的类加载器

那我们仿照 ExtClassLoader,AppClassLoader 来实现一个自定义的类加载器,我们同样是继承ClassLoader

编写一个测试类TestPerson

public class TestPerson {
    String name = "xiao ming";
    public void print(){
        System.out.println("hello my name is: "+ name);
    }
}
登入後複製

接着 编写一个自定义类加载器MyTestClassLoader:

public class MyTestClassLoader extends ClassLoader  {

    final String classNameSpecify  = "TestPerson";

    public MyTestClassLoader() {

    }


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

    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        File file = getClassFile(name);
        try
        {
            byte[] bytes = getClassBytes(file);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    private File getClassFile(String name)
    {
        File file = new File("D:\\ideaProjects\\src\\main\\java\\com\\zj\\ideaprojects\\test2\\"+ classNameSpecify+ ".class");
        return file;
    }

    private byte[] getClassBytes(File file) throws Exception
    {
        // 这里要读入.class的字节,因此要使用字节流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);

        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }

        fis.close();

        return baos.toByteArray();
    }

    //我们这边要打破双亲委派模型,重写整个loadClass方法
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> c = findLoadedClass(name);
        if (c == null && name.contains(classNameSpecify)){//指定的类,不走双亲委派机制,自定义加载
            c = findClass(name);
            if (c != null){
                return c;
            }
        }
        return super.loadClass(name);
    }
}
登入後複製

最后在编写一个测试controller:

@RestController
public class TestClassController {
    @RequestMapping(value = "testClass",method = {RequestMethod.GET, RequestMethod.POST})
    public void testClassLoader() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        MyTestClassLoader myTestClassLoader = new MyTestClassLoader();
        Class<?> c1 = Class.forName("com.zj.ideaprojects.test2.TestPerson", true, myTestClassLoader);
        Object obj = c1.newInstance();
        System.out.println("当前类加载器:"+obj.getClass().getClassLoader());
        obj.getClass().getMethod("print").invoke(obj);

    }
}
登入後複製

先找到TestPerson所在的目录, 执行命令:javac TestPerson,生成TestPerson.class

这里没有使用idea的build,是因为我们代码的class读取路径 是写死了的,不走默认CLASSPATH
D:\ideaProjects\src\main\java\com\zj\ideaprojects\test2\TestPerson.class

我们然后用postman调用testClassLoader()测试接口

结果:

当前类加载器:com.zj.ideaprojects.test2.MyTestClassLoader@1d75e392
hello my name is: xiao ming

然后修改TestPerson,将name 改为 “xiao niu”

public class TestPerson {
    String name = "xiao niu";
    public void print(){
        System.out.println("hello my name is: "+ name);
    }
}
登入後複製

然后在当前目录 重新编译, 执行命令:javac TestPerson,会在当前目录重新生成TestPerson.class 不重启项目,直接用postman 直接调这个测试接口 结果:

当前类加载器:com.zj.ideaprojects.test2.MyTestClassLoader@7091bd27
hello my name is: xiao niu

这样就实现了“热部署”!!!

为什么我们这边要打破双亲委派机制

如果不打破的话,结果 当前类加载器会显示"sun.misc.Launcher$AppClassLoader",原因是由于idea启动项目的时候会自动帮我们编译,将class放到 CLASSPATH路径下。其实可以把默认路径下的.class删除也行。这里也是为了展示如何打破双亲委派机制,才如此实现的。

官方推荐我们自定义类加载器时,遵循双亲委派机制。但是凡事得看实际需求嘛

自定义类加载器时,如何打破双亲委派机制

通过上面的例子我们可以看出:

1、如果不想打破双亲委派机制,我们自定义类加载器,那么只需要重写findClass方法即可

2、如果想打破双亲委派机制,我们自定义类加载器,那么还得重写整个loadClass方法

SPI机制 与 线程上下文类加载器

如果你阅读到这里,你会发现双亲委派机制的各种好处,但万物都不是绝对正确的,我们需要一分为二地看待问题。

在某些场景下双亲委派制过于局限,所以有时候必须打破双亲委派机制来达到目的。比如 :SPI机制、线程上下文类加载器

1.SPI(Service Provider Interface)服务提供接口。它是jdk内置的一种服务发现机制,将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 让服务定义与实现分离解耦

Java類別載入器與雙親委派機制怎麼應用

SPI机制图

2.线程上下文类加载器(context class loader)是可以破坏Java类加载委托机制,使程序可以逆向使用类加载器,使得java类加载体系显得更灵活。

Java 应用运行的初始线程的上下文类加载器是应用类加载器,在线程中运行的代码可以通过此类加载器来加载类和资源。Java.lang.Thread中的方法getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。

SPI机制在框架的设计上应用广泛,下面举几个常用的例子:

JDBC

平时获取jdbc,我们可以这样:Connection connection =DriverManager.getConnection("jdbc://localhost:3306");

我们读DriverManager的源码发现:其实就是查询classPath下,所有META-INF下给定Class名的文件,并将其内容返回,使用迭代器遍历,这里遍历的内部使用Class.forName加载了类。

其中有一处非常重要 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);我们看下它的实现:

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();//important !
        return ServiceLoader.load(service, cl);
    }
登入後複製

我们可以看出JDBC,DriverManager类和ServiceLoader类都是属于核心库 rt.jar 的,它们的类加载器是Bootstrap ClassLoader类加载器。而具体的数据库驱动相关功能却是第三方提供的,第三方的类不能被引导类加载器(Bootstrap ClassLoader)加载。

所以java.util.ServiceLoader类进行动态装载时,使用了线程的上下文类加载器(ThreadContextClassLoader)让父级类加载器能通过调用子级类加载器来加载类,这打破了双亲委派机制

Tomcat

Tomcat是web容器,我们把war包放到 tomcat 的webapp目录下,这意味着一个tomcat可以部署多个应用程序。

不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。防止出现一个应用中加载的类库会影响另一个应用的情况。如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。

Java類別載入器與雙親委派機制怎麼應用

Tomcat类加载器种类

  • 如果Tomcat本身的依赖和Web应用还需要共享,Common类加载器(CommonClassLoader)来装载实现共享

  • Catalina类加载器(CatalinaClassLoader) 用来 隔绝Web应用程序与Tomcat本身的类

  • Shared类加载器(SharedClassLoader):如果WebAppClassLoader自身没有加载到某个类,那就委托SharedClassLoader去加载

  • WebAppClassLoader:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器WebAppClassLoader(多个应用程序,就有多个WebAppClassLoader)负责优先加载本身的目录下的class文件加载不到时再交给CommonClassLoader以及上层的ClassLoader进行加载这破坏了双亲委派机制。

  • Jsp类加载器(JasperLoader):实现热部署的功能,修改文件不用重启就自动重新装载类库。JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

我们来模拟一下tomcat 多个版本代码共存:

这边的例子换了个电脑,所以目录结构、路径与上面的例子有点变化

Java類別載入器與雙親委派機制怎麼應用

我们先编写 App类

public class App {
    String name = "webapp 1";
    public void print() {
        System.out.println("this is "+ name);
    }
}
登入後複製

javac App生成的App.class 放入 tomcatTest\war1\com\zj\demotest\tomcatTest 目录下

将name改为webapp 2,重新生成的App.class 放入 tomcatTest\war2\com\zj\demotest\tomcatTest 目录下

然后我们编写类加载器:

public class MyTomcatClassloader extends ClassLoader {

    private String classPath;

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


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        File file = getClassFile(name);
        try
        {
            byte[] bytes = getClassBytes(file);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    private File getClassFile(String name)
    {
        name = name.replaceAll("\\.", "/");
        File file = new File(classPath+ "/"+ name + ".class");//拼接路径,找到class文件
        return file;
    }

    private byte[] getClassBytes(File file) throws Exception
    {
        // 这里要读入.class的字节,因此要使用字节流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);

        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1) {
                break;
            }

            by.flip();
            wbc.write(by);
            by.clear();
        }

        fis.close();

        return baos.toByteArray();
    }

    //我们这边要打破双亲委派模型,重写整个loadClass方法
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> c = findLoadedClass(name);
        if (c == null && name.contains("tomcatTest")){//指定的目录下的类,不走双亲委派机制,自定义加载
            c = findClass(name);
            if (c != null){
                return c;
            }
        }
        return super.loadClass(name);
    }

}
登入後複製

最后编写测试controller:

@RestController
public class TestController {

    @RequestMapping(value = "/testTomcat",method = {RequestMethod.GET, RequestMethod.POST})
    public void testclass() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        MyTomcatClassloader myTomcatClassloader = new MyTomcatClassloader("D:\\GiteeProjects\\study-java\\demo-test\\src\\main\\java\\com\\zj\\demotest\\tomcatTest\\war1");
        Class cl = myTomcatClassloader.loadClass("com.zj.demotest.tomcatTest.App");
        Object obj = cl.newInstance();
        System.out.println("当前类加载器:"+obj.getClass().getClassLoader());
        obj.getClass().getMethod("print").invoke(obj);

        MyTomcatClassloader myTomcatClassloader22 = new MyTomcatClassloader("D:\\GiteeProjects\\study-java\\demo-test\\src\\main\\java\\com\\zj\\demotest\\tomcatTest\\war2");
        Class cl22 = myTomcatClassloader22.loadClass("com.zj.demotest.tomcatTest.App");
        Object obj22 = cl22.newInstance();
        System.out.println("当前类加载器:"+obj22.getClass().getClassLoader());
        obj22.getClass().getMethod("print").invoke(obj22);

    }

}
登入後複製

然后postman 调一下这个接口, 结果:

当前类加载器:com.zj.demotest.tomcatTest.MyTomcatClassloader@18fbb876
this is webapp 1
当前类加载器:com.zj.demotest.tomcatTest.MyTomcatClassloader@5f7ed4a9
this is webapp 2

我们发现2个同样的类能共存在同一个JVM中,互不影响。

注意:同一个JVM内,2个相同的包名和类名的对象是可以共存的,前提是他们的类加载器不一样。所以我们要判断多个类对象是否是同一个,除了要看包名和类名相同,还得注意他们的类加载器是否一致

SpringBoot Starter

springboot自动配置的原因是因为使用了@EnableAutoConfiguration注解。

当程序包含了EnableAutoConfiguration注解,那么就会执行下面的方法,然后会加载所有spring.factories文件,将其内容封装成一个map,spring.factories其实就是一个名字特殊的properties文件。

在spring-boot应用启动时,会调用loadFactoryNames方法,其中传递的一个参数就是:org.springframework.boot.autoconfigure.EnableAutoConfiguration

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
        return configurations;
    }
登入後複製

META-INF/spring.factories会被读取到。

Java類別載入器與雙親委派機制怎麼應用

它还使用了this.getBeanClassLoader() 获取类加载器。所以我们立刻明白了文章一开始的例子,SpringBoot项目直接build项目,不重启项目,就能实现热部署效果。

以上是Java類別載入器與雙親委派機制怎麼應用的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

Java Spring 面試題 Java Spring 面試題 Aug 30, 2024 pm 04:29 PM

在本文中,我們保留了最常被問到的 Java Spring 面試問題及其詳細答案。這樣你就可以順利通過面試。

突破或從Java 8流返回? 突破或從Java 8流返回? Feb 07, 2025 pm 12:09 PM

Java 8引入了Stream API,提供了一種強大且表達力豐富的處理數據集合的方式。然而,使用Stream時,一個常見問題是:如何從forEach操作中中斷或返回? 傳統循環允許提前中斷或返回,但Stream的forEach方法並不直接支持這種方式。本文將解釋原因,並探討在Stream處理系統中實現提前終止的替代方法。 延伸閱讀: Java Stream API改進 理解Stream forEach forEach方法是一個終端操作,它對Stream中的每個元素執行一個操作。它的設計意圖是處

PHP:網絡開發的關鍵語言 PHP:網絡開發的關鍵語言 Apr 13, 2025 am 12:08 AM

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

PHP與Python:了解差異 PHP與Python:了解差異 Apr 11, 2025 am 12:15 AM

PHP和Python各有優勢,選擇應基於項目需求。 1.PHP適合web開發,語法簡單,執行效率高。 2.Python適用於數據科學和機器學習,語法簡潔,庫豐富。

Java程序查找膠囊的體積 Java程序查找膠囊的體積 Feb 07, 2025 am 11:37 AM

膠囊是一種三維幾何圖形,由一個圓柱體和兩端各一個半球體組成。膠囊的體積可以通過將圓柱體的體積和兩端半球體的體積相加來計算。本教程將討論如何使用不同的方法在Java中計算給定膠囊的體積。 膠囊體積公式 膠囊體積的公式如下: 膠囊體積 = 圓柱體體積 兩個半球體體積 其中, r: 半球體的半徑。 h: 圓柱體的高度(不包括半球體)。 例子 1 輸入 半徑 = 5 單位 高度 = 10 單位 輸出 體積 = 1570.8 立方單位 解釋 使用公式計算體積: 體積 = π × r2 × h (4

PHP與Python:核心功能 PHP與Python:核心功能 Apr 13, 2025 am 12:16 AM

PHP和Python各有優勢,適合不同場景。 1.PHP適用於web開發,提供內置web服務器和豐富函數庫。 2.Python適合數據科學和機器學習,語法簡潔且有強大標準庫。選擇時應根據項目需求決定。

PHP與其他語言:比較 PHP與其他語言:比較 Apr 13, 2025 am 12:19 AM

PHP適合web開發,特別是在快速開發和處理動態內容方面表現出色,但不擅長數據科學和企業級應用。與Python相比,PHP在web開發中更具優勢,但在數據科學領域不如Python;與Java相比,PHP在企業級應用中表現較差,但在web開發中更靈活;與JavaScript相比,PHP在後端開發中更簡潔,但在前端開發中不如JavaScript。

創造未來:零基礎的 Java 編程 創造未來:零基礎的 Java 編程 Oct 13, 2024 pm 01:32 PM

Java是熱門程式語言,適合初學者和經驗豐富的開發者學習。本教學從基礎概念出發,逐步深入解說進階主題。安裝Java開發工具包後,可透過建立簡單的「Hello,World!」程式來實踐程式設計。理解程式碼後,使用命令提示字元編譯並執行程序,控制台上將輸出「Hello,World!」。學習Java開啟了程式設計之旅,隨著掌握程度加深,可創建更複雜的應用程式。

See all articles