⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 风象南 「风象南」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

每个程序员都有过这样的经历——凌晨三点被电话惊醒,生产环境出现紧急bug,而修复发布又需要漫长的流程。

今天我们来介绍如何用SpringBoot 打造一个热补丁加载器,让你在紧急时刻也能从容应对。

背景:为什么需要热补丁?

想象一下这个场景:周五晚上8点,你刚准备下班,突然收到监控报警——生产环境某个关键接口出现空指针异常,影响了大量用户。这时候你面临几个选择:

  • 传统发布流程:修改代码 → 测试 → 打包 → 发布,至少需要1-2小时
  • 回滚到上个版本:可能会丢失其他新功能
  • 热补丁修复:几分钟内修复问题,不影响服务运行

显然,第三种方案可以解燃眉之急。

设计思路

我们的热补丁加载器基于以下几个核心思想:

  • 动态类加载:利用Java的ClassLoader机制动态加载补丁类
  • 多层次替换:支持Spring Bean、普通Java类、静态方法等多种替换方式
  • 字节码增强:通过Java Agent和Instrumentation API实现任意类的运行时替换
  • 版本管理:每个补丁都有版本号,支持回滚
  • 安全可控:只允许特定路径的补丁文件,防止安全风险

核心实现

1. 项目结构

首先,我们来看看完整的项目结构:

springboot-hot-patch/
├── src/main/java/com/example/hotpatch/
│ ├── agent/ # Java Agent相关
│ │ └── HotPatchAgent.java
│ ├── annotation/ # 注解定义
│ │ ├── HotPatch.java
│ │ └── PatchType.java
│ ├── config/ # 配置类
│ │ ├── HotPatchConfig.java
│ │ └── HotPatchProperties.java
│ ├── controller/ # 控制器
│ │ ├── HotPatchController.java
│ │ └── TestController.java
│ ├── core/ # 核心热补丁加载器
│ │ └── HotPatchLoader.java
│ ├── example/ # 示例代码
│ │ ├── UserService.java
│ │ ├── StringUtils.java
│ │ └── MathHelper.java
│ ├── instrumentation/ # 字节码操作
│ │ └── InstrumentationHolder.java
│ ├── model/ # 数据模型
│ │ ├── PatchInfo.java
│ │ └── PatchResult.java
│ └── patches/ # 补丁示例
│ ├── UserServicePatch.java
│ ├── StringUtilsPatch.java
│ └── MathHelperDividePatch.java
├── src/main/resources/
│ ├── static/ # Web模板
│ │ ├── index.html
│ └── application.properties
├── patches/ # 补丁文件目录
└── pom.xml

2. Maven配置

<?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>com.example</groupId>
<artifactId>springboot-hot-patch</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Spring Boot Hot Patch Loader</name>
<description>A Spring Boot 3 based hot patch loader for runtime class replacement</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>3.2.0</spring-boot.version>
<asm.version>9.5</asm.version>
<micrometer.version>1.12.0</micrometer.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- ASM for bytecode manipulation -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm.version}</version>
</dependency>

<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>${asm.version}</version>
</dependency>

<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-util</artifactId>
<version>${asm.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

3. 注解和枚举定义

/**
* 补丁类型枚举
*/
public enum PatchType {
/**
* Spring Bean 替换
*/
SPRING_BEAN,

/**
* 普通Java类替换(整个类)
*/
JAVA_CLASS,

/**
* 静态方法替换
*/
STATIC_METHOD,

/**
* 实例方法替换
*/
INSTANCE_METHOD
}

/**
* 增强的热补丁注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HotPatch {
/**
* 补丁类型
*/
PatchType type() default PatchType.SPRING_BEAN;

/**
* 原始Bean名称(当type=SPRING_BEAN时使用)
*/
String originalBean() default "";

/**
* 原始类的全限定名(当type=JAVA_CLASS或STATIC_METHOD时使用)
*/
String originalClass() default "";

/**
* 要替换的方法名(当type=STATIC_METHOD或INSTANCE_METHOD时使用)
*/
String methodName() default "";

/**
* 方法签名(用于方法重载区分)
*/
String methodSignature() default "";

/**
* 补丁版本
*/
String version() default "1.0";

/**
* 补丁描述
*/
String description() default "";

/**
* 是否启用安全验证
*/
boolean securityCheck() default true;
}

4. Java Agent支持

