Files
MyBlog/src/programming/backend/java/功能整理/06Spring Boot JAR 瘦身与加密.md
2025-12-15 23:16:59 +08:00

46 KiB
Raw Blame History

icon, date, category, tag, title
icon date category tag title
mdi:package-variant-closed 2025-05-13
实用工具
JAVA
SpringBoot
JAR
JAR包瘦身
Spring Boot JAR 瘦身与加密

Spring Boot JAR 瘦身与加密:构建安全高效的部署方案

Spring Boot JAR 瘦身与加密:构建安全高效的部署方案

在 Spring Boot 应用程序部署过程中,我们常常面临两个主要挑战:

  1. JAR 包体积过大Spring Boot 应用打包时会将所有依赖一起打包,导致最终 JAR 文件臃肿
  2. 代码安全性问题:部署到客户环境或公开场合的 JAR 包可能被反编译,造成核心业务逻辑泄露

为了解决这些问题,本文将介绍一套完整的解决方案,包括 JAR 包瘦身和 JAR 包加密两部分,以及配套的自定义类加载器,实现高效安全的 Spring Boot 应用部署。

整体方案设计

该方案由两个主要项目组成:

  1. spring-boot-jar-slim-encrypt:用于将 Spring Boot 应用 JAR 包瘦身和加密
  2. spring-boot-custom-classloader用于加载第三方JAR

工作流程

┌────────────────────┐
│  原始Spring Boot   │
│      JAR包         │
└──────────┬─────────┘
           │
           ▼
┌────────────────────┐    ┌────────────────────┐
│ spring-boot-jar-   │    │                    │
│ slim-encrypt工具   ├───►│  提取依赖到libs目录  │
└──────────┬─────────┘    └────────────────────┘
           │
           ▼
┌────────────────────┐
│    瘦身后的JAR包    │
└──────────┬─────────┘
           │
           ▼
┌────────────────────┐
│   XJar加密处理     │
└──────────┬─────────┘
           │
           ▼
┌────────────────────┐
│   加密后的JAR包     │
│     (.xjar)        │
└──────────┬─────────┘
           │
           │  部署
           ▼
┌─────────────────────────────────────────┐
│          运行时环境                      │
│  ┌─────────────────┐ ┌───────────────┐  │
│  │  加密JAR (.xjar) │ │ 提取的依赖库   │  │
│  └────────┬────────┘ └───────┬───────┘  │
│           │                  │          │
│           │                  │          │
│           ▼                  ▼          │
│  ┌─────────────────────────────────┐    │
│  │      PlainTextClassLoader       │    │
│  │    (自定义类加载器)              │    │
│  └─────────────┬─────────────┬─────┘    │
│                │             │          │
│                ▼             ▼          │
│  ┌─────────────────┐ ┌───────────────┐  │
│  │   解密JAR内容    │ │  加载外部依赖  │  │
│  └─────────────────┘ └───────────────┘  │
│                                         │
└─────────────────────────────────────────┘
  1. 使用 spring-boot-jar-slim-encrypt 工具提取原始 JAR 包中的依赖
  2. 将依赖库单独存储在 libs 目录
  3. 对精简后的 JAR 包进行加密
  4. 使用自定义类加载器加载外部依赖
  5. 使用xJar进行解密

项目一spring-boot-custom-classloader

这是一个自定义类加载器项目,它实现了类加载器:

1. JarClassLoader 接口

package com.mangmang;

import java.io.File;

public interface JarClassLoader {
    String JAR_EXTENSION = ".jar";

    /**
     * 从指定目录加载所有JAR文件
     *
     * @param jarDir 包含要加载的JAR文件的目录路径
     * @throws IllegalArgumentException 如果jarDir为null或不存在
     */
    default void loadJar(String jarDir) {
        if (jarDir == null || jarDir.trim().isEmpty()) {
            throw new IllegalArgumentException("JAR目录路径不能为空");
        }

        File directory = new File(jarDir);
        if (!directory.exists() || !directory.isDirectory()) {
            throw new IllegalArgumentException("指定路径不是有效目录: " + jarDir);
        }

        File[] jarFiles = directory.listFiles(this::isJarFile);
        if (jarFiles == null) {
            return;
        }

        for (File jarFile : jarFiles) {
            System.out.println("加载 》" + jarFile.getName());
            scanJarFile(jarFile);
        }
    }

    /**
     * 递归扫描文件或目录以查找JAR文件
     *
     * @param file 要扫描的文件或目录
     * @throws IllegalArgumentException 如果file为null
     */
    default void scanJarFile(File file) {
        if (file == null) {
            throw new IllegalArgumentException("文件不能为null");
        }

        if (!file.exists()) {
            return;
        }

        if (isJarFile(file)) {
            addJARFile(file);
        } else if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (files != null) {
                for (File f : files) {
                    scanJarFile(f);
                }
            }
        }
    }

    /**
     * 检查文件是否为JAR文件
     */
    default boolean isJarFile(File file) {
        return file.isFile() && file.getName().endsWith(JAR_EXTENSION);
    }

    /**
     * 将JAR文件添加到类加载器
     *
     * @param jar 要添加的JAR文件
     * @throws IllegalArgumentException 如果jar为null或不是有效的JAR文件
     */
    void addJARFile(File jar);
}

