Use Spring Boot to dynamically load jar packages. The dynamic configuration function is awesome!

2024.08.09

In today's software development, flexibility and scalability are crucial. The Spring Boot framework provides us with powerful tools and mechanisms that make it easy and efficient to dynamically load jar packages and dynamically configure. This feature has great advantages in dealing with ever-changing business needs and complex operating environments.

Principles and advantages of dynamically loading jar packages

The implementation of dynamic loading of jar packages is based on Java's class loading mechanism. In Java, the class loader is responsible for loading the bytecode of a class into the JVM and creating the corresponding class object. Usually, Java applications use the default class loader hierarchy, including the startup class loader, the extension class loader, and the application class loader. However, in order to implement dynamic loading of jar packages, we need to create a custom class loader.

The custom class loader inherits from the java.lang.ClassLoader class and overrides its findClass or loadClass method to implement custom class search and loading logic. When a jar package needs to be loaded dynamically, the custom class loader first obtains the file path of the jar package and then reads the bytecode data in the jar package.

By parsing the bytecode data, the class information defined in it is found and loaded into the JVM. In this process, the class dependencies also need to be processed to ensure that all related classes can be loaded correctly.

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

First, it greatly improves the flexibility of the system. In traditional application deployment, if you need to add new features or fix defects, you often need to recompile, package, and deploy the entire application. By dynamically loading jar packages, you can directly load new functional modules when the application is running without interrupting the service, achieving seamless functional expansion and update.

Secondly, it helps reduce system maintenance costs. For some frequently changing business needs, there is no need to carry out large-scale application deployment due to small functional adjustments, which reduces the risk and manpower investment in the deployment process.

Furthermore, dynamically loading jar packages can improve development efficiency. Developers can independently develop and test new functional modules, and then dynamically load them into the production environment when needed, avoiding frequent integration and conflicts with existing codes.

In addition, it also provides strong support for the modular design of the system. Different functional modules can be encapsulated in independent jar packages and dynamically loaded according to actual needs, making the system architecture clearer and easier to manage.

Project dependency configuration (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 properties file configuration (application.yml)

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

Backend code example

DynamicConfig class:

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.

Tool class 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.

The core backend code is implemented as follows:

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.

Front-end page using 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.

Summarize

This article shows a complete example of using Spring Boot to dynamically load and unload JAR packages and dynamically modify YAML configuration information, including the update of project configuration, the implementation of related classes, and the front-end page implemented using Thymeleaf, providing developers with a reference implementation solution.