/**
* Instrumentation持有器 - 用于获取JVM的Instrumentation实例
*/
public class InstrumentationHolder {
private static volatile Instrumentation instrumentation;

public static void setInstrumentation(Instrumentation inst) {
instrumentation = inst;
}

public static Instrumentation getInstrumentation() {
return instrumentation;
}

public static boolean isAvailable() {
return instrumentation != null;
}
}

/**
* Java Agent入口类
*/
public class HotPatchAgent {

public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("HotPatch Agent 启动成功");
InstrumentationHolder.setInstrumentation(inst);
}

public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("HotPatch Agent 动态加载成功");
InstrumentationHolder.setInstrumentation(inst);
}
}

5. 配置属性类

/**
* 热补丁配置属性
*/
@ConfigurationProperties(prefix = "hotpatch")
@Component
@Data
public class HotPatchProperties {
/**
* 是否启用热补丁功能
*/
private boolean enabled = false;

/**
* 补丁文件存放路径
*/
private String path = "./patches";

/**
* 允许的补丁文件最大大小(字节)
*/
private long maxFileSize = 10 * 1024 * 1024;

/**
* 是否启用补丁签名验证
*/
private boolean signatureVerification = false;

/**
* 允许执行热补丁操作的角色列表
*/
private List<String> allowedRoles = List.of("ADMIN", "DEVELOPER");
}

6. 数据模型

/**
* 补丁信息类
*/
@Data
@AllArgsConstructor
public class PatchInfo {
private String name;
private String version;
private Class<?> patchClass;
private PatchType patchType;
private long loadTime;
private String originalTarget; // 原始目标(Bean名称或类名)

public PatchInfo(String name, String version, Class<?> patchClass,
PatchType patchType, long loadTime) {
this.name = name;
this.version = version;
this.patchClass = patchClass;
this.patchType = patchType;
this.loadTime = loadTime;
this.originalTarget = extractOriginalTarget(patchClass);
}

private String extractOriginalTarget(Class<?> patchClass) {
HotPatch annotation = patchClass.getAnnotation(HotPatch.class);
if (annotation != null) {
switch (annotation.type()) {
case SPRING_BEAN:
return annotation.originalBean();
case JAVA_CLASS:
return annotation.originalClass();
case STATIC_METHOD:
case INSTANCE_METHOD:
return annotation.originalClass() + "." + annotation.methodName();
default:
return "Unknown";
}
}
return "Unknown";
}
}

/**
* 补丁操作结果类
*/
@Data
@AllArgsConstructor
public class PatchResult {
private boolean success;
private String message;
private Object data;

public static PatchResult success(String message) {
return new PatchResult(true, message, null);
}

public static PatchResult success(String message, Object data) {
return new PatchResult(true, message, data);
}

public static PatchResult failed(String message) {
return new PatchResult(false, message, null);
}
}

7. 核心热补丁加载器

考虑篇幅,以下为部分关键代码

/**
* 增强版补丁加载器核心类
*/
@Component
@Slf4j
public class HotPatchLoader {

private final ConfigurableApplicationContext applicationContext;
private final HotPatchProperties properties;
private final Map<String, PatchInfo> loadedPatches = new ConcurrentHashMap<>();
private final Instrumentation instrumentation;

public HotPatchLoader(ConfigurableApplicationContext applicationContext,
HotPatchProperties properties) {
this.applicationContext = applicationContext;
this.properties = properties;
// 获取 Instrumentation 实例
this.instrumentation = InstrumentationHolder.getInstrumentation();
}

/**
* 加载热补丁 - 支持任意类替换
* @param patchName 补丁名称
* @param version 版本号
*/
public PatchResult loadPatch(String patchName, String version) {
if (!properties.isEnabled()) {
return PatchResult.failed("热补丁功能未启用");
}

try {
// 1. 验证补丁文件
File patchFile = validatePatchFile(patchName, version);

// 2. 创建专用的类加载器
URLClassLoader patchClassLoader = createPatchClassLoader(patchFile);

// 3. 加载补丁类
Class<?> patchClass = loadPatchClass(patchClassLoader, patchName);

// 4. 获取补丁注解信息
HotPatch patchAnnotation = patchClass.getAnnotation(HotPatch.class);
if (patchAnnotation == null) {
return PatchResult.failed("补丁类缺少 @HotPatch 注解");
}

// 5. 根据补丁类型选择替换策略
PatchType patchType = patchAnnotation.type();
switch (patchType) {
case SPRING_BEAN:
replaceSpringBean(patchClass, patchAnnotation);
break;
case JAVA_CLASS:
replaceJavaClass(patchClass, patchAnnotation);
break;
case STATIC_METHOD:
replaceStaticMethod(patchClass, patchAnnotation);
break;
case INSTANCE_METHOD:
return PatchResult.failed("实例方法替换暂未实现,请使用动态代理方式");
default:
return PatchResult.failed("不支持的补丁类型: " + patchType);
}

// 6. 记录补丁信息
PatchInfo patchInfo = new PatchInfo(patchName, version,
patchClass, patchType, System.currentTimeMillis());
loadedPatches.put(patchName, patchInfo);

log.info("热补丁 {}:{} ({}) 加载成功", patchName, version, patchType);
return PatchResult.success("补丁加载成功");

} catch (Exception e) {
log.error("热补丁加载失败: {}", e.getMessage(), e);
return PatchResult.failed("补丁加载失败: " + e.getMessage());
}
}
}