这是一个接口,定义了 JAR 文件加载的核心方法:

  • loadJar(String jarDir): 从指定目录加载所有 JAR 文件
  • scanJarFile(File file): 递归扫描文件或目录查找 JAR 文件
  • isJarFile(File file): 检查文件是否为 JAR 文件
  • addJARFile(File jar): 将 JAR 文件添加到类加载器

2. PlainTextClassLoader 实现

package com.mangmang;

import org.springframework.boot.context.event.ApplicationStartingEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.lang.NonNull;

import java.io.File;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * 明文类加载器
 * <p>
 * 该类实现了自定义类加载器接口和Spring应用启动事件监听器接口。
 * 主要功能是在应用启动时从JVM启动参数中查找指定路径的JAR文件并加载。
 * 通过反射机制将JAR文件动态添加到当前线程的类加载器中实现运行时加载额外的类库。
 * </p>
 * <p>
 * 使用方法:
 * 1. 在JVM启动参数中添加 -Dexternal.jars.path=你的JAR文件目录路径
 * 2. 系统将自动加载该目录下所有的JAR文件
 * </p>
 * <p>
 * 示例:
 * java -Dexternal.jars.path.path=/path/to/jars -jar your-application.jar
 * </p>
 */
public class PlainTextClassLoader implements JarClassLoader, ApplicationListener<ApplicationStartingEvent> {

    private final String findPath = "external.jars.path";  // 查找路径的键名
    private final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();  // 获取当前线程的类加载器
    private final List<File> jarFiles = new ArrayList<>();  // 存储已加载的JAR文件列表

    /**
     * 构造函数
     * <p>
     * 在初始化时执行以下操作:
     * 1. 设置当前线程的类加载器
     * 2. 输出启动日志信息
     * 3. 从JVM启动参数中检索包含"external.jars.path.path"的参数
     * 4. 提取路径值并调用loadJar方法加载指定目录下的JAR文件
     * </p>
     */
    public PlainTextClassLoader() {
        // 设置当前线程的类加载器
        Thread.currentThread().setContextClassLoader(classLoader);

        // 打印启动信息
        System.out.println("启动自定义明文类加载器");

        // 查找并加载外部JAR文件
        loadExternalJarsFromSystemProperties();
    }

    /**
     * 从系统属性中查找并加载外部JAR文件
     */
    private void loadExternalJarsFromSystemProperties() {
        List<String> inputArguments = ManagementFactory.getRuntimeMXBean().getInputArguments();

        // 查找包含指定路径参数的启动参数
        inputArguments.stream()
                .filter(arg -> arg.contains(findPath))
                .map(this::extractPathFromArgument)
                .filter(Objects::nonNull)
                .forEach(this::loadJar);
    }

    /**
     * 从JVM参数中提取路径值
     *
     * @param argument JVM启动参数
     * @return 提取的路径值如果提取失败则返回null
     */
    private String extractPathFromArgument(String argument) {
        String prefix = "-D" + findPath + "=";
        if (argument.startsWith(prefix)) {
            String path = argument.replace(prefix, "");
            if (!path.isEmpty()) {
                return path;
            }
        }
        return null;
    }

    /**
     * 处理应用程序启动事件
     * <p>
     * 当Spring应用启动时会触发此方法。
     * 目前该方法为空实现,可以在此添加应用启动时需要执行的代码。
     * </p>
     *
     * @param event Spring应用启动事件对象
     */

    @Override
    public void onApplicationEvent(@NonNull ApplicationStartingEvent event) {
        // 应用程序启动事件的处理方法,目前为空
    }

    /**
     * 将JAR文件添加到类加载器
     * <p>
     * 通过反射机制调用URLClassLoader的addURL方法将指定的JAR文件URL添加到当前类加载器。
     * 添加成功后JAR文件中的类可以被当前JVM加载和使用。
     * 同时将已加载的JAR文件记录到jarFiles列表中。
     * </p>
     *
     * @param jar 要添加到类加载器的JAR文件对象
     * @throws RuntimeException 如果添加过程中发生任何异常将抛出RuntimeException
     */
    @Override
    public void addJARFile(File jar) {
        if (jar == null) {
            throw new IllegalArgumentException("JAR文件不能为null");
        }

        try {
            addUrlToClassLoader(jar);
            jarFiles.add(jar);
            System.out.println(jarFiles);
        } catch (Exception e) {
            throw new RuntimeException("添加JAR文件到类加载器失败: " + jar.getName(), e);
        }
    }

    /**
     * 通过反射将JAR文件URL添加到类加载器
     *
     * @param jar 要添加的JAR文件
     * @throws Exception 如果反射操作失败
     */
    private void addUrlToClassLoader(File jar) throws Exception {
        Method addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
        if (!addUrlMethod.isAccessible()) {
            addUrlMethod.setAccessible(true);
        }
        URL jarUrl = jar.toURI().toURL();
        addUrlMethod.invoke(classLoader, jarUrl);
    }

这是一个明文类加载器,实现了 JarClassLoader 接口和 Spring 的 ApplicationListener<ApplicationStartingEvent> 接口,用于在 Spring Boot 应用启动时加载外部 JAR 文件:

主要特点:

