使用Spring Boot 實作動態載入jar 包,動態配置功能太讚了

2024.08.09

在當今的軟體開發中,靈活性和可擴展性是至關重要的。 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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.

YAML 屬性檔案設定(application.yml)

# 动态配置相关属性
dynamic:
  enabled: true
  # 其他动态配置项
  • 1.
  • 2.
  • 3.
  • 4.

後端程式碼範例

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();
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.

工具類別 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();
            }
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.

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("配置更新成功");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.

核心的後端程式碼實作如下:

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);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

使用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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.

總結

本文展示了一個使用Spring Boot 實現動態載入、卸載JAR 套件和動態修改YAML 配置資訊的完整範例,包括專案配置的更新、相關類別的實作以及使用Thymeleaf 實現的前端頁面,為開發者提供了一個可參考的實現方案。