热部署系统实现

本文详细介绍了Java热部署的原理,包括类加载器的工作机制,如BootStrap、ExtClassLoader和AppClassLoader。通过自定义类加载器和重写findClass方法可以实现热部署,避免服务中断。此外,还展示了如何通过插件类加载器实现类隔离加载,以解决不同版本中间件依赖冲突的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

热部署

是指在不关闭或重启服务的情况下,更新Java类文件或配置文件,实现修改内容生效;通过热部署,可提高开发效率,节省程序打包重启的时间,同时,可实现生产环境中需要不停机或重启的服务的升级。在大厂的核心中台,订单服务,商品服务往往有几千台服务器,服务的升级发布往往要花费大量时间。

1.热部署实现原理

对于Java应用程序,热部署就是程序运行时实现Java类文件更新。要实现程序在运行中进行程序更新,就需要让java虚拟机在检测到Java类文件发生变化时,把原来的类文件卸载,并重新加载新的类文件。总的来说,热部署的本质是让jvm重新加载新的class文件。程序运行时,类加载器只会加载一次Java类文件,切不能卸载,这很明显不符合热部署的需要。但是,因为类加载器是可以进行更换的,所以,我们采取的方式是自定义类加载器,在自定义的类加载器中,重写findClass方法,从而实现热部署。

热部署实现方式:

  1. 热部署前,销毁自定义的类加载器;
  2. 更新Java Class文件;
  3. 创建新的ClassLoader去加载更新后的Java Class文件。

2.热部署系统交互

ClassLoader 介绍

 热部署功能,主要使用ClassLoader 实现,下面介绍下 ClassLoader 类

BootStrap根类加载器

引导类加载器,它的实现依赖于底层操作系统,是用c编写的,不是由ClassLoader类继承的。 根加载器从由系统属性sun.boot.class.path指定的目录中加载类库。 缺省设置为没有父加载器的jre目录的lib目录的class文件。 负责加载虚拟机的核心类库,如java.lang.*。 Object类由根类加载器加载。

ExtClassLoader扩展类加载器 

ExtClassLoader扩展类加载器,用java编写,是ClassLoader的子类。负责加载 %JAVA_HOME%中lib/ext文件下的jar包和class类文件,将用户创建的jar文件放在此目录中时,扩展类加载器会自动加载。ExtClassLoader加载器是AppClassLoader的父类,当然也不是继承(extends)关系,也是类中有parent变量

AppClassLoader应用类加载器 

AppClassLoader是自定义加载器的父类,负责加载classPath下的类文件,平时引用的jar包以及我们自己写的类都是这个加载器进行加载的,同时AppClassLoader还是线程上下文加载器,如果想实现一个自定义加载器的话就继承(extends)ClassLoader来实现。

类加载的流程

向上委派

AppClassLoader是加载我们自己编写的class类的,当他遇到一个新的class类的时候,不会直接进行加载,而是向上委派给ExtClassLoader,向上委派就是去查找ExtClassLoader是否缓存了这个class类,如果有则返回,如果没有则继续委派给BootstrapClassLoader,如果BootstrapClassLoader中缓存有则加载返回。

向下查找

开始进行向下查找了,就意味着当前class类向上委派到BootstrapClassLoader时还是没有该类的缓存,此时BootstrapClassLoader会查找加载自己路径也就是%JAVA_HOME%/lib下的jar与class类文件,如果有则加载返回,没有则继续向下查找。ExtClassLoader也是做同样的操作。查找加载ExtClassLoader对应路径的文件,如果有则加载返回,没有则继续向下到AppClassLoader查找加载,AppClassLoader是加载classPath也就是我们程序员自己编写的class类,如果AppClassLoader找不到则会抛出找不到class类异常

流程简介

向往委派是到顶层类加载器为止,向下查找是到发起的加载器为止,如果是有自定义类加载的情况,发起和截至会是这个自定义加载器。

作用

这样做的原因主要是为了安全,避免程序员编写类动态替换Java的核心类比如说String,同时也是避免了相同的class类被不同的ClassLoader重复加载

package com.example;

public class Demo {
    public static void main(String[] args) {
        ClassLoader classLoader = Demo.class.getClassLoader();

        while (classLoader != null){
            System.out.println(classLoader);
            classLoader = classLoader.getParent();
        }

        System.out.println(classLoader);
    }
}

运行结果

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@23ab930d
null

自定义类加载器

JVM中除了根加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。
ClassLoader类有如下三个关键方法:

loadClass(String name, boolean resolve):该方法为CLassLoader的入口点,根据指定的二进制名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。