  • 在 Spring Boot 应用启动时自动执行
  • 通过 JVM 参数 -Dexternal.jars.path=你的JAR文件目录路径 指定外部 JAR 文件目录
  • 使用反射机制将 JAR 文件 URL 添加到当前线程的类加载器中

使用示例:

java -Dexternal.jars.path=/path/to/jars -jar your-application.jar

3. MANIFEAT.MF

src/main/resources/META-INF/spring.factories

org.springframework.context.ApplicationListener=\
com.mangmang.PlainTextClassLoader

4. pom.xml

<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>
    
    <artifactId>spring-boot-custom-classloader</artifactId>
    <packaging>jar</packaging>
    
    <name>spring-boot-custom-classloader</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.6.7</version>
        </dependency>
    </dependencies>
</project>

项目二spring-boot-jar-slim-encrypt

这个工具用于压缩和加密 Spring Boot JAR 文件,主要包含以下两个核心类:

1. JarUtil 工具类

package com.mangmang;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.FileUtil;
import io.xjar.XCryptos;
import io.xjar.XEncryption;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.compress.utils.Sets;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

public class JarUtil {
    /**
     * 表示UTF-8字符编码的常量。
     * 该变量用于在各种文件操作中强制使用UTF-8编码
     * 确保在整个应用程序中一致地处理文本数据。
     */
    private static final String UTF_8 = "UTF-8";
    /**
     * 常量BOOT_INF_LIB表示JAR文件中通常存储依赖库的默认目录路径。
     * 该路径主要用于在压缩或排除等操作中识别和处理库文件。
     */
    private static final String BOOT_INF_LIB = "BOOT-INF/lib";
    /**
     * 用于标识JARJava归档文件的文件扩展名。
     * 该常量表示JAR文件的标准扩展名通常用于
     * 文件过滤、命名或在目录或归档中识别JAR文件的操作。
     */
    private static final String JAR_EXTENSION = ".jar";
    /**
     * 定义在管理JAR过程中生成的需求文件的后缀
     * 特别是在处理依赖项或排除项时使用。
     * 该字符串用作特定的文件名模式,用于保存与特定服务
     * 相关的排除依赖项或其他需求的列表。
     * 默认值为"-requirements.txt"。
     */
    private static final String REQUIREMENTS_SUFFIX = "-requirements.txt";
    /**
     * 预定义的、不可修改的特定jar文件名集合被视为
     * "安全"或"始终包含"的文件。这些jar文件通常在
     * 处理或压缩操作中免于排除过滤。
     * 该集合包含以下jar标识符
     * - "spring"
     * - "logback-core"
     * - "tomcat"
     * 该变量用于根据jar文件名决定是否包含特定jar文件的操作中。
     * 它作为应用程序关键或必要jar的白名单。
     */
    private static final Set<String> WHITE_LIST_JARS = Sets.newHashSet("spring", "logback-core", "tomcat");

    /**
     * 通过排除和包含指定的条目来压缩给定的源JAR文件并将结果写入目标JAR文件。
     * 它处理源JAR的条目应用排除和包含规则还可以将某些条目提取到指定的目录中。
     * 在此过程中创建一个临时文件成功完成后将其重命名为目标JAR文件。
     *
     * @param serviceName 正在处理的服务名称,主要用于日志记录和创建其他相关文件。
     * @param sourceJar   要压缩的源JAR文件。
     * @param includes    指定应保留哪些条目的包含模式集合。可能会自动添加额外的默认包含项。
     * @param exclusions  指定应排除哪些条目的排除模式集合。
     * @param targetJar   将写入压缩JAR的文件。
     * @param libDir      某些被排除的条目可能被提取到的目录(如适用)。
     */
    public static void compress(String serviceName, File sourceJar, Set<String> includes, Set<String> exclusions, File targetJar, String libDir) {
        includes.addAll(WHITE_LIST_JARS);
        File tempJar = new File(targetJar.getAbsolutePath() + ".tmp");
        Set<String> excludedJars = new HashSet<>();

        if (processJarEntries(sourceJar, tempJar, includes, exclusions, libDir, excludedJars)) {
            finalizeCompression(serviceName, targetJar, tempJar, excludedJars, libDir);
        } else {
            boolean delete = tempJar.delete();
            System.out.println("删除临时文件:{" + delete + "}");
        }
    }

/**
     * 处理源JAR文件中的条目以生成临时JAR文件
     * 同时根据包含和排除规则过滤条目。如果需要,
     * 还会将指定的JAR条目提取到库目录中。
     *
     * @param sourceJar    要处理的源JAR文件
     * @param tempJar      要创建的临时JAR文件
     * @param includes     定义要包含的条目的模式集合
     * @param exclusions   定义要排除的条目的模式集合
     * @param libDir       特定JAR应该被提取到的目录如果不需要提取则为null
     * @param excludedJars 用于存储被排除的JAR条目名称的集合
     * @return 如果处理成功完成则返回true否则返回false
     */
    private static boolean processJarEntries(File sourceJar, File tempJar, Set<String> includes,
                                             Set<String> exclusions, String libDir, Set<String> excludedJars) {
        try (JarFile jar = new JarFile(sourceJar);
             JarOutputStream tempJarStream = new JarOutputStream(Files.newOutputStream(tempJar.toPath()))) {
            for (Enumeration<JarEntry> entries = jar.entries(); entries.hasMoreElements(); ) {
                JarEntry entry = entries.nextElement();
                String entryName = entry.getName();

                if (shouldExcludeEntry(entryName, includes, exclusions)) {
                    if (libDir != null && !libDir.isEmpty()) {
                        extractJarToLib(jar, entry, libDir, excludedJars);
                    }
                    continue;
                }

                copyEntryToJar(jar, entry, tempJarStream);
            }
            return true;
        } catch (Exception ex) {
            System.out.println("处理异常:" + ex.getMessage());
            return false;
        }
    }

