添加 spring-boot-jar-slim-encrypt 模块,包含 JAR 文件压缩和加密工具

- 实现了 `SpringBootJarSlimEncryptApplication` 用于处理使用 XJar 加密的 JAR 文件,并支持基于 XML 的包含/排除配置。
- 新增 Maven `pom.xml` 文件为新模块并设置必要的依赖(XJar、Dom4j、Hutool)。
- 引入了 `PlainTextClassLoader` 用于外部 JAR 文件的动态类加载。
- 修改根目录下的 `pom.xml` 文件以包含新的模块(`spring-boot-jar-slim-encrypt`、`thin-launcher-demo`、`spring-boot-custom-classloader`)。
- 添加了诸如 `JarUtil` 等工具类,用于处理 JAR 文件的操作和加密。
This commit is contained in:
liujing33
2025-05-13 21:24:32 +08:00
parent ba04a1047b
commit b735e4af1b
14 changed files with 990 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
<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>
<parent>
<groupId>com.mangmang</groupId>
<artifactId>learning-nexus</artifactId>
<version>1.0.0</version>
</parent>
<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>
</project>

View File

@@ -0,0 +1,256 @@
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);
}
}

View File

@@ -0,0 +1,281 @@
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;
}
}