热部署系统实现

2023-11-11

热部署

是指在不关闭或重启服务的情况下,更新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的博客-CSDN博客_java中的热部署

热部署_Can96的博客-CSDN博客_热部署

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

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

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

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

热部署系统实现 的相关文章

随机推荐

  • 认识 MySQL

    文章目录 1 前言 2 数据库 3 MySQL 1 前言 在当今信息时代 数据被认为是最宝贵的资源之一 因为它可以帮助我们洞察趋势 做出决策 构建智能系统 并推动创新 而数据库技术的崛起 尤其是MySQL数据库 为我们提供了强大的工具来存储
  • vue中实现删除校验-iview的气泡提示

    前言 很多时候我们担心删除会出现误删的情况 这样就需要对删除进行二次校验 确定是否继续删除 效果图 实现代码
  • Type Incompatible operand types String and int

    今天eclipse包了一个错误 意思就是Description Resource Path Location Type Incompatible operand types String an 但是昨天还没有错误为什么那 最后找了好久发现不
  • 区块链之PBFT算法

    在公有链中用的最多的是pow算法和pos算法 这些算法都是参与者的利益直接相关 通过利益来制约节点诚实的工作 解决分布式系统中的拜占庭问题 拜占庭容错算法是一种状态机副本复制算法 通过节点间的多轮消息传递 网络内的所有诚实节点就可以达成一致
  • jQuery实现省市二级联动

    主要实现流程 步骤分析 1 设置加载页面函数事件 2 在里面获取select的id并且设置下拉事件并且绑定函数 3 定义2维数组存放相应的城市 4 遍历2维数组省份 并且使用if判断 点击时的this value值 如果值与省份 二位数组下
  • 深入理解 == 与 equals 区别

    深入理解 与 equals 区别 这是一个老生常谈的问题了 也是在面试过程中常见的问题之一 网上所提供的常用回答是 equals比较值 比较值和引用 对java源码有了一定了解了之后回头再思考这个问题并不是那么的简单单一 java中的二元运
  • springboot的配置注入

    文章目录 第一种 使用 Value 第二种 使用 ConfigurationProperties springboot配置注入 数据绑定 有两种方式 第一种 使用 Value 首先 在application yml中定义自己的数据 appl
  • 蓝桥杯基础试题汇总(Python)看这一篇就够了

    目录 蓝桥杯习题汇总 1 试题 基础练习 A B问题 2 数列问题 3 试题 基础练习 十六进制转八进制 4 试题 基础练习 十六进制转十进制 5 试题 基础练习 十进制转十六进制 6 试题 基础练习 序列求和 7 试题 基础练习 圆的面积
  • 浅谈数据同步实现rsync+inotify

    浅谈数据同步实现rsync inotify 数据的实时同步介绍 数据的实时同步实现 inotify inotify的介绍 实现inotify软件 inotify rsync使用方式 实现inotify rsync 1 rsync基本概述 2
  • ubuntu安装deb包

    ubuntu安装deb包 安装deb包 sudo dpkg i 包名 安装deb包后 可能会出现依赖关系而不能正常安装软件 这个时候先更新下源然后解决依赖关系后重装即可 sudo apt get update 更新 sudo apt get
  • 教程网站 汇总:Linux 、 C /C++ 、HTML、CSS

    C 语言教程 菜鸟教程 https www runoob com cprogramming c tutorial html C 教程 菜鸟教程 https www runoob com cplusplus cpp tutorial html
  • 安装apache后无法访问localhost但可以访问127.0.0.1的解决方法

    localhost与127 0 0 1的概念和工作原理之不同 概念 localhost 也叫local 正确的解释是 本地服务器 127 0 0 1 在windows等系统的正确解释是 本机地址 本机服务器 工作原理 localhot 是不
  • VS2019的常见错误和调试功能

    目录 一 VS2019常见问题 1 scanf问题 2 如何在当前页面下再创建新项目和创建多项目后无法运行当前项目的问题 二 VS2019的调试功能 不打断点 三 VS2019的调试功能 打断点 四 总结 一 VS2019常见问题 1 sc
  • 爬虫实战爬取豆瓣电影Top250榜单电影

    爬虫实战爬取豆瓣电影Top250榜单电影 实战内容 直接上代码 重要地方有注释 from bs4 import BeautifulSoup import re import urllib request urllib error impor
  • Postman + Pre-resuestScript:预请求脚本发送GET请求

    通过预执行脚本 Pre request Script 发送GET请求 一 效果演示 二 控制台 Console 打印响应结果 代码注释详解 pm sendRequest 是发送一个请求 function 中的 err 表示请求返回的错误信息
  • Node的Buffer对象和fs模块

    一 Node的模块化管理 1 模块化 node应用程序由模块组成 遵循的是CommonJS模块规范 使用模块管理的好处是隔离模块的作用域 避免出现命名冲突 2 什么是CommonJS 是一套代码的规范 构建一个在浏览器之外的JavaScri
  • 报错:ImportError: rocketmq dynamic library not found解决方法

    目录 一 ImportError rocketmq dynamic library not found 二 OSError librocketmq so cannot open shared object file No such file
  • Shell脚本的通配符和特殊符号

    通配符 符号 意义 0到无穷个任意字符 一个任意字符 如 abcd 表示a b c d中任意一个 在编码顺序内的所有字符 如 0 9 表示0到9间的数字 反向选择 如 abc 表示非a b c的其它字符 特殊符号 符号 内容 管线 分割两个
  • 项目质量管理__七种基本质量工具__老七工具和新七工具

    七种基本质量工具 用于在PDCA Plan Do Check Action 循环的框架内解决与质量相关的问题 1 老七工具 包括因果图 流程图 核查表 帕累托图 直方图 控制图和散点图 因果图 又称鱼骨图或石川馨图 流程图 也称过程图 核查
  • 热部署系统实现

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