    /**
     * 根据预定义的标准确定是否应排除特定的jar条目。
     * 该方法评估条目是否属于BOOT-INF/lib目录是否具有".jar"扩展名,
     * 以及是否不满足由includes和exclusions集合定义的包含/排除条件。
     *
     * @param entryName  要检查的jar条目名称
     * @param includes   jar包含条件的集合
     * @param exclusions jar排除条件的集合
     * @return 如果应排除该条目则返回true否则返回false
     */
    private static boolean shouldExcludeEntry(String entryName, Set<String> includes, Set<String> exclusions) {
        if (!entryName.startsWith(BOOT_INF_LIB)) {
            return false;
        }
        String jarName = entryName.substring(entryName.lastIndexOf("/") + 1);
        return jarName.endsWith(JAR_EXTENSION) && !isWhiteJar(jarName, includes, exclusions);
    }

 /**
     * 从JAR文件中提取指定的JAR条目到指定的库目录。
     * 如果条目对应于JAR文件且在库目录中尚不存在
     * 则将其复制到该目录并将其名称添加到被排除的JAR集合中。
     *
     * @param jar          包含要提取的条目的JAR文件
     * @param entry        要提取的JAR条目
     * @param libDir       提取的JAR文件将被复制到的目录
     * @param excludedJars 处理过程中被排除的JAR文件名的集合
     * @throws IOException 如果在从文件系统读取或写入时发生I/O错误
     */
    private static void extractJarToLib(JarFile jar, JarEntry entry, String libDir,
                                        Set<String> excludedJars) throws IOException {
        String jarName = entry.getName().substring(entry.getName().lastIndexOf("/") + 1);
        File outputFile = new File(libDir, jarName);

        if (!outputFile.exists()) {
            FileUtil.touch(outputFile);
        }

        try (InputStream input = jar.getInputStream(entry);
             FileOutputStream output = new FileOutputStream(outputFile)) {
            IOUtils.copy(input, output);
            excludedJars.add(jarName);
            System.out.println("Excluding: " + outputFile.getAbsolutePath());
        }
    }

    /**
     * 将单个{@link JarEntry}从源{@link JarFile}复制到目标{@link JarOutputStream}。
     *
     * @param jar    包含要复制的条目的源{@link JarFile}
     * @param entry  要复制的{@link JarEntry}
     * @param output 将写入条目的目标{@link JarOutputStream}
     * @throws IOException 如果在复制过程中发生I/O错误
     */
    private static void copyEntryToJar(JarFile jar, JarEntry entry, JarOutputStream output) throws IOException {
        try (InputStream input = jar.getInputStream(entry)) {
            output.putNextEntry(entry);
            IOUtils.copy(input, output);
        }
    }

    /**
     * 通过处理目标和临时JAR文件完成压缩过程
     * 并可选择将排除的JAR列表写入需求文件。
     *
     * @param serviceName  与压缩过程关联的服务名称
     * @param targetJar    要创建或更新的目标JAR文件
     * @param tempJar      压缩过程中使用的临时JAR文件
     * @param excludedJars 压缩过程中排除的JAR文件名的集合
     * @param libDir       存储库文件的目录
     */
    private static void finalizeCompression(String serviceName, File targetJar, File tempJar, Set<String> excludedJars, String libDir) {
        boolean deleteTarget = targetJar.delete();
        System.out.println("删除目标文件结果:" + deleteTarget);
        boolean rename = tempJar.renameTo(targetJar);
        System.out.println("临时文件重命名结果:" + rename);

        if (CollectionUtil.isNotEmpty(excludedJars)) {
            File requirementsFile = new File(libDir, serviceName + REQUIREMENTS_SUFFIX);
            FileUtil.writeLines(excludedJars, requirementsFile, UTF_8);
        }
    }

