查看原文
其他

使用 Spring Boot 实现动态加载 jar 包,动态配置功能太赞了!

编程疏影 路条编程
2024-09-05


使用 Spring Boot 实现动态加载 jar 包,动态配置功能太赞了!

在当今的软件开发中,灵活性和可扩展性是至关重要的。Spring Boot 框架为我们提供了强大的工具和机制,使得实现动态加载 jar 包和动态配置变得轻松而高效。这一特性在应对不断变化的业务需求和复杂的运行环境时具有极大的优势。

动态加载 jar 包的原理与优势

动态加载 jar 包的实现基于 Java 的类加载机制。在 Java 中,类加载器负责将类的字节码加载到 JVM 中,并创建对应的类对象。通常,Java 应用使用默认的类加载器层次结构,包括启动类加载器、扩展类加载器和应用类加载器。然而,为了实现动态加载 jar 包,我们需要创建自定义的类加载器。

自定义类加载器继承自 java.lang.ClassLoader 类,并覆盖其 findClass  loadClass 方法来实现自定义的类查找和加载逻辑。当需要动态加载 jar 包时,自定义类加载器首先获取 jar 包的文件路径,然后读取 jar 包中的字节码数据。

通过解析字节码数据,找到其中定义的类信息,并将其加载到 JVM 中。在这个过程中,还需要处理类的依赖关系,确保所有相关的类都能正确加载。

动态加载 jar 包带来了诸多显著的优势。

首先,它极大地提高了系统的灵活性。在传统的应用部署中,如果需要添加新的功能或修复缺陷,往往需要重新编译、打包和部署整个应用。而通过动态加载 jar 包,可以在应用运行时直接加载新的功能模块,无需中断服务,实现了无缝的功能扩展和更新。

其次,它有助于降低系统的维护成本。对于一些频繁变化的业务需求,不必因为小的功能调整而进行大规模的应用部署,减少了部署过程中的风险和人力投入。

再者,动态加载 jar 包能够提高开发效率。开发人员可以独立开发和测试新的功能模块,然后在需要时将其动态加载到生产环境中,避免了与现有代码的频繁集成和冲突。

此外,它还为系统的模块化设计提供了有力支持。不同的功能模块可以封装在独立的 jar 包中,根据实际需求动态加载,使系统的架构更加清晰和易于管理。

项目依赖配置(pom.xml)

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.10</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.icoderoad</groupId>
<artifactId>dynamic-loading-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>dynamic-loading-demo</name>
<description>Demo project for dynamic loading with Spring Boot</description>

<properties>
<java.version>11</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

YAML 属性文件配置(application.yml)

# 动态配置相关属性
dynamic:
enabled: true
# 其他动态配置项

后端代码示例

DynamicConfig 类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Component;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