findClass(String name):根据二进制名称来查找类。

defineClass(String name, byte[] b, int off, int len):将指定类的字节码文件读入字节数组byte[] b内,并把它转化为Class对象。

当我们自定义类加载器时,一般只要重写findClass方法即可。

package com.example;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;

public class MyClassLoader extends ClassLoader {

    private String dir;

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

    public MyClassLoader(String dir, ClassLoader parent) {
        super(parent);
        this.dir = dir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String classFileName = dir + "/" + name.replace(".", "/") + ".class";

        try {
            FileInputStream fis = new FileInputStream(classFileName);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int content;

            while ((content = fis.read()) != -1) {
                bos.write(content);
                bos.flush();
            }

            byte[] buffer = bos.toByteArray();
            return defineClass(name, buffer, 0, buffer.length);
        } catch (Exception ex) {
            System.out.println(ex);
        }

        return null;
    }
}
  • 我们创建一个Person类,把Person类文件放在/Users/yangyanping/Downloads 目录下

 Person 类代码

package com.example;

public class Person {
    public Person() {
        System.out.println(getClass().getClassLoader());
    }
}

 使用javac 编译 Person.java 得到Person.class 文件

javac Person.java

  编写测试类Demo

package com.example;

public class Demo {
    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("/Users/yangyanping/Downloads");
        System.out.println("myClassLoader parent = " + myClassLoader.getParent());
        Class clasz = myClassLoader.loadClass("com.example.Person");
        Object object = clasz.newInstance();

        System.out.println(object);
    }
}

 运行结果

myClassLoader parent = sun.misc.Launcher$AppClassLoader@18b4aac2
com.example.MyClassLoader@3fa77460
com.example.Person@1ed6993a
  • 在项目中创建同一个 Person 类,测试
package com.example;

public class Person {
    public Person() {
        System.out.println(getClass().getClassLoader());
    }
}

 运行Demo类,结果如下

myClassLoader parent = sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
com.example.Person@3fa77460

  从运行结果知道,使用的是AppClassLoader 类来加载Person 类,而不是我们自己定义的       MyClassLoader类来加载。这是由于上面讲到的 父类委托机制 。

  •     使用MyClassLoader 加载 Person 类.
package com.example;

public class Demo {
    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("/Users/yangyanping/Downloads", null);
        System.out.println("myClassLoader parent = " + myClassLoader.getParent());
        Class clasz = myClassLoader.loadClass("com.example.Person");
        Object object = clasz.newInstance();

        System.out.println(object);
    }
}

     运行结果:

myClassLoader parent = null
com.example.MyClassLoader@3fa77460
com.example.Person@1ed6993a

代码演示

SPI 接口定义

/**
 * 检查订单参数
 */
public interface OrderHandler {
    void checkParam(Object[] args);
}

业务方实现

/**
 * 业务方实现
 */
public class OrderHandlerImpl implements OrderHandler {
    @Override
    public void checkParam(Object[] args) {
        System.out.println("checkParam start .....");
    }
}

 单元测试

import com.example.handler.OrderHandler;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        // 业务方实现的SPI jar包
        String path = "/Users/yangyanping/Downloads/code/ql-er/demo/order-handler-test/target/order-handler-test-0.0.1-SNAPSHOT.jar";


        File moduleFile = new File(path);
        URL moduleURL = moduleFile.toURI().toURL();

        URLClassLoader classLoader = new URLClassLoader(new URL[]{moduleURL});

        Map<Class<?>, Object> objectMap = getClass(path, classLoader);

        CheckWrapper checkWrapper = new CheckWrapper();
        checkWrapper.setObjectMap(objectMap);

        OrderServiceImpl impl = new OrderServiceImpl();
        impl.setCheckWrapper(checkWrapper);

        impl.subimit();
    }

    public static Map<Class<?>, Object> getClass(String path,
                                                  ClassLoader loader) throws Exception {
        Map<Class<?>, Object> objectMap = new HashMap<>();
        List<String> classes = getAllClasses(path);
        for (String className : classes) {
            Class<?> claz = loader.loadClass(className);

            Object obj = claz.newInstance();
            if (obj instanceof OrderHandler) {
                objectMap.put(OrderHandler.class, obj);
            }

        }
        return objectMap;
    }

    private static List<String> getAllClasses(String module) throws Exception {
        List<String> result = new ArrayList<String>();
        @SuppressWarnings("resource")
        JarFile jar = new JarFile(new File(module));
        Enumeration<JarEntry> entries = jar.entries();
        while (entries.hasMoreElements()) {
            JarEntry entry = entries.nextElement();
            String className = getClassName(entry);
            if (className != null && className.length() > 0) {
                result.add(className);
            }
        }
        return result;
    }

    private static String getClassName(JarEntry jarEntry) {
        String jarName = jarEntry.getName();
        if (!jarName.endsWith(".class")) {
            return null;
        }
        if (jarName.charAt(0) == '/') {
            jarName = jarName.substring(1);
        }
        jarName = jarName.replace("/", ".");
        return jarName.substring(0, jarName.length() - 6);
    }
}

