基于SpringBoot實(shí)現(xiàn)熱補(bǔ)丁加載器的詳細(xì)方案
每個(gè)程序員都有過(guò)這樣的經(jīng)歷——凌晨三點(diǎn)被電話驚醒,生產(chǎn)環(huán)境出現(xiàn)緊急bug,而修復(fù)發(fā)布又需要漫長(zhǎng)的流程。
今天我們來(lái)介紹如何用SpringBoot 打造一個(gè)熱補(bǔ)丁加載器,讓你在緊急時(shí)刻也能從容應(yīng)對(duì)。

背景:為什么需要熱補(bǔ)???
想象一下這個(gè)場(chǎng)景:周五晚上8點(diǎn),你剛準(zhǔn)備下班,突然收到監(jiān)控報(bào)警——生產(chǎn)環(huán)境某個(gè)關(guān)鍵接口出現(xiàn)空指針異常,影響了大量用戶(hù)。這時(shí)候你面臨幾個(gè)選擇:
傳統(tǒng)發(fā)布流程:修改代碼 → 測(cè)試 → 打包 → 發(fā)布,至少需要1-2小時(shí)
回滾到上個(gè)版本:可能會(huì)丟失其他新功能
熱補(bǔ)丁修復(fù):幾分鐘內(nèi)修復(fù)問(wèn)題,不影響服務(wù)運(yùn)行
顯然,第三種方案可以解燃眉之急。
設(shè)計(jì)思路
我們的熱補(bǔ)丁加載器基于以下幾個(gè)核心思想:
動(dòng)態(tài)類(lèi)加載:利用Java的ClassLoader機(jī)制動(dòng)態(tài)加載補(bǔ)丁類(lèi)
多層次替換:支持Spring Bean、普通Java類(lèi)、靜態(tài)方法等多種替換方式
字節(jié)碼增強(qiáng):通過(guò)Java Agent和Instrumentation API實(shí)現(xiàn)任意類(lèi)的運(yùn)行時(shí)替換
版本管理:每個(gè)補(bǔ)丁都有版本號(hào),支持回滾
安全可控:只允許特定路徑的補(bǔ)丁文件,防止安全風(fēng)險(xiǎn)
核心實(shí)現(xiàn)
1. 項(xiàng)目結(jié)構(gòu)
首先,我們來(lái)看看完整的項(xiàng)目結(jié)構(gòu):
springboot-hot-patch/ ├── src/main/java/com/example/hotpatch/ │ ├── agent/ # Java Agent相關(guān) │ │ └── HotPatchAgent.java │ ├── annotation/ # 注解定義 │ │ ├── HotPatch.java │ │ └── PatchType.java │ ├── config/ # 配置類(lèi) │ │ ├── HotPatchConfig.java │ │ └── HotPatchProperties.java │ ├── controller/ # 控制器 │ │ ├── HotPatchController.java │ │ └── TestController.java │ ├── core/ # 核心熱補(bǔ)丁加載器 │ │ └── HotPatchLoader.java │ ├── example/ # 示例代碼 │ │ ├── UserService.java │ │ ├── StringUtils.java │ │ └── MathHelper.java │ ├── instrumentation/ # 字節(jié)碼操作 │ │ └── InstrumentationHolder.java │ ├── model/ # 數(shù)據(jù)模型 │ │ ├── PatchInfo.java │ │ └── PatchResult.java │ └── patches/ # 補(bǔ)丁示例 │ ├── UserServicePatch.java │ ├── StringUtilsPatch.java │ └── MathHelperDividePatch.java ├── src/main/resources/ │ ├── static/ # Web模板 │ │ ├── index.html │ └── application.properties ├── patches/ # 補(bǔ)丁文件目錄 └── 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. 注解和枚舉定義
/**
* 補(bǔ)丁類(lèi)型枚舉
*/
public enum PatchType {
/**
* Spring Bean 替換
*/
SPRING_BEAN,
/**
* 普通Java類(lèi)替換(整個(gè)類(lèi))
*/
JAVA_CLASS,
/**
* 靜態(tài)方法替換
*/
STATIC_METHOD,
/**
* 實(shí)例方法替換
*/
INSTANCE_METHOD
}
/**
* 增強(qiáng)的熱補(bǔ)丁注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HotPatch {
/**
* 補(bǔ)丁類(lèi)型
*/
PatchType type() default PatchType.SPRING_BEAN;
/**
* 原始Bean名稱(chēng)(當(dāng)type=SPRING_BEAN時(shí)使用)
*/
String originalBean() default "";
/**
* 原始類(lèi)的全限定名(當(dāng)type=JAVA_CLASS或STATIC_METHOD時(shí)使用)
*/
String originalClass() default "";
/**
* 要替換的方法名(當(dāng)type=STATIC_METHOD或INSTANCE_METHOD時(shí)使用)
*/
String methodName() default "";
/**
* 方法簽名(用于方法重載區(qū)分)
*/
String methodSignature() default "";
/**
* 補(bǔ)丁版本
*/
String version() default "1.0";
/**
* 補(bǔ)丁描述
*/
String description() default "";
/**
* 是否啟用安全驗(yàn)證
*/
boolean securityCheck() default true;
}
4. Java Agent支持
/**
* Instrumentation持有器 - 用于獲取JVM的Instrumentation實(shí)例
*/
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入口類(lèi)
*/
public class HotPatchAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("HotPatch Agent 啟動(dòng)成功");
InstrumentationHolder.setInstrumentation(inst);
}
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("HotPatch Agent 動(dòng)態(tài)加載成功");
InstrumentationHolder.setInstrumentation(inst);
}
}
5. 配置屬性類(lèi)
/**
* 熱補(bǔ)丁配置屬性
*/
@ConfigurationProperties(prefix = "hotpatch")
@Component
@Data
public class HotPatchProperties {
/**
* 是否啟用熱補(bǔ)丁功能
*/
private boolean enabled = false;
/**
* 補(bǔ)丁文件存放路徑
*/
private String path = "./patches";
/**
* 允許的補(bǔ)丁文件最大大?。ㄗ止?jié))
*/
private long maxFileSize = 10 * 1024 * 1024;
/**
* 是否啟用補(bǔ)丁簽名驗(yàn)證
*/
private boolean signatureVerification = false;
/**
* 允許執(zhí)行熱補(bǔ)丁操作的角色列表
*/
private List<String> allowedRoles = List.of("ADMIN", "DEVELOPER");
}
6. 數(shù)據(jù)模型
/**
* 補(bǔ)丁信息類(lèi)
*/
@Data
@AllArgsConstructor
public class PatchInfo {
private String name;
private String version;
private Class<?> patchClass;
private PatchType patchType;
private long loadTime;
private String originalTarget; // 原始目標(biāo)(Bean名稱(chēng)或類(lèi)名)
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";
}
}
/**
* 補(bǔ)丁操作結(jié)果類(lèi)
*/
@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. 核心熱補(bǔ)丁加載器
考慮篇幅,以下為部分關(guān)鍵代碼
/**
* 增強(qiáng)版補(bǔ)丁加載器核心類(lèi)
*/
@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 實(shí)例
this.instrumentation = InstrumentationHolder.getInstrumentation();
}
/**
* 加載熱補(bǔ)丁 - 支持任意類(lèi)替換
* @param patchName 補(bǔ)丁名稱(chēng)
* @param version 版本號(hào)
*/
public PatchResult loadPatch(String patchName, String version) {
if (!properties.isEnabled()) {
return PatchResult.failed("熱補(bǔ)丁功能未啟用");
}
try {
// 1. 驗(yàn)證補(bǔ)丁文件
File patchFile = validatePatchFile(patchName, version);
// 2. 創(chuàng)建專(zhuān)用的類(lèi)加載器
URLClassLoader patchClassLoader = createPatchClassLoader(patchFile);
// 3. 加載補(bǔ)丁類(lèi)
Class<?> patchClass = loadPatchClass(patchClassLoader, patchName);
// 4. 獲取補(bǔ)丁注解信息
HotPatch patchAnnotation = patchClass.getAnnotation(HotPatch.class);
if (patchAnnotation == null) {
return PatchResult.failed("補(bǔ)丁類(lèi)缺少 @HotPatch 注解");
}
// 5. 根據(jù)補(bǔ)丁類(lèi)型選擇替換策略
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("實(shí)例方法替換暫未實(shí)現(xiàn),請(qǐng)使用動(dòng)態(tài)代理方式");
default:
return PatchResult.failed("不支持的補(bǔ)丁類(lèi)型: " + patchType);
}
// 6. 記錄補(bǔ)丁信息
PatchInfo patchInfo = new PatchInfo(patchName, version,
patchClass, patchType, System.currentTimeMillis());
loadedPatches.put(patchName, patchInfo);
log.info("熱補(bǔ)丁 {}:{} ({}) 加載成功", patchName, version, patchType);
return PatchResult.success("補(bǔ)丁加載成功");
} catch (Exception e) {
log.error("熱補(bǔ)丁加載失敗: {}", e.getMessage(), e);
return PatchResult.failed("補(bǔ)丁加載失敗: " + e.getMessage());
}
}
}
8. REST API控制器
/**
* 熱補(bǔ)丁管理控制器
*/
@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("請(qǐng)求加載熱補(bǔ)丁: {}:{}", 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("請(qǐng)求回滾補(bǔ)丁: {}", 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管理界面
我們提供了一個(gè)美觀實(shí)用的Web管理界面:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>熱補(bǔ)丁管理器</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">?? 熱補(bǔ)丁管理器</h1>
<p class="text-xl opacity-90">Spring Boot 線上緊急修復(fù)控制臺(tái)</p>
</div>
</div>
</header>
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 統(tǒng)計(jì)卡片 -->
<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">已加載補(bǔ)丁</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">成功次數(shù)</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>
<!-- 消息顯示區(qū)域 -->
<div id="message" class="mb-6"></div>
<!-- 加載補(bǔ)丁區(qū)域 -->
<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">?? 加載補(bǔ)丁</h2>
</div>
<div class="space-y-4">
<!-- 補(bǔ)丁選擇下拉框 -->
<div>
<label for="patchSelector" class="block text-sm font-medium text-gray-700 mb-2">選擇補(bǔ)丁</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="">正在掃描補(bǔ)丁目錄...</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>
<!-- 或者手動(dòng)輸入 -->
<div class="border-t pt-4">
<p class="text-sm text-gray-600 mb-3">或者手動(dòng)輸入補(bǔ)丁信息:</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">補(bǔ)丁名稱(chēng)</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">版本號(hào)</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>
加載補(bǔ)丁
</button>
</div>
</div>
</div>
<!-- 補(bǔ)丁列表區(qū)域 -->
<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">?? 已加載補(bǔ)丁</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>
<!-- 加載狀態(tài) -->
<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">正在加載補(bǔ)丁列表...</span>
</div>
</div>
<!-- 補(bǔ)丁列表 -->
<div id="patchList" class="space-y-4">
<!-- 補(bǔ)丁項(xiàng)目將在這里顯示 -->
</div>
</div>
</main>
<script>
// API 基礎(chǔ)路徑
const API_BASE = '/api/hotpatch';
// 統(tǒng)計(jì)數(shù)據(jù)
let stats = {
total: 0,
success: 0,
lastLoad: null
};
// 頁(yè)面加載完成后初始化
document.addEventListener('DOMContentLoaded', function() {
scanPatchesDirectory();
refreshPatches();
updateStats();
});
// 掃描補(bǔ)丁目錄
async function scanPatchesDirectory() {
const selector = document.getElementById('patchSelector');
try {
// 這里模擬掃描補(bǔ)丁目錄的API調(diào)用
// 實(shí)際應(yīng)該調(diào)用后端API來(lái)獲取patches目錄下的所有jar文件
const response = await fetch(`${API_BASE}/scan-patches`);
if (response.ok) {
const patches = await response.json();
selector.innerHTML = '<option value="">請(qǐng)選擇一個(gè)補(bǔ)丁</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不存在,使用模擬數(shù)據(jù)
selector.innerHTML = `
<option value="">請(qǐng)選擇一個(gè)補(bǔ)丁</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) {
// 使用模擬數(shù)據(jù)
selector.innerHTML = `
<option value="">請(qǐng)選擇一個(gè)補(bǔ)丁</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秒后自動(dòng)隱藏
setTimeout(() => {
messageDiv.innerHTML = '';
}, 3000);
}
// 加載補(bǔ)丁
async function loadPatch() {
const patchName = document.getElementById('patchName').value.trim();
const version = document.getElementById('patchVersion').value.trim();
if (!patchName || !version) {
showMessage('請(qǐng)選擇補(bǔ)丁或手動(dòng)輸入補(bǔ)丁名稱(chēng)和版本號(hào)', '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(`網(wǎng)絡(luò)錯(cuò)誤: ${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>
加載補(bǔ)丁
`;
}
}
// 刷新補(bǔ)丁列表
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">暫無(wú)已加載的補(bǔ)丁</h3>
<p class="text-gray-500">在上方選擇補(bǔ)丁并點(diǎn)擊"加載補(bǔ)丁"開(kāi)始使用</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>
加載時(shí)間: ${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>
類(lèi)型: ${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>
目標(biāo): ${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>
回滾補(bǔ)丁
</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">? 加載補(bǔ)丁列表失敗: ${error.message}</p>
</div>
`;
} finally {
loading.classList.add('hidden');
}
}
// 回滾補(bǔ)丁
async function rollbackPatch(patchName) {
if (!confirm(`確定要回滾補(bǔ)丁 "${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(`網(wǎng)絡(luò)錯(cuò)誤: ${error.message}`, 'error');
}
}
// 更新統(tǒng)計(jì)信息
function updateStats() {
document.getElementById('totalPatches').textContent = stats.total;
document.getElementById('successCount').textContent = stats.success;
document.getElementById('lastLoadTime').textContent = stats.lastLoad || '--';
}
// 鍵盤(pán)事件:回車(chē)加載補(bǔ)丁
document.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const target = e.target;
if (target.id === 'patchName' || target.id === 'patchVersion') {
loadPatch();
}
}
});
</script>
</body>
</html>
實(shí)際使用示例
1. Spring Bean替換示例
假設(shè)我們的原始服務(wù)類(lèi)有bug:
@Service
public class UserService {
public String getUserInfo(Long userId) {
// 這里有空指針異常的bug
if (userId == null) {
return null; // 這里會(huì)導(dǎo)致后續(xù)調(diào)用出現(xiàn)問(wèn)題
}
if (userId == 1L) {
return "Alice";
} else if (userId == 2L) {
return "Bob";
} else {
return null; // 這里會(huì)導(dǎo)致后續(xù)調(diào)用出現(xiàn)空指針異常
}
}
public int getUserNameLength(Long userId) {
String userName = getUserInfo(userId);
return userName.length(); // 當(dāng)userName為null時(shí)會(huì)拋出空指針異常
}
}
創(chuàng)建Spring Bean補(bǔ)?。?/p>
@HotPatch(
type = PatchType.SPRING_BEAN,
originalBean = "userService",
version = "1.0.1",
description = "修復(fù)getUserInfo空指針異常"
)
@Service
public class UserServicePatch {
public String getUserInfo(Long userId) {
// 修復(fù)空指針異常問(wèn)題
if (userId == null) {
return "未知用戶(hù)"; // 返回默認(rèn)值而不是null
}
if (userId == 1L) {
return "Alice";
} else if (userId == 2L) {
return "Bob";
} else {
return "未知用戶(hù)"; // 返回默認(rèn)值而不是null
}
}
public int getUserNameLength(Long userId) {
String userName = getUserInfo(userId);
return userName != null ? userName.length() : 0; // 安全的長(zhǎng)度計(jì)算
}
}
2. 普通Java類(lèi)替換示例
假設(shè)有一個(gè)工具類(lèi)需要修復(fù):
// 原始類(lèi)
public class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.length() == 0; // 忘記考慮空白字符
}
}
創(chuàng)建類(lèi)替換補(bǔ)?。?/p>
@HotPatch(
type = PatchType.JAVA_CLASS,
originalClass = "com.example.hotpatch.example.StringUtils",
version = "1.0.2",
description = "修復(fù)isEmpty方法邏輯,考慮空白字符"
)
public class StringUtilsPatch {
public static boolean isEmpty(String str) {
// 修復(fù):考慮空白字符
return str == null || str.trim().length() == 0;
}
public static String trim(String str) {
return str == null ? null : str.trim();
}
}
3. 打包和部署補(bǔ)丁
編譯補(bǔ)丁
# 1. 編譯補(bǔ)丁類(lèi)(需要依賴(lài)原項(xiàng)目的classpath) javac -cp "target/classes:target/lib/*" src/main/java/patches/UserServicePatch.java # 2. 打包為jar(包含補(bǔ)丁注解信息) jar cf UserService-1.0.1.jar -C target/classes patches/UserServicePatch.class # 3. 將補(bǔ)丁放到指定目錄 cp *.jar ./patches/
啟動(dòng)應(yīng)用(帶Agent支持)
# 啟動(dòng)Spring Boot應(yīng)用,加載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
動(dòng)態(tài)加載補(bǔ)丁
# 通過(guò)API加載不同類(lèi)型的補(bǔ)丁
# 1. 加載Spring Bean補(bǔ)丁
curl -X POST "http://localhost:8080/api/hotpatch/load" \
-d "patchName=UserService&version=1.0.1"
# 2. 加載Java類(lèi)補(bǔ)丁
curl -X POST "http://localhost:8080/api/hotpatch/load" \
-d "patchName=StringUtils&version=1.0.2"
4. 應(yīng)用配置
# application.properties spring.application.name=springboot-hot-patch server.port=8080 # Hot Patch Configuration hotpatch.enabled=true hotpatch.path=./patches
5. 測(cè)試驗(yàn)證
創(chuàng)建測(cè)試控制器驗(yàn)證補(bǔ)丁效果:
/**
* 測(cè)試控制器 - 用于測(cè)試熱補(bǔ)丁功能
*/
@RestController
@RequestMapping("/api/test")
public class TestController {
@Autowired
private UserService userService;
// 測(cè)試Spring Bean補(bǔ)丁
@GetMapping("/user")
public String testUser(@RequestParam(value = "id",required = false) Long id) {
try {
int userNameLength = userService.getUserNameLength(id);
return "用戶(hù)名長(zhǎng)度: " + userNameLength;
} catch (Exception e) {
return "錯(cuò)誤: " + e.getMessage();
}
}
// 測(cè)試工具類(lèi)補(bǔ)丁
@GetMapping("/string-utils")
public boolean testStringUtils(@RequestParam(defaultValue = " ") String str) {
return StringUtils.isEmpty(str);
}
// 測(cè)試靜態(tài)方法補(bǔ)丁
@GetMapping("/math/{a}/")
public String testMath(@PathVariable int a, @PathVariable int b) {
try {
int result = MathHelper.divide(a, b);
return "計(jì)算結(jié)果: " + a + " / " + b + " = " + result;
} catch (Exception e) {
return "錯(cuò)誤: " + e.getMessage();
}
}
}
測(cè)試步驟:
# 1. 測(cè)試原始版本(會(huì)出錯(cuò)) curl "http://localhost:8080/api/test/user" # 返回null或異常 # 2. 通過(guò)Web界面加載補(bǔ)丁 訪問(wèn) http://localhost:8080/index.html 加載對(duì)應(yīng)補(bǔ)丁 # 3. 再次測(cè)試(已修復(fù)) curl "http://localhost:8080/api/test/user" # 返回"用戶(hù)名長(zhǎng)度: 4"
最佳實(shí)踐
1. 補(bǔ)丁開(kāi)發(fā)規(guī)范
明確的命名約定:補(bǔ)丁類(lèi)名 = 原類(lèi)名 + Patch
版本管理:使用語(yǔ)義化版本號(hào)
充分測(cè)試:補(bǔ)丁代碼必須經(jīng)過(guò)嚴(yán)格測(cè)試
最小化改動(dòng):只修復(fù)必要的問(wèn)題,避免引入新功能
2. 部署流程
1. 開(kāi)發(fā)階段:本地開(kāi)發(fā)并測(cè)試補(bǔ)丁
2. 測(cè)試階段:在測(cè)試環(huán)境驗(yàn)證補(bǔ)丁效果
3. 審核階段:代碼審核和安全檢查
4. 部署階段:生產(chǎn)環(huán)境熱加載
5. 監(jiān)控階段:觀察補(bǔ)丁效果和系統(tǒng)穩(wěn)定性
3. 監(jiān)控告警
@EventListener
public void onPatchLoaded(PatchLoadedEvent event) {
// 發(fā)送告警通知
alertService.sendAlert(
"熱補(bǔ)丁加載通知",
String.format("補(bǔ)丁 %s:%s 已成功加載",
event.getPatchName(), event.getVersion())
);
// 記錄審計(jì)日志
auditService.log("PATCH_LOADED", event.getPatchName(),
SecurityContextHolder.getContext().getAuthentication().getName());
}
適用場(chǎng)景
這個(gè)熱補(bǔ)丁系統(tǒng)特別適合以下場(chǎng)景:
- 緊急Bug修復(fù):生產(chǎn)環(huán)境出現(xiàn)嚴(yán)重bug,需要快速修復(fù)
- 性能優(yōu)化:發(fā)現(xiàn)性能瓶頸,需要臨時(shí)優(yōu)化邏輯
- 功能開(kāi)關(guān):需要臨時(shí)快速開(kāi)啟/關(guān)閉某些功能特性
- 參數(shù)調(diào)優(yōu):需要臨時(shí)調(diào)整算法參數(shù)或配置值
注意事項(xiàng)
- 謹(jǐn)慎使用:熱補(bǔ)丁雖然強(qiáng)大,但應(yīng)當(dāng)作為應(yīng)急手段,不能替代正常的發(fā)版流程
- 充分測(cè)試:每個(gè)補(bǔ)丁都必須經(jīng)過(guò)嚴(yán)格測(cè)試,確保不會(huì)引入新問(wèn)題
- 權(quán)限控制:建立嚴(yán)格的權(quán)限管理體系,防止誤操作
寫(xiě)在最后
作為一名程序員,我們都經(jīng)歷過(guò)被生產(chǎn)bug"半夜驚醒"的痛苦。
傳統(tǒng)的修復(fù)流程往往需要1-2小時(shí),而用戶(hù)可能在這期間流失,業(yè)務(wù)損失難以估量。
熱補(bǔ)丁技術(shù)讓我們能夠在幾分鐘內(nèi)修復(fù)問(wèn)題,雖然不是銀彈,但確實(shí)是應(yīng)急工具箱中的一件利器。
當(dāng)然,好的架構(gòu)設(shè)計(jì)和充分的測(cè)試永遠(yuǎn)是避免生產(chǎn)問(wèn)題的最佳實(shí)踐。熱補(bǔ)丁只是我們技術(shù)工具鏈中的一環(huán),真正的穩(wěn)定性還是要從設(shè)計(jì)、開(kāi)發(fā)、測(cè)試、部署等各個(gè)環(huán)節(jié)來(lái)保障。
以上就是基于SpringBoot實(shí)現(xiàn)熱補(bǔ)丁加載器的詳細(xì)方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot熱補(bǔ)丁加載器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot+Swagger打造美觀API文檔的實(shí)戰(zhàn)指南
在現(xiàn)代 Web 開(kāi)發(fā)中,API 文檔是開(kāi)發(fā)者和團(tuán)隊(duì)協(xié)作的重要組成部分,本文將詳細(xì)介紹如何在 SpringBoot2和SpringBoot3中集成Swagger,并展示多種接口文檔風(fēng)格,幫助開(kāi)發(fā)者選擇適合自己項(xiàng)目的展示方式2025-08-08
詳解Spring Security中的HttpBasic登錄驗(yàn)證模式
HttpBasic登錄驗(yàn)證模式是Spring Security實(shí)現(xiàn)登錄驗(yàn)證最簡(jiǎn)單的一種方式,也可以說(shuō)是最簡(jiǎn)陋的一種方式,這篇文章主要介紹了Spring Security的HttpBasic登錄驗(yàn)證模式,需要的朋友可以參考下2019-11-11
druid?handleException執(zhí)行流程源碼解析
這篇文章主要為大家介紹了druid?handleException執(zhí)行流程源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
webservice實(shí)現(xiàn)springboot項(xiàng)目間接口調(diào)用與對(duì)象傳遞示例
本文主要介紹了webservice實(shí)現(xiàn)springboot項(xiàng)目間接口調(diào)用與對(duì)象傳遞示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
SpringBoot整合Quartz實(shí)現(xiàn)定時(shí)任務(wù)詳解
這篇文章主要介紹了Java?任務(wù)調(diào)度框架?Quartz,Quartz是OpenSymphony開(kāi)源組織在Job?scheduling領(lǐng)域又一個(gè)開(kāi)源項(xiàng)目,完全由Java開(kāi)發(fā),可以用來(lái)執(zhí)行定時(shí)任務(wù),類(lèi)似于java.util.Timer。,下面我們來(lái)學(xué)習(xí)一下關(guān)于?Quartz更多的詳細(xì)內(nèi)容,需要的朋友可以參考一下2022-08-08
Java多線程編程實(shí)戰(zhàn)之模擬大量數(shù)據(jù)同步
這篇文章主要介紹了Java多線程編程實(shí)戰(zhàn)之模擬大量數(shù)據(jù)同步,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02