8. REST API控制器

/**
* 热补丁管理控制器
*/
@RestController
@RequestMapping("/api/hotpatch")
@Slf4j
public class HotPatchController {

private final HotPatchLoader patchLoader;

public HotPatchController(HotPatchLoader patchLoader) {
this.patchLoader = patchLoader;
}

@PostMapping("/load")
public ResponseEntity<PatchResult> loadPatch(
@RequestParam String patchName,
@RequestParam String version) {

log.info("请求加载热补丁: {}:{}", patchName, version);
PatchResult result = patchLoader.loadPatch(patchName, version);

return ResponseEntity.ok(result);
}

@GetMapping("/list")
public ResponseEntity<List<PatchInfo>> listPatches() {
List<PatchInfo> patches = patchLoader.getLoadedPatches();
return ResponseEntity.ok(patches);
}

@PostMapping("/rollback")
public ResponseEntity<PatchResult> rollbackPatch(
@RequestParam String patchName) {

log.info("请求回滚补丁: {}", patchName);
PatchResult result = patchLoader.rollbackPatch(patchName);
return ResponseEntity.ok(result);
}

@GetMapping("/status")
public ResponseEntity<String> getStatus() {
return ResponseEntity.ok("Hot Patch Loader is running");
}
}

9. Web管理界面

我们提供了一个美观实用的Web管理界面:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>热补丁管理器</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body { font-family: 'Inter', sans-serif; }
.gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.card-shadow { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); }
.hover-lift { transition: all 0.3s ease; }
.hover-lift:hover { transform: translateY(-2px); box-shadow: 0 8px 25px -8px rgba(0, 0, 0, 0.2); }
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- Header -->
<header class="gradient-bg text-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="text-center">
<h1 class="text-4xl font-bold mb-2">🔥 热补丁管理器</h1>
<p class="text-xl opacity-90">Spring Boot 线上紧急修复控制台</p>
</div>
</div>
</header>

<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white rounded-xl card-shadow p-6 text-center hover-lift">
<div class="inline-flex items-center justify-center w-12 h-12 bg-blue-100 rounded-lg mb-4">
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14-7l-3 3m0-3L13 7"/>
</svg>
</div>
<div class="text-3xl font-bold text-gray-900" id="totalPatches">0</div>
<div class="text-sm text-gray-500 mt-1">已加载补丁</div>
</div>

<div class="bg-white rounded-xl card-shadow p-6 text-center hover-lift">
<div class="inline-flex items-center justify-center w-12 h-12 bg-green-100 rounded-lg mb-4">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="text-3xl font-bold text-gray-900" id="successCount">0</div>
<div class="text-sm text-gray-500 mt-1">成功次数</div>
</div>

<div class="bg-white rounded-xl card-shadow p-6 text-center hover-lift">
<div class="inline-flex items-center justify-center w-12 h-12 bg-purple-100 rounded-lg mb-4">
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="text-xl font-bold text-gray-900" id="lastLoadTime">--</div>
<div class="text-sm text-gray-500 mt-1">最后加载</div>
</div>
</div>

<!-- 消息显示区域 -->
<div id="message" class="mb-6"></div>

<!-- 加载补丁区域 -->
<div class="bg-white rounded-xl card-shadow p-6 mb-8">
<div class="flex items-center mb-6">
<div class="inline-flex items-center justify-center w-10 h-10 bg-blue-100 rounded-lg mr-3">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
</svg>
</div>
<h2 class="text-2xl font-bold text-gray-900">📦 加载补丁</h2>
</div>