@Component
@ConfigurationProperties(prefix = "dynamic")
public class DynamicConfig {

private String configProperty;

@Autowired
private String filePath;

public String getConfigProperty() {
return configProperty;
}

public void setConfigProperty(String configProperty) {
this.configProperty = configProperty;
// 同步修改 YAML 文件中的配置信息
modifyYaml(filePath, "configProperty", configProperty);
}

public void modifyYaml(String filePath, String key, String value) {
try (FileInputStream inputStream = new FileInputStream(new File(filePath))) {
Yaml yaml = new Yaml();
Map<String, Object> config = yaml.load(inputStream);

config.put(key, value);

DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);

try (FileWriter writer = new FileWriter(new File(filePath))) {
yaml.dump(config, writer, options);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

工具类 JarLoadingUtils

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class JarLoadingUtils {

private Map<String, ClassLoader> loadedJars = new HashMap<>();

public void loadJars(List<String> jarPaths) throws IOException {
for (String jarPath : jarPaths) {
URL url = new URL(jarPath);
CustomClassLoader classLoader = new CustomClassLoader();
classLoader.loadJar(url.getFile());
loadedJars.put(jarPath, classLoader);
System.out.println("正在加载 JAR 包: " + jarPath);
}
}

public void unloadJar(String jarPath) {
ClassLoader classLoader = loadedJars.remove(jarPath);
if (classLoader!= null) {
// 执行卸载相关的逻辑
System.out.println("正在卸载 JAR 包: " + jarPath);
}
}

class CustomClassLoader extends URLClassLoader {

public CustomClassLoader() {
super(new URL[0], getParentClassLoader());
}

public void loadJar(String jarPath) {
try {
URL url = new File(jarPath).toURI().toURL();
addURL(url);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

DynamicLoadingController 类:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.io.IOException;
import java.util.List;
import java.util.Map;

public class DynamicLoadingController {

private JarLoadingUtils jarLoadingUtils;
private DynamicConfig dynamicConfig;

public DynamicLoadingController(JarLoadingUtils jarLoadingUtils, DynamicConfig dynamicConfig) {
this.jarLoadingUtils = jarLoadingUtils;
this.dynamicConfig = dynamicConfig;
}

@PostMapping("/dynamic/load")
public ResponseEntity<String> loadJars(@RequestBody List<String> jarPaths) {
try {
jarLoadingUtils.loadJars(jarPaths);
return ResponseEntity.status(HttpStatus.OK).body("JAR 包加载成功");
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("加载 JAR 包时出错: " + e.getMessage());
}
}

@PostMapping("/dynamic/unload")
public ResponseEntity<String> unloadJar(@RequestBody String jarPath) {
jarLoadingUtils.unloadJar(jarPath);
return ResponseEntity.status(HttpStatus.OK).body("JAR 包卸载成功");
}

@PostMapping("/dynamic/config/update")
public ResponseEntity<String> updateConfig(@RequestBody Map<String, String> configData) {
String key = configData.get("key");
String value = configData.get("value");
dynamicConfig.setConfigProperty(value);
return ResponseEntity.status(HttpStatus.OK).body("配置更新成功");
}
}

核心的后端代码实现如下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DynamicLoadingApplication implements ApplicationRunner {

@Autowired
private DynamicConfig dynamicConfig;

@Autowired
private JarLoadingUtils jarLoadingUtils;

public static void main(String[] args) {
SpringApplication.run(DynamicLoadingApplication.class, args);
}

@Override
public void run(ApplicationArguments args) throws Exception {
// 模拟动态加载 jar 包的逻辑
List<String> jarPaths = new ArrayList<>();
jarPaths.add("path/to/your/jar/file1.jar");
jarPaths.add("path/to/your/jar/file2.jar");
jarLoadingUtils.loadJars(jarPaths);
}
}

使用 Thymeleaf 的前端页面:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>动态加载配置页面</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
$("#loadButton").click(function() {
$.ajax({
url: "/dynamic/load",
type: "POST",
success: function(response) {
$("#loadResult").text(response);
},
error: function(xhr, status, error) {
$("#loadResult").text("加载出错: " + error);
}
});
});

$("#unloadButton").click(function() {
var jarPath = $("#unloadPath").val();
$.ajax({
url: "/dynamic/unload",
type: "POST",
data: JSON.stringify({ "jarPath": jarPath }),
contentType: "application/json",
success: function(response) {
$("#unloadResult").text(response);
},
error: function(xhr, status, error) {
$("#unloadResult").text("卸载出错: " + error);
}
});
});

$("#updateButton").click(function() {
var key = $("#updateKey").val();
var value = $("#updateValue").val();
$.ajax({
url: "/dynamic/config/update",
type: "POST",
data: JSON.stringify({ "key": key, "value": value }),
contentType: "application/json",
success: function(response) {
$("#updateResult").text(response);
},
error: function(xhr, status, error) {
$("#updateResult").text("更新出错: " + error);
}
});
});
});
</script>
</head>
<body>
<h2>动态操作</h2>
<button id="loadButton">触发动态加载</button>
<p id="loadResult"></p>
<form>
<input type="text" id="unloadPath" placeholder="输入要卸载的 JAR 路径" />
<button id="unloadButton">触发动态卸载</button>
</form>
<p id="unloadResult"></p>
<form>
<input type="text" id="updateKey" placeholder="输入配置键" />
<input type="text" id="updateValue" placeholder="输入配置值" />
<button id="updateButton">触发动态配置更新</button>
</form>
<p id="updateResult"></p>
</body>
</html>

总结:

本文展示了一个使用 Spring Boot 实现动态加载、卸载 JAR 包和动态修改 YAML 配置信息的完整示例,包括项目配置的更新、相关类的实现以及使用 Thymeleaf 实现的前端页面,为开发者提供了一个可参考的实现方案。


今天就讲到这里,如果有问题需要咨询,大家可以直接留言或扫下方二维码来知识星球找我,我们会尽力为你解答。


作者:路条编程(转载请获本公众号授权,并注明作者与出处)


继续滑动看下一个
路条编程
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存