    /**
     * 确定给定的jar文件名是否匹配任何指定的包含模式
     * 且不是排除集的一部分。
     *
     * @param jarName    要检查的jar文件名
     * @param includes   表示包含模式的字符串集合如果jar名称包含
     *                   这些模式中的任何一个,则被视为匹配
     * @param exclusions 表示要排除的jar名称的字符串集合如果jar名称
     *                   存在于此集合中,则被视为排除
     * @return 如果jar名称匹配任何包含模式且不是
     * 排除集的一部分,则返回{@code true},否则返回{@code false}
     */
    private static boolean isWhiteJar(String jarName, Set<String> includes, Set<String> exclusions) {
        if (exclusions.contains(jarName)) {
            return false;
        }
        for (String include : includes) {
            if (jarName.contains(include)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 使用提供的密码将指定的原始JAR文件加密为加密的JAR文件。
     * 支持包含和排除模式用于选择性地加密JAR中的文件条目。
     *
     * @param rawFile  要加密的输入JAR文件
     * @param xjarFile 输出的加密JAR文件
     * @param pass     用于加密的密码,如果为空则生成默认密码
     * @param includes 指定要包含在加密中的文件条目的包含模式数组
     * @param excludes 指定要从加密中排除的文件条目的排除模式数组
     * @throws Exception 如果在加密过程中发生错误
     */
    public static void encrypt(File rawFile, File xjarFile, String pass, String[] includes, String[] excludes) throws Exception {
        XEncryption xe = XCryptos.encryption().from(rawFile.getAbsolutePath());
        xe.use((pass == null || pass.trim().isEmpty() || pass.startsWith("默认")) ? "0755isa" : pass);
        if (includes != null) {
            for (String include : includes) {
                xe.include(include);
            }
        }
        if (excludes != null) {
            for (String exclude : excludes) {
                xe.exclude(exclude);
            }
        }
        xe.to(xjarFile);
    }
}

这个工具类提供了两个主要功能:

JAR 包压缩功能

compress 方法实现了 JAR 瘦身功能:

  • 根据包含列表和排除列表过滤 JAR 中的依赖
  • 将被排除的依赖提取到指定的库目录
  • 生成一个记录排除依赖的需求文件
  • 创建一个只包含必要依赖的精简 JAR 文件

JAR 包加密功能

encrypt 方法利用 XJar 库实现了 JAR 加密:

  • 支持指定加密密码
  • 通过包含和排除模式选择性地加密 JAR 中的内容
  • 生成加密后的 XJar 文件

2. 主应用

package com.mangmang;

import cn.hutool.core.io.FileUtil;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.SAXException;

import java.io.File;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
 *
 * SpringBoot JAR文件压缩和加密工具的主类。
 * <p>
 * 此类提供了一个基于XJar加密技术的框架用于压缩和加密JAR文件。
 * 它支持根据XML配置文件中的规则包含或排除特定的依赖项并将结果保存为加密的JAR文件。
 * 程序从原始JAR文件开始根据需要压缩它们然后应用加密。
 * </p>
 */
public class SpringBootJarSlimEncryptApplication {

    /**
     * 类的Logger实例用于记录程序执行过程中的各种级别的日志信息。
     */
    private static final Logger LOGGER = Logger.getLogger(SpringBootJarSlimEncryptApplication.class.getName());

    /**
     * 包含程序所有配置常量的内部静态类。
     * 这些常量定义了输入/输出目录、加密设置和其他程序所需的各种配置参数。
     */
    private static class Config {
        /**
         * 存储依赖项XML文件的目录路径。
         * 默认为"./xml/",可通过系统属性"xml.dir"覆盖。
         */
        static final String DEPENDENCY_XML_DIR = System.getProperty("xml.dir", "./config/xml/");

        /**
         * 存储原始JAR文件的目录路径。
         * 默认为"./rawJars/",可通过系统属性"raw.dir"覆盖。
         */
        static final String RAW_JAR_DIR = System.getProperty("raw.dir", "./config/rawJars/");

        /**
         * 存储压缩后JAR文件的目录路径。
         * 默认为"./compressJars/",可通过系统属性"compress.dir"覆盖。
         */
        static final String COMPRESS_JAR_DIR = System.getProperty("compress.dir", "./config/compressJars/");

        /**
         * 存储提取的库文件的目录路径。
         * 默认为"./libs/",可通过系统属性"libs.dir"覆盖。
         */
        static final String LIB_DIR = System.getProperty("libs.dir", "./config/libs/");

        /**
         * 存储加密后的XJar文件的目录路径。
         * 默认为"./xJars/",可通过系统属性"xjar.dir"覆盖。
         */
        static final String X_JAR_DIR = System.getProperty("xjar.dir", "./config/xJars/");

        /**
         * 控制是否启用压缩功能的标志。
         * 默认为true可通过系统属性"compress.enable"覆盖。
         */
        static final boolean COMPRESS_ENABLED = Boolean.parseBoolean(System.getProperty("compress.enable", "true"));

        /**
         * 定义要包含在XJar加密中的文件模式数组。
         * 这些文件将在加密过程中被加密。
         */
        static final String[] X_JAR_INCLUDES = new String[]{"/com/mangmang/**", "*.yaml", "*.yml", "mapper/**.xml"};

        /**
         * 定义要从XJar加密中排除的文件模式数组。
         * 这些文件在加密过程中将保持未加密状态。
         */
        static final String[] X_JAR_EXCLUDES = new String[]{"/com/mangmang/pinyin/**"};

        /**
         * 用于X_JAR文件加密的密码。
         */
        static final String ENCRYPTION_PASSWORD = "0755isa";

        /**
         * 包含要排除的依赖项列表的XML文件的名称。
         */
        static final String EXCLUSIONS_XML = "config/exclusions.xml";

        /**
         * 包含要包含的依赖项列表的XML文件的名称。
         */
        static final String INCLUDES_XML = "config/includes.xml";
    }
	/**
     * 应用程序的主入口点。
     * 4. 处理所有服务,根据需要进行压缩和加密
     * </p>
     *
     * @param args 命令行参数,当前未使用
     */
    public static void main(String[] args) {
        try {
            //1. 确保所有必需的目录存在
            ensureDirectoriesExist();
            //2. 查找所有原始JAR服务
            Set<String> serviceList = findAllRawJarServices();
            //3. 从XML配置文件加载排除和包含的JAR
            Set<String> exclusionJars = loadJarsFromXml(new File(Config.DEPENDENCY_XML_DIR + File.separator + Config.EXCLUSIONS_XML));
            Set<String> includedJars = loadJarsFromXml(new File(Config.DEPENDENCY_XML_DIR + File.separator + Config.INCLUDES_XML));
            processAllServices(serviceList, includedJars, exclusionJars);
        } catch (Exception e) {
            LOGGER.log(Level.SEVERE, "处理JAR文件过程中发生错误", e);
        }
    }

    /**
     * 处理提供的服务列表,对每个服务应用包含和排除规则,然后处理它们。
     * <p>
     * 对于列表中的每个服务,此方法会调用{@link #processService}方法,
     * 并记录任何可能发生的错误。
     * </p>
     *
     * @param serviceList   要处理的服务名称集合
     * @param includedJars  定义要包含的JAR的规则集合
     * @param exclusionJars 定义要排除的JAR的规则集合
     */
    private static void processAllServices(Set<String> serviceList, Set<String> includedJars, Set<String> exclusionJars) {
        for (String service : serviceList) {
            LOGGER.info("开始处理" + service);
            try {
                processService(service, includedJars, exclusionJars);
            } catch (Exception e) {
                LOGGER.log(Level.SEVERE, "处理服务 " + service + " 时发生错误", e);
            }
        }
    }

    /**
     * 处理单个服务,应用压缩(如果启用)和加密操作。
     * <p>
     * 根据配置,此方法将执行以下操作之一:
     * - 如果启用了压缩压缩原始JAR文件然后加密压缩后的JAR
     * - 如果禁用了压缩直接加密原始JAR文件
     * </p>
     *
     * @param service       要处理的服务的名称
     * @param includedJars  要包含在压缩JAR中的JAR文件集合
     * @param exclusionJars 要从压缩JAR中排除的JAR文件集合
     * @throws Exception 如果在处理过程中发生错误
     */
    private static void processService(String service, Set<String> includedJars, Set<String> exclusionJars) throws Exception {
        File rawJarFile = new File(Config.RAW_JAR_DIR + File.separator + service + ".jar");
        File xjarFile = new File(Config.X_JAR_DIR + File.separator + service + ".xjar");

        if (Config.COMPRESS_ENABLED) {
            File compressedJarFile = new File(Config.COMPRESS_JAR_DIR + File.separator + service + "-compress.jar");
            JarUtil.compress(service, rawJarFile, includedJars, exclusionJars, compressedJarFile, Config.LIB_DIR);
            JarUtil.encrypt(compressedJarFile, xjarFile, Config.ENCRYPTION_PASSWORD, Config.X_JAR_INCLUDES, Config.X_JAR_EXCLUDES);
            if (xjarFile.exists()) {
                LOGGER.info("压缩并加密" + service + "成功");
            }
        } else {
            JarUtil.encrypt(rawJarFile, xjarFile, Config.ENCRYPTION_PASSWORD, Config.X_JAR_INCLUDES, Config.X_JAR_EXCLUDES);
            if (xjarFile.exists()) {
                LOGGER.info("加密" + service + "成功");
            }
        }
    }
   
    
    
   	/**
     * 查找RAW_JAR_DIR目录中的所有JAR文件并返回不带.jar扩展名的服务名称集合。
     * <p>
     * 此方法扫描配置的原始JAR目录查找所有以.jar结尾的文件
     * 然后从文件名中删除.jar扩展名以获取服务名称。
     * </p>
     *
     * @return 原始JAR目录中找到的服务名称的集合不带.jar扩展名
     */
    private static Set<String> findAllRawJarServices() {
        File dir = new File(Config.RAW_JAR_DIR);
        File[] files = dir.listFiles();
        if (files == null) {
            return Collections.emptySet();
        }

        return Arrays.stream(files)
                .filter(file -> file.getName().endsWith(".jar"))
                .map(file -> file.getName().replace(".jar", ""))
                .collect(Collectors.toSet());
    }

    /**
     * 确保所有必需的目录存在,如果不存在则创建它们。
     * <p>
     * 此方法检查配置中定义的所有目录,并在必要时创建它们。
     * 这些目录包括:
     * - 依赖项XML目录
     * - 原始JAR目录
     * - 压缩JAR目录
     * - 库目录
     * - X_JAR目录
     * </p>
     */
    private static void ensureDirectoriesExist() {
        String[] dirs = {
                Config.DEPENDENCY_XML_DIR,
                Config.RAW_JAR_DIR,
                Config.COMPRESS_JAR_DIR,
                Config.LIB_DIR,
                Config.X_JAR_DIR
        };

        for (String dir : dirs) {
            File file = new File(dir);
            if (!file.exists()) {
                FileUtil.mkdir(file);
            }
        }
    }


    /**
     * 从XML文件中加载JAR依赖项列表。
     * <p>
     * 此方法解析指定的XML文件查找依赖项元素并提取artifactId和可选的version
     * 以构建JAR文件名列表。
     * </p>
     *
     * @param xmlFile 包含依赖项列表的XML文件
     * @return 从XML文件中提取的JAR名称集合
     * @throws SAXException 如果在解析XML时发生错误
     */
    private static Set<String> loadJarsFromXml(File xmlFile) throws SAXException {
        Set<String> jars = new HashSet<>();
        if (!xmlFile.exists()) {
            return jars;
        }

        SAXReader saxReader = new SAXReader();
        saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        saxReader.setEncoding("UTF-8");

        try {
            Document document = saxReader.read(xmlFile);
            Element rootElement = document.getRootElement();

            if (!rootElement.hasContent()) {
                return jars;
            }

            List<Element> dependencies = rootElement.elements("dependency");
            if (dependencies.isEmpty()) {
                return jars;
            }

            for (Element element : dependencies) {
                Element artifactId = element.element("artifactId");
                String artifactIdText = artifactId.getText();
                Element version = element.element("version");

                String jarName;
                if (Objects.nonNull(version)) {
                    String versionText = version.getText();
                    jarName = artifactIdText + "-" + versionText + ".jar";
                } else {
                    jarName = artifactIdText;
                }
                jars.add(jarName);
            }
        } catch (Exception e) {
            LOGGER.log(Level.WARNING, "解析XML文件 " + xmlFile.getName() + " 时发生错误", e);
        }

        return jars;
    }
}

这是工具的主应用类,提供了完整的命令行接口来处理 JAR 文件的压缩和加密:

主要功能:

  • 通过配置常量定义输入/输出目录、加密设置等
  • 支持从 XML 配置文件加载要包含和排除的依赖项
  • 批量处理多个 JAR 文件
  • 支持通过系统属性覆盖默认配置

主要配置参数:

  • xml.dir: 依赖项 XML 文件目录(默认: ./config/xml/
  • raw.dir: 原始 JAR 文件目录(默认: ./config/rawJars/
  • compress.dir: 压缩后 JAR 文件目录(默认: ./config/compressJars/
  • libs.dir: 提取的库文件目录(默认: ./config/libs/
  • xjar.dir: 加密后的 XJar 文件目录(默认: ./config/xJars/
  • compress.enable: 是否启用压缩功能(默认: true

3. pom.xml

<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>
    
    <artifactId>spring-boot-jar-slim-encrypt</artifactId>
    <packaging>jar</packaging>

    <name>spring-boot-jar-slim-encrypt</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <!-- 设置 jitpack.io 仓库 -->
    <repositories>
        <repository>
            <id>jitpack.io</id>
            <url>https://jitpack.io</url>
        </repository>
    </repositories>

    <!-- 添加 XJar 依赖 -->
    <dependencies>
        <dependency>
            <groupId>com.github.core-lib</groupId>
            <artifactId>xjar</artifactId>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>2.1.4</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-core</artifactId>
            <version>5.8.25</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.0</version>
        </dependency>
    </dependencies>

   <build>
        <plugins>
            <!-- 插件 1: maven-jar-plugin -->
            <!-- 用于构建标准的 JAR 文件(仅包含项目编译输出,不含依赖) -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.0</version>
                <configuration>
                    <!-- 配置 JAR 包的归档文件 -->
                    <archive>
                        <manifest>
                            <!-- 指定可执行 JAR 的主类(含 main 方法的类) -->
                            <!-- 作用:使生成的 JAR 可通过 java -jar 直接运行 -->
                            <mainClass>com.mangmang.SpringBootJarSlimEncryptApplication</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

            <!-- 插件 2: maven-assembly-plugin -->
            <!-- 用于构建包含所有依赖的 "fat jar"(即 "uber jar"),适合独立运行 -->
            <!-- 生成的 JAR 名称通常会附加 "-jar-with-dependencies" 后缀 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.3.0</version>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <!-- 绑定到 Maven 生命周期的 "package" 阶段 -->
                        <!-- 即执行 mvn package 时会触发此插件 -->
                        <phase>package</phase>
                        <goals>
                            <!-- 执行目标single 表示生成单个 JAR 文件 -->
                            <goal>single</goal>
                        </goals>
                        <configuration>
                            <!-- 使用预定义描述符 "jar-with-dependencies" -->
                            <!-- 作用:包含项目所有依赖(包括本地依赖和第三方库) -->
                            <descriptorRefs>
                                <descriptorRef>jar-with-dependencies</descriptorRef>
                            </descriptorRefs>
                            <archive>
                                <manifest>
                                    <!-- 同样指定主类,确保 fat jar 可直接运行 -->
                                    <mainClass>com.mangmang.SpringBootJarSlimEncryptApplication</mainClass>
                                </manifest>
                            </archive>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

使用步骤

步骤 1: 集成自定义类加载器

在你的 Spring Boot 项目中添加自定义类加载器依赖:

<dependency>
    <groupId>com.mangmang</groupId>
    <artifactId>spring-boot-custom-classloader</artifactId>
    <version>1.0.0</version>
</dependency>

步骤 2: 构建 Spring Boot 应用

正常构建你的 Spring Boot 应用:

mvn clean package

步骤 3: 配置依赖排除和包含规则

默认路径为根目录下的./config/xml/

创建两个 XML 文件以定义要排除和包含的依赖项:

exclusions.xml(要排除的依赖):

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<exclusions service="cygsystemweb">
    <!-- https://mvnrepository.com/artifact/xml-apis/xml-apis -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.26</version>
    </dependency>
</exclusions>

includes.xml(要保留的依赖):

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<include service="spring-boot-thin-launcher-1.0-SNAPSHOT">
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>2.10.3</version>
    </dependency>
</include>

步骤 4: 执行 JAR 瘦身和加密工具

将你的 Spring Boot JAR 文件放入 ./config/rawJars/ 目录,然后运行瘦身加密工具:

java -jar spring-boot-jar-slim-encrypt.jar

你可以通过系统属性覆盖默认配置:

java -Dcompress.enable=true -Dlibs.dir=/custom/libs/path -jar spring-boot-jar-slim-encrypt.jar

步骤 5: 部署和运行

1. 精简JAR启动指令

java -Dexternal.jars.path.path=/path/to/jars -jar your-application.jar

2. 加密JAR启动指令

  1. 需要进入xjar.dir(默认: ./config/xJars/)对应目录
  2. 使用go build xjar.go 编译
  3. 启动win10示例
    .\xjar.exe  java  "-Dexternal.jars.path=..\libs" -jar .\spring-boot-thin-launcher-1.0-SNAPSHOT.xjar
    
  4. linux 示例
    ./xjar java -Xms256m -Xmx1024m -Dexternal.jars.path=./libs -jar /path/to/encrypted.jar
    

技术原理解析

JAR 瘦身原理

  1. 扫描 Spring Boot JAR 中的 BOOT-INF/lib 目录
  2. 根据配置的排除和包含规则过滤依赖
  3. 将被排除的依赖提取到外部目录
  4. 创建一个不包含被排除依赖的新 JAR 文件

JAR 加密原理

  1. 使用 XJar 库实现 JAR 文件内容的加密
  2. 只加密指定的文件模式(如 Java 类文件、配置文件等)
  3. 避免加密某些需要保持明文的资源

自定义类加载器原理

  1. 在 Spring Boot 应用启动时初始化自定义类加载器
  2. 扫描指定目录下的 JAR 文件
  3. 使用反射机制将 JAR 文件 URL 添加到当前类加载器

方案优势

  1. 减小 JAR 体积:将大型依赖库外置,显著减小主 JAR 文件体积
  2. 提高安全性:通过加密保护核心业务逻辑和敏感配置
  3. 灵活配置:支持通过 XML 配置和系统属性灵活控制瘦身和加密过程
  4. 无缝集成:与 Spring Boot 应用无缝集成,无需修改应用代码

注意事项

  1. 确保加密密码安全保存,丢失密码将导致无法运行加密的 JAR
  2. 测试瘦身后的应用,确保所有需要的依赖都能正确加载
  3. 部署时必须将提取的依赖库和加密后的 JAR 一起部署
  4. 启动应用时必须指定外部库路径参数

结论

通过结合 JAR 瘦身、JAR 加密和自定义类加载器,我们成功构建了一套完整的 Spring Boot 应用优化和保护方案。这不仅有效减小了部署包的体积,还提高了应用的安全性,为企业级 Spring Boot 应用的部署提供了一种实用的解决方案。

在实际应用中,可以根据具体需求调整配置参数,以达到最佳的平衡点。例如,可以根据应用规模和安全需求调整要排除的依赖和加密的文件模式。

这套工具实现了将原本臃肿的 Spring Boot 应用拆分为核心加密 JAR 和外部依赖库的方案,使得应用部署更加灵活,也为应用分发和更新提供了更多可能性。


注:本方案适用于需要保护核心业务逻辑或减小部署包体积的 Spring Boot 应用。对于简单应用或开源项目,可能不必使用这么复杂的方案。