<div class="space-y-4">
<!-- 补丁选择下拉框 -->
<div>
<label for="patchSelector" class="block text-sm font-medium text-gray-700 mb-2">选择补丁</label>
<div class="relative">
<select id="patchSelector" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white">
<option value="">正在扫描补丁目录...</option>
</select>
<div class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
</div>
</div>

<!-- 或者手动输入 -->
<div class="border-t pt-4">
<p class="text-sm text-gray-600 mb-3">或者手动输入补丁信息:</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="patchName" class="block text-sm font-medium text-gray-700 mb-2">补丁名称</label>
<input type="text" id="patchName" placeholder="如: UserService"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200">
</div>
<div>
<label for="patchVersion" class="block text-sm font-medium text-gray-700 mb-2">版本号</label>
<input type="text" id="patchVersion" placeholder="如: 1.0.1"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200">
</div>
</div>
</div>

<!-- 加载按钮 -->
<div class="flex justify-end pt-4">
<button id="loadBtn" onclick="loadPatch()"
class="inline-flex items-center px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
加载补丁
</button>
</div>
</div>
</div>

<!-- 补丁列表区域 -->
<div class="bg-white rounded-xl card-shadow p-6">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center">
<div class="inline-flex items-center justify-center w-10 h-10 bg-green-100 rounded-lg mr-3">
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
</div>
<h2 class="text-2xl font-bold text-gray-900">📋 已加载补丁</h2>
</div>
<button onclick="refreshPatches()"
class="inline-flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors duration-200">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
刷新列表
</button>
</div>

<!-- 加载状态 -->
<div id="loading" class="hidden text-center py-12">
<div class="inline-flex items-center">
<svg class="animate-spin -ml-1 mr-3 h-8 w-8 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span class="text-lg text-gray-600">正在加载补丁列表...</span>
</div>
</div>

<!-- 补丁列表 -->
<div id="patchList" class="space-y-4">
<!-- 补丁项目将在这里显示 -->
</div>
</div>
</main>

<script>
// API 基础路径
const API_BASE = '/api/hotpatch';

// 统计数据
let stats = {
total: 0,
success: 0,
lastLoad: null
};

// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
scanPatchesDirectory();
refreshPatches();
updateStats();
});

// 扫描补丁目录
async function scanPatchesDirectory() {
const selector = document.getElementById('patchSelector');

try {
// 这里模拟扫描补丁目录的API调用
// 实际应该调用后端API来获取patches目录下的所有jar文件
const response = await fetch(`${API_BASE}/scan-patches`);

if (response.ok) {
const patches = await response.json();

selector.innerHTML = '<option value="">请选择一个补丁</option>';
patches.forEach(patch => {
const option = document.createElement('option');
option.value = JSON.stringify({name: patch.name, version: patch.version});
option.textContent = `${patch.name} (${patch.version})`;
selector.appendChild(option);
});
} else {
// 如果API不存在,使用模拟数据
selector.innerHTML = `
<option value="">请选择一个补丁</option>
<option value='{"name":"StringUtils","version":"1.0.2"}'>StringUtils (1.0.2)</option>
<option value='{"name":"UserService","version":"1.0.1"}'>UserService (1.0.1)</option>
`;
}
} catch (error) {
// 使用模拟数据
selector.innerHTML = `
<option value="">请选择一个补丁</option>
<option value='{"name":"StringUtils","version":"1.0.2"}'>StringUtils (1.0.2)</option>
<option value='{"name":"UserService","version":"1.0.1"}'>UserService (1.0.1)</option>
`;
}
}

// 下拉框选择事件
document.getElementById('patchSelector').addEventListener('change', function() {
const selectedValue = this.value;
if (selectedValue) {
const patch = JSON.parse(selectedValue);
document.getElementById('patchName').value = patch.name;
document.getElementById('patchVersion').value = patch.version;
} else {
document.getElementById('patchName').value = '';
document.getElementById('patchVersion').value = '';
}
});

