热部署
是指在不关闭或重启服务的情况下,更新Java类文件或配置文件,实现修改内容生效;通过热部署,可提高开发效率,节省程序打包重启的时间,同时,可实现生产环境中需要不停机或重启的服务的升级。在大厂的核心中台,订单服务,商品服务往往有几千台服务器,服务的升级发布往往要花费大量时间。
1.热部署实现原理
对于Java应用程序,热部署就是程序运行时实现Java类文件更新。要实现程序在运行中进行程序更新,就需要让java虚拟机在检测到Java类文件发生变化时,把原来的类文件卸载,并重新加载新的类文件。总的来说,热部署的本质是让jvm重新加载新的class文件。程序运行时,类加载器只会加载一次Java类文件,切不能卸载,这很明显不符合热部署的需要。但是,因为类加载器是可以进行更换的,所以,我们采取的方式是自定义类加载器,在自定义的类加载器中,重写findClass方法,从而实现热部署。
热部署实现方式:
- 热部署前,销毁自定义的类加载器;
- 更新Java Class文件;
- 创建新的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
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与应用的隔离 - 知乎