实现插件类隔离加载 

为什么需要类隔离加载

 项目开发过程中,需要依赖不同版本的中间件依赖包,以适配不同的中间件服务端

如果这些中间件依赖包版本之间不能向下兼容,高版本依赖无法连接低版本的服务端,相反低版本依赖也无法连接高版本服务端

项目中也不能同时引入两个版本的中间件依赖,势必会导致类加载冲突,程序无法正常执行

解决方案

1、插件包开发:将不同版本的依赖做成不同的插件包,而不是直接在项目中进行依赖引入,这样不同的依赖版本就是不同的插件包了

2、插件包打包:将插件包打包时合入所有的三方库依赖

3、插件包加载:主程序根据中间件版本加载不同的插件包即可执行业务逻辑即可

插件包开发

此处以commons-lang3依赖举例

新建Maven项目,开发插件包,引入中间件依赖,插件包里面依赖的版本是3.11

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.11</version>
</dependency>

 获取commons-lang3的StringUtils类全路径,代码如下:

public class PluginProvider {
    public void test() {
        // 获取当前的类加载器
        System.out.println("Plugin: " + this.getClass().getClassLoader());
        // 获取类全路径
        System.out.println("Plugin: " + StringUtils.class.getResource("").getPath());
    }
}

POM配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>plugin</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <mainClass>cn.example.PluginProvider</mainClass>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

PluginClassLoader

public class PluginClassLoader extends URLClassLoader {
    public PluginClassLoader(URL[] urls) {
        // 类加载器的双亲委派机制
        // 先使用父加载器加载class,加载不到时再调用findClass方法
        super(urls, null);
    }
}

测试代码

package org.example;

import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;

import java.net.URL;

public class PluginTester {
    public static void main(String[] args) {

        // 打印当前类加载器
        System.out.println("Boot: " + PluginTester.class.getClassLoader());
        // 获取StringUtils的类全路径
        System.out.println("Boot: " + StringUtils.class.getResource("").getPath());
        // 模拟调用插件包
        testPlugin();
    }

    public static void testPlugin() {
        try {
            // 加载插件包
            ClassPathResource resource = new ClassPathResource("plugin/plugin-1.0-SNAPSHOT-jar-with-dependencies.jar");
            // 打印插件包路径
            System.out.println(resource.getURL().getPath());

//            URLClassLoader classLoader = new URLClassLoader(new URL[]{resource.getURL()});
            // 初始化自己的ClassLoader
            PluginClassLoader pluginClassLoader = new PluginClassLoader(new URL[]{resource.getURL()});
            // 这里需要临时更改当前线程的 ContextClassLoader
            // 避免中间件代码中存在Thread.currentThread().getContextClassLoader()获取类加载器
            // 因为它们会获取当前线程的 ClassLoader 来加载 class,而当前线程的ClassLoader极可能是App ClassLoader而非自定义的ClassLoader, 也许是为了安全起见,但是这会导致它可能加载到启动项目中的class(如果有),或者发生其它的异常,所以我们在执行时需要临时的将当前线程的ClassLoader设置为自定义的ClassLoader,以实现绝对的隔离执行
            ClassLoader originClassLoader = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(pluginClassLoader);

            // 加载插件包中的类
            Class<?> clazz = pluginClassLoader.loadClass("cn.example.PluginProvider");
            // 反射执行
            clazz.getDeclaredMethod("test", null).invoke(clazz.newInstance(), null);

            Thread.currentThread().setContextClassLoader(originClassLoader);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

参考:

Java服务器热部署的实现原理_chenjie19891104的博客-优快云博客_java中的热部署

热部署_Can96的博客-优快云博客_热部署

Java类加载器 — classloader 的原理及应用 - 掘金

【架构视角】一篇文章带你彻底吃透Spring - 知乎

https://betheme.net/houduan/6474.html?action=onClick

你绝对不知道的类加载器骚操作-腾讯云开发者社区-腾讯云

Arthas源码分析-arthas与应用的隔离 - 知乎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值