// 显示消息
function showMessage(text, type = 'success') {
const messageDiv = document.getElementById('message');
let bgColor = type === 'success' ? 'bg-green-50 border-green-200 text-green-800' : 'bg-red-50 border-red-200 text-red-800';
let icon = type === 'success' ?
'<svg class="w-5 h-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>' :
'<svg class="w-5 h-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>';

messageDiv.innerHTML = `
<div class="border rounded-lg p-4 ${bgColor} mb-4">
<div class="flex">
<div class="flex-shrink-0">
${icon}
</div>
<div class="ml-3">
<p class="text-sm font-medium">${text}</p>
</div>
</div>
</div>
`;

// 3秒后自动隐藏
setTimeout(() => {
messageDiv.innerHTML = '';
}, 3000);
}

// 加载补丁
async function loadPatch() {
const patchName = document.getElementById('patchName').value.trim();
const version = document.getElementById('patchVersion').value.trim();

if (!patchName || !version) {
showMessage('请选择补丁或手动输入补丁名称和版本号', 'error');
return;
}

const loadBtn = document.getElementById('loadBtn');
loadBtn.disabled = true;
loadBtn.innerHTML = `
<svg class="animate-spin -ml-1 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
加载中...
`;

try {
const response = await fetch(`${API_BASE}/load`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `patchName=${encodeURIComponent(patchName)}&version=${encodeURIComponent(version)}`
});

const result = await response.json();

if (result.success) {
showMessage(`✅ ${result.message}`, 'success');
stats.success++;
stats.lastLoad = new Date().toLocaleTimeString();

// 清空输入框和选择器
document.getElementById('patchName').value = '';
document.getElementById('patchVersion').value = '';
document.getElementById('patchSelector').value = '';

// 刷新列表
refreshPatches();
updateStats();
} else {
showMessage(`❌ ${result.message}`, 'error');
}
} catch (error) {
showMessage(`网络错误: ${error.message}`, 'error');
} finally {
loadBtn.disabled = false;
loadBtn.innerHTML = `
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
加载补丁
`;
}
}

// 刷新补丁列表
async function refreshPatches() {
const loading = document.getElementById('loading');
const patchList = document.getElementById('patchList');

loading.classList.remove('hidden');

try {
const response = await fetch(`${API_BASE}/list`);
const patches = await response.json();

stats.total = patches.length;
updateStats();

if (patches.length === 0) {
patchList.innerHTML = `
<div class="text-center py-16">
<div class="inline-flex items-center justify-center w-16 h-16 bg-gray-100 rounded-full mb-4">
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2M4 13h2m13-8V4a2 2 0 00-2-2H9a2 2 0 00-2 2v1M8 7V4a2 2 0 012-2h4a2 2 0 012 2v3"/>
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">暂无已加载的补丁</h3>
<p class="text-gray-500">在上方选择补丁并点击"加载补丁"开始使用</p>
</div>
`;
} else {
patchList.innerHTML = patches.map(patch => `
<div class="bg-gradient-to-r from-blue-50 to-purple-50 border border-blue-200 rounded-lg p-6 hover-lift">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center mb-3">
<div class="inline-flex items-center justify-center w-10 h-10 bg-blue-100 rounded-lg mr-3">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900">📦 ${patch.name}</h3>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
v${patch.version}
</span>
</div>
</div>

<div class="space-y-2 text-sm text-gray-600">
<div class="flex items-center">
<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
加载时间: ${new Date(patch.loadTime).toLocaleString()}
</div>
<div class="flex items-center">
<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
</svg>
类型: ${patch.patchType}
</div>
<div class="flex items-center">
<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
目标: ${patch.originalTarget}
</div>
</div>
</div>

<button onclick="rollbackPatch('${patch.name}')"
class="inline-flex items-center px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-lg transition-colors duration-200">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/>
</svg>
回滚补丁
</button>
</div>
</div>
`).join('');
}
} catch (error) {
patchList.innerHTML = `
<div class="text-center py-16">
<div class="inline-flex items-center justify-center w-16 h-16 bg-red-100 rounded-full mb-4">
<svg class="w-8 h-8 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<h3 class="text-lg font-medium text-red-900 mb-2">加载失败</h3>
<p class="text-red-600">❌ 加载补丁列表失败: ${error.message}</p>
</div>
`;
} finally {
loading.classList.add('hidden');
}
}

// 回滚补丁
async function rollbackPatch(patchName) {
if (!confirm(`确定要回滚补丁 "${patchName}" 吗?`)) {
return;
}

try {
const response = await fetch(`${API_BASE}/rollback`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `patchName=${encodeURIComponent(patchName)}`
});

const result = await response.json();

if (result.success) {
showMessage(`✅ ${result.message}`, 'success');
refreshPatches();
} else {
showMessage(`❌ ${result.message}`, 'error');
}
} catch (error) {
showMessage(`网络错误: ${error.message}`, 'error');
}
}

