热部署初探与双亲委派机制
一、热部署初探
热部署就是在不重启服务的情况下,无需重新启动整个应用,就能对代码、配置等进行更新并使新的更改在服务中生效。以下代码可以打破双亲委派机制,利用类加载器的隔离实现热部署。可分为以下三步进行:
-
自定义类加载器
public class HotClassLoader extends ClassLoader { // 1. 指定父类加载器(默认系统类加载器) public HotClassLoader(ClassLoader parent) { super(parent); } // 2. 核心方法:从字节码加载类 public Class<?> loadByte(byte[] classByte) { return defineClass(null, classByte, 0, classByte.length); } }
-
加载类定义
public class HotDemo { public void printVersion() { System.out.println("【原始版本】9.0"); } public static void main(String[] args) throws Exception { } }
-
测试类
public class HotLoadTest { public static void main(String[] args) throws Exception { // 注意: 这是一个循环。 while (true) { // 1. 读取更新后的class文件 byte[] bytes = Files.readAllBytes(Paths.get("E:\\IDEAProjects\\JavaBasicKnowladge\\out\\production\\JavaBasicKnowladge\\orverLoad\\HotDemo.class")); // 2. 用自定义加载器加载 HotClassLoader loader = new HotClassLoader(HotLoadTest.class.getClassLoader()); Class<?> clazz = loader.loadByte(bytes); // 3. 反射调用方法 Object obj = clazz.getDeclaredConstructor().newInstance(); clazz.getMethod("printVersion").invoke(obj); Thread.sleep(3000); // 3秒后重新加载 } } }
热部署机制:
- 打破类加载缓存:通常来说,类加载器会对已加载的类进行缓存,减少重复创建。上面程序每次新建HotClassLoader实例,利用不同类加载器的独立命名空间,即使类名相同。JVM也会将其视为不同的类,从而绕过缓存机制。(类加载器的命名空间(Namespace) 是JVM用于隔离不同类加载器加载的类的核心机制。JVM判断类的唯一性,通过类的全限定名与加载该类的加载器实例判断。)
- 动态加载字节码:loadByte()方法直接调用defineClass(),这一步绕过了传统的类加载流程,将外部传入的最新字节码动态转换为Class对象。这使得修改后的类无需重启即可被加载。
- 隔离父类加载器:显式指定父加载器,并确保目标类未被父加载器加载过,避免双亲委派机制导致无法重新加载。
二、打破双亲委派机制
-
打破双亲委派机制类加载器
public class BreakDelegateLoader extends ClassLoader { // 必须重写loadClass而非findClass @Override public Class<?> loadClass(String name) throws ClassNotFoundException { // 1. 优先检查是否已加载 Class<?> c = findLoadedClass(name); if (c != null) { return c; } // 2. 针对特定包名打破委派 if (name.startsWith("com.example.breaking")) { return findClass(name); } // 3. 其他类仍走双亲委派 return super.loadClass(name); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadClassData(name); return defineClass(name, data, 0, data.length); } catch (IOException e) { throw new ClassNotFoundException(name); } } private byte[] loadClassData(String className) throws IOException { String path = className.replace('.', '/') + ".class"; try (InputStream ins = getClass().getClassLoader().getResourceAsStream(path)) { // Java 8 兼容写法 ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] data = new byte[1024]; int bytesRead; while ((bytesRead = ins.read(data, 0, data.length)) != -1) { buffer.write(data, 0, bytesRead); } return buffer.toByteArray(); } } }
-
加载类
public class TestClass { static { System.out.println("【类加载】初始化完成,加载器:" + TestClass.class.getClassLoader()); } // 添加构造方法调用 public TestClass() { System.out.println("实例已创建"); } }
-
测试类
public class BB { public static void main(String[] args) throws Exception { // 使用自定义加载器优先加载 BreakDelegateLoader loader = new BreakDelegateLoader(); // 必须通过自定义加载器触发首次加载 Class<?> c1 = loader.loadClass("com.example.breaking.TestClass"); c1.newInstance(); // 触发初始化 // 再用系统加载器加载 Class<?> c2 = Class.forName("com.example.breaking.TestClass"); c2.newInstance(); System.out.println("是否为同一个类: " + (c1 == c2)); // 应为false } }
示意图:
机制 | 优势 | 典型场景 |
---|---|---|
双亲委派 | 安全性、类唯一性、避免冲突 | 常规Java应用、核心类库加载 |
打破双亲委派 | 灵活性、隔离性、动态性 | Web容器、热部署、SPI、插件化架构 |
核心权衡:在安全稳定与灵活扩展之间取舍。双亲委派是默认的“安全模式”,而打破它是为了满足特定场景下的高级需求。理解两者的优劣,才能合理设计类加载策略。