--- icon: mdi:package-variant-closed date: 2025-05-13 category: - 实用工具 - JAVA - SpringBoot - JAR tag: - JAR包瘦身 title: 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 接口 ```java 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 实现 ```java 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; /** * 明文类加载器 *

* 该类实现了自定义类加载器接口和Spring应用启动事件监听器接口。 * 主要功能是在应用启动时,从JVM启动参数中查找指定路径的JAR文件并加载。 * 通过反射机制将JAR文件动态添加到当前线程的类加载器中,实现运行时加载额外的类库。 *

*

* 使用方法: * 1. 在JVM启动参数中添加 -Dexternal.jars.path=你的JAR文件目录路径 * 2. 系统将自动加载该目录下所有的JAR文件 *

*

* 示例: * java -Dexternal.jars.path.path=/path/to/jars -jar your-application.jar *

*/ public class PlainTextClassLoader implements JarClassLoader, ApplicationListener { private final String findPath = "external.jars.path"; // 查找路径的键名 private final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // 获取当前线程的类加载器 private final List jarFiles = new ArrayList<>(); // 存储已加载的JAR文件列表 /** * 构造函数 *

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

*/ public PlainTextClassLoader() { // 设置当前线程的类加载器 Thread.currentThread().setContextClassLoader(classLoader); // 打印启动信息 System.out.println("启动自定义明文类加载器"); // 查找并加载外部JAR文件 loadExternalJarsFromSystemProperties(); } /** * 从系统属性中查找并加载外部JAR文件 */ private void loadExternalJarsFromSystemProperties() { List 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; } /** * 处理应用程序启动事件 *

* 当Spring应用启动时会触发此方法。 * 目前该方法为空实现,可以在此添加应用启动时需要执行的代码。 *

* * @param event Spring应用启动事件对象 */ @Override public void onApplicationEvent(@NonNull ApplicationStartingEvent event) { // 应用程序启动事件的处理方法,目前为空 } /** * 将JAR文件添加到类加载器 *

* 通过反射机制调用URLClassLoader的addURL方法,将指定的JAR文件URL添加到当前类加载器。 * 添加成功后,JAR文件中的类可以被当前JVM加载和使用。 * 同时将已加载的JAR文件记录到jarFiles列表中。 *

* * @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` 接口,用于在 Spring Boot 应用启动时加载外部 JAR 文件: 主要特点: - 在 Spring Boot 应用启动时自动执行 - 通过 JVM 参数 `-Dexternal.jars.path=你的JAR文件目录路径` 指定外部 JAR 文件目录 - 使用反射机制将 JAR 文件 URL 添加到当前线程的类加载器中 使用示例: ```bash java -Dexternal.jars.path=/path/to/jars -jar your-application.jar ``` ### 3. MANIFEAT.MF >src/main/resources/META-INF/spring.factories ```text org.springframework.context.ApplicationListener=\ com.mangmang.PlainTextClassLoader ``` ### 4. pom.xml ```xml 4.0.0 spring-boot-custom-classloader jar spring-boot-custom-classloader http://maven.apache.org UTF-8 org.springframework.boot spring-boot-starter 2.6.7 ``` ## 项目二:spring-boot-jar-slim-encrypt 这个工具用于压缩和加密 Spring Boot JAR 文件,主要包含以下两个核心类: ### 1. JarUtil 工具类 ```java 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"; /** * 用于标识JAR(Java归档)文件的文件扩展名。 * 该常量表示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 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 includes, Set exclusions, File targetJar, String libDir) { includes.addAll(WHITE_LIST_JARS); File tempJar = new File(targetJar.getAbsolutePath() + ".tmp"); Set 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 includes, Set exclusions, String libDir, Set excludedJars) { try (JarFile jar = new JarFile(sourceJar); JarOutputStream tempJarStream = new JarOutputStream(Files.newOutputStream(tempJar.toPath()))) { for (Enumeration 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 includes, Set 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 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 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 includes, Set 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. 主应用 ```java 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文件压缩和加密工具的主类。 *

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

*/ 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. 处理所有服务,根据需要进行压缩和加密 *

* * @param args 命令行参数,当前未使用 */ public static void main(String[] args) { try { //1. 确保所有必需的目录存在 ensureDirectoriesExist(); //2. 查找所有原始JAR服务 Set serviceList = findAllRawJarServices(); //3. 从XML配置文件加载排除和包含的JAR Set exclusionJars = loadJarsFromXml(new File(Config.DEPENDENCY_XML_DIR + File.separator + Config.EXCLUSIONS_XML)); Set 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); } } /** * 处理提供的服务列表,对每个服务应用包含和排除规则,然后处理它们。 *

* 对于列表中的每个服务,此方法会调用{@link #processService}方法, * 并记录任何可能发生的错误。 *

* * @param serviceList 要处理的服务名称集合 * @param includedJars 定义要包含的JAR的规则集合 * @param exclusionJars 定义要排除的JAR的规则集合 */ private static void processAllServices(Set serviceList, Set includedJars, Set exclusionJars) { for (String service : serviceList) { LOGGER.info("开始处理" + service); try { processService(service, includedJars, exclusionJars); } catch (Exception e) { LOGGER.log(Level.SEVERE, "处理服务 " + service + " 时发生错误", e); } } } /** * 处理单个服务,应用压缩(如果启用)和加密操作。 *

* 根据配置,此方法将执行以下操作之一: * - 如果启用了压缩:压缩原始JAR文件,然后加密压缩后的JAR * - 如果禁用了压缩:直接加密原始JAR文件 *

* * @param service 要处理的服务的名称 * @param includedJars 要包含在压缩JAR中的JAR文件集合 * @param exclusionJars 要从压缩JAR中排除的JAR文件集合 * @throws Exception 如果在处理过程中发生错误 */ private static void processService(String service, Set includedJars, Set 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扩展名的服务名称集合。 *

* 此方法扫描配置的原始JAR目录,查找所有以.jar结尾的文件, * 然后从文件名中删除.jar扩展名以获取服务名称。 *

* * @return 原始JAR目录中找到的服务名称的集合(不带.jar扩展名) */ private static Set 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()); } /** * 确保所有必需的目录存在,如果不存在则创建它们。 *

* 此方法检查配置中定义的所有目录,并在必要时创建它们。 * 这些目录包括: * - 依赖项XML目录 * - 原始JAR目录 * - 压缩JAR目录 * - 库目录 * - X_JAR目录 *

*/ 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依赖项列表。 *

* 此方法解析指定的XML文件,查找依赖项元素,并提取artifactId和可选的version, * 以构建JAR文件名列表。 *

* * @param xmlFile 包含依赖项列表的XML文件 * @return 从XML文件中提取的JAR名称集合 * @throws SAXException 如果在解析XML时发生错误 */ private static Set loadJarsFromXml(File xmlFile) throws SAXException { Set 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 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 ```xml 4.0.0 spring-boot-jar-slim-encrypt jar spring-boot-jar-slim-encrypt http://maven.apache.org UTF-8 jitpack.io https://jitpack.io com.github.core-lib xjar 4.0.0 org.dom4j dom4j 2.1.4 cn.hutool hutool-core 5.8.25 org.apache.commons commons-lang3 3.0 org.apache.maven.plugins maven-jar-plugin 3.2.0 com.mangmang.SpringBootJarSlimEncryptApplication org.apache.maven.plugins maven-assembly-plugin 3.3.0 make-assembly package single jar-with-dependencies com.mangmang.SpringBootJarSlimEncryptApplication ``` ## 使用步骤 ### 步骤 1: 集成自定义类加载器 在你的 Spring Boot 项目中添加自定义类加载器依赖: ```xml com.mangmang spring-boot-custom-classloader 1.0.0 ``` ### 步骤 2: 构建 Spring Boot 应用 正常构建你的 Spring Boot 应用: ```bash mvn clean package ``` ### 步骤 3: 配置依赖排除和包含规则 >默认路径为根目录下的./config/xml/ 创建两个 XML 文件以定义要排除和包含的依赖项: **exclusions.xml**(要排除的依赖): ```xml cn.hutool hutool-all 5.8.26 ``` **includes.xml**(要保留的依赖): ```xml com.fasterxml.jackson.core jackson-annotations 2.10.3 ``` ### 步骤 4: 执行 JAR 瘦身和加密工具 将你的 Spring Boot JAR 文件放入 `./config/rawJars/` 目录,然后运行瘦身加密工具: ```bash java -jar spring-boot-jar-slim-encrypt.jar ``` 你可以通过系统属性覆盖默认配置: ```bash java -Dcompress.enable=true -Dlibs.dir=/custom/libs/path -jar spring-boot-jar-slim-encrypt.jar ``` ### 步骤 5: 部署和运行 #### 1. 精简JAR启动指令 ```shell 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示例 ```powershell .\xjar.exe java "-Dexternal.jars.path=..\libs" -jar .\spring-boot-thin-launcher-1.0-SNAPSHOT.xjar ``` 4. linux 示例 ```shell ./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 应用。对于简单应用或开源项目,可能不必使用这么复杂的方案。*