// 更新统计信息
function updateStats() {
document.getElementById('totalPatches').textContent = stats.total;
document.getElementById('successCount').textContent = stats.success;
document.getElementById('lastLoadTime').textContent = stats.lastLoad || '--';
}

// 键盘事件:回车加载补丁
document.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const target = e.target;
if (target.id === 'patchName' || target.id === 'patchVersion') {
loadPatch();
}
}
});
</script>
</body>
</html>

实际使用示例

1. Spring Bean替换示例

假设我们的原始服务类有bug:

@Service
public class UserService {

public String getUserInfo(Long userId) {
// 这里有空指针异常的bug
if (userId == null) {
return null; // 这里会导致后续调用出现问题
}

if (userId == 1L) {
return "Alice";
} else if (userId == 2L) {
return "Bob";
} else {
return null; // 这里会导致后续调用出现空指针异常
}
}

public int getUserNameLength(Long userId) {
String userName = getUserInfo(userId);
return userName.length(); // 当userName为null时会抛出空指针异常
}
}

创建Spring Bean补丁:

@HotPatch(
type = PatchType.SPRING_BEAN,
originalBean = "userService",
version = "1.0.1",
description = "修复getUserInfo空指针异常"
)
@Service
public class UserServicePatch {

public String getUserInfo(Long userId) {
// 修复空指针异常问题
if (userId == null) {
return "未知用户"; // 返回默认值而不是null
}

if (userId == 1L) {
return "Alice";
} else if (userId == 2L) {
return "Bob";
} else {
return "未知用户"; // 返回默认值而不是null
}
}

public int getUserNameLength(Long userId) {
String userName = getUserInfo(userId);
return userName != null ? userName.length() : 0; // 安全的长度计算
}
}

2. 普通Java类替换示例

假设有一个工具类需要修复:

// 原始类
public class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.length() == 0; // 忘记考虑空白字符
}
}

创建类替换补丁:

@HotPatch(
type = PatchType.JAVA_CLASS,
originalClass = "com.example.hotpatch.example.StringUtils",
version = "1.0.2",
description = "修复isEmpty方法逻辑,考虑空白字符"
)
public class StringUtilsPatch {

public static boolean isEmpty(String str) {
// 修复:考虑空白字符
return str == null || str.trim().length() == 0;
}

public static String trim(String str) {
return str == null ? null : str.trim();
}
}

4. 打包和部署补丁

编译补丁

# 1. 编译补丁类(需要依赖原项目的classpath)
javac -cp "target/classes:target/lib/*" src/main/java/patches/UserServicePatch.java

# 2. 打包为jar(包含补丁注解信息)
jar cf UserService-1.0.1.jar -C target/classes patches/UserServicePatch.class

# 3. 将补丁放到指定目录
cp *.jar ./patches/

启动应用(带Agent支持)

# 启动Spring Boot应用,加载Java Agent
java -javaagent:target/springboot-hot-patch-1.0.0-agent.jar \
-Dhotpatch.enabled=true \
-Dhotpatch.path=./patches \
-jar target/springboot-hot-patch-1.0.0.jar

动态加载补丁

# 通过API加载不同类型的补丁

# 1. 加载Spring Bean补丁
curl -X POST "http://localhost:8080/api/hotpatch/load" \
-d "patchName=UserService&version=1.0.1"

# 2. 加载Java类补丁
curl -X POST "http://localhost:8080/api/hotpatch/load" \
-d "patchName=StringUtils&version=1.0.2"

5. 应用配置

# application.properties
spring.application.name=springboot-hot-patch
server.port=8080

# Hot Patch Configuration
hotpatch.enabled=true
hotpatch.path=./patches

6. 测试验证

创建测试控制器验证补丁效果:

/**
* 测试控制器 - 用于测试热补丁功能
*/
@RestController
@RequestMapping("/api/test")
public class TestController {

@Autowired
private UserService userService;

// 测试Spring Bean补丁
@GetMapping("/user")
public String testUser(@RequestParam(value = "id",required = false) Long id) {
try {
int userNameLength = userService.getUserNameLength(id);
return "用户名长度: " + userNameLength;
} catch (Exception e) {
return "错误: " + e.getMessage();
}
}

// 测试工具类补丁
@GetMapping("/string-utils")
public boolean testStringUtils(@RequestParam(defaultValue = " ") String str) {
return StringUtils.isEmpty(str);
}

// 测试静态方法补丁
@GetMapping("/math/{a}/{b}")
public String testMath(@PathVariable int a, @PathVariable int b) {
try {
int result = MathHelper.divide(a, b);
return "计算结果: " + a + " / " + b + " = " + result;
} catch (Exception e) {
return "错误: " + e.getMessage();
}
}
}

测试步骤:

# 1. 测试原始版本(会出错)
curl "http://localhost:8080/api/test/user" # 返回null或异常

# 2. 通过Web界面加载补丁
访问 http://localhost:8080/index.html 加载对应补丁

# 3. 再次测试(已修复)
curl "http://localhost:8080/api/test/user" # 返回"用户名长度: 4"

最佳实践

1. 补丁开发规范

  • 明确的命名约定:补丁类名 = 原类名 + Patch
  • 版本管理:使用语义化版本号
  • 充分测试:补丁代码必须经过严格测试
  • 最小化改动:只修复必要的问题,避免引入新功能

2. 部署流程

  • 开发阶段:本地开发并测试补丁
  • 测试阶段:在测试环境验证补丁效果
  • 审核阶段:代码审核和安全检查
  • 部署阶段:生产环境热加载
  • 监控阶段:观察补丁效果和系统稳定性

3. 监控告警

@EventListener
public void onPatchLoaded(PatchLoadedEvent event) {
// 发送告警通知
alertService.sendAlert(
"热补丁加载通知",
String.format("补丁 %s:%s 已成功加载",
event.getPatchName(), event.getVersion())
);

// 记录审计日志
auditService.log("PATCH_LOADED", event.getPatchName(),
SecurityContextHolder.getContext().getAuthentication().getName());
}

适用场景

这个热补丁系统特别适合以下场景:

  • 紧急Bug修复:生产环境出现严重bug,需要快速修复
  • 性能优化:发现性能瓶颈,需要临时优化逻辑
  • 功能开关:需要临时快速开启/关闭某些功能特性
  • 参数调优:需要临时调整算法参数或配置值

注意事项

  • 谨慎使用:热补丁虽然强大,但应当作为应急手段,不能替代正常的发版流程
  • 充分测试:每个补丁都必须经过严格测试,确保不会引入新问题
  • 权限控制:建立严格的权限管理体系,防止误操作

写在最后

作为一名程序员,我们都经历过被生产bug”半夜惊醒”的痛苦。

传统的修复流程往往需要1-2小时,而用户可能在这期间流失,业务损失难以估量。

热补丁技术让我们能够在几分钟内修复问题,虽然不是银弹,但确实是应急工具箱中的一件利器。

当然,好的架构设计和充分的测试永远是避免生产问题的最佳实践。热补丁只是我们技术工具链中的一环,真正的稳定性还是要从设计、开发、测试、部署等各个环节来保障。

https://github.com/yuboon/java-examples/tree/master/springboot-hot-patch

文章目录
  1. 1. 背景:为什么需要热补丁?
  2. 2. 设计思路
  3. 3. 核心实现
    1. 3.0.1. 1. 项目结构
    2. 3.0.2. 2. Maven配置
    3. 3.0.3. 3. 注解和枚举定义
    4. 3.0.4. 4. Java Agent支持
    5. 3.0.5. 5. 配置属性类
    6. 3.0.6. 6. 数据模型
    7. 3.0.7. 7. 核心热补丁加载器
    8. 3.0.8. 8. REST API控制器
    9. 3.0.9. 9. Web管理界面
  • 4. 实际使用示例
    1. 4.0.1. 1. Spring Bean替换示例
    2. 4.0.2. 2. 普通Java类替换示例
    3. 4.0.3. 4. 打包和部署补丁
      1. 4.0.3.1. 编译补丁
      2. 4.0.3.2. 启动应用(带Agent支持)
      3. 4.0.3.3. 动态加载补丁
    4. 4.0.4. 5. 应用配置
    5. 4.0.5. 6. 测试验证
  • 5. 最佳实践
    1. 5.0.1. 1. 补丁开发规范
    2. 5.0.2. 2. 部署流程
    3. 5.0.3. 3. 监控告警
  • 6. 适用场景
  • 7. 注意事项
  • 8. 写在最后