From b735e4af1b33174c8e59b64b6b3348548f71dd2f Mon Sep 17 00:00:00 2001 From: liujing33 <114553116+liujing33@users.noreply.github.com> Date: Tue, 13 May 2025 21:24:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20`spring-boot-jar-slim-encr?= =?UTF-8?q?ypt`=20=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=8C=85=E5=90=AB=20JAR=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=8E=8B=E7=BC=A9=E5=92=8C=E5=8A=A0=E5=AF=86?= =?UTF-8?q?=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了 `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 文件的操作和加密。 --- config/xml/exclusions.xml | 9 + config/xml/includes.xml | 8 + pom.xml | 3 + spring-boot-custom-classloader/pom.xml | 26 ++ .../java/com/mangmang/JarClassLoader.java | 76 +++++ .../com/mangmang/PlainTextClassLoader.java | 146 +++++++++ .../main/resources/META-INF/spring.factories | 2 + spring-boot-jar-slim-encrypt/pom.xml | 108 +++++++ .../src/main/java/com/mangmang/JarUtil.java | 256 ++++++++++++++++ .../SpringBootJarSlimEncryptApplication.java | 281 ++++++++++++++++++ thin-launcher-demo/pom.xml | 48 +++ .../mangmang/ThinLauncherDemoApplication.java | 13 + .../mangmang/controller/HelloController.java | 13 + .../src/main/resources/application.properties | 1 + 14 files changed, 990 insertions(+) create mode 100644 config/xml/exclusions.xml create mode 100644 config/xml/includes.xml create mode 100644 spring-boot-custom-classloader/pom.xml create mode 100644 spring-boot-custom-classloader/src/main/java/com/mangmang/JarClassLoader.java create mode 100644 spring-boot-custom-classloader/src/main/java/com/mangmang/PlainTextClassLoader.java create mode 100644 spring-boot-custom-classloader/src/main/resources/META-INF/spring.factories create mode 100644 spring-boot-jar-slim-encrypt/pom.xml create mode 100644 spring-boot-jar-slim-encrypt/src/main/java/com/mangmang/JarUtil.java create mode 100644 spring-boot-jar-slim-encrypt/src/main/java/com/mangmang/SpringBootJarSlimEncryptApplication.java create mode 100644 thin-launcher-demo/pom.xml create mode 100644 thin-launcher-demo/src/main/java/com/mangmang/ThinLauncherDemoApplication.java create mode 100644 thin-launcher-demo/src/main/java/com/mangmang/controller/HelloController.java create mode 100644 thin-launcher-demo/src/main/resources/application.properties diff --git a/config/xml/exclusions.xml b/config/xml/exclusions.xml new file mode 100644 index 0000000..98e78cc --- /dev/null +++ b/config/xml/exclusions.xml @@ -0,0 +1,9 @@ + + + + + cn.hutool + hutool-all + 5.8.26 + + \ No newline at end of file diff --git a/config/xml/includes.xml b/config/xml/includes.xml new file mode 100644 index 0000000..9f5fd74 --- /dev/null +++ b/config/xml/includes.xml @@ -0,0 +1,8 @@ + + + + com.fasterxml.jackson.core + jackson-annotations + 2.10.3 + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 100bbba..d65fe4d 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,9 @@ spring-cloud-demo swing-and-javafx websocket + thin-launcher-demo + spring-boot-jar-slim-encrypt + spring-boot-custom-classloader diff --git a/spring-boot-custom-classloader/pom.xml b/spring-boot-custom-classloader/pom.xml new file mode 100644 index 0000000..8a3074d --- /dev/null +++ b/spring-boot-custom-classloader/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + com.mangmang + learning-nexus + 1.0.0 + + + spring-boot-custom-classloader + jar + + spring-boot-custom-classloader + http://maven.apache.org + + + UTF-8 + + + + + org.springframework.boot + spring-boot-starter + + + diff --git a/spring-boot-custom-classloader/src/main/java/com/mangmang/JarClassLoader.java b/spring-boot-custom-classloader/src/main/java/com/mangmang/JarClassLoader.java new file mode 100644 index 0000000..6a1e23a --- /dev/null +++ b/spring-boot-custom-classloader/src/main/java/com/mangmang/JarClassLoader.java @@ -0,0 +1,76 @@ +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); +} \ No newline at end of file diff --git a/spring-boot-custom-classloader/src/main/java/com/mangmang/PlainTextClassLoader.java b/spring-boot-custom-classloader/src/main/java/com/mangmang/PlainTextClassLoader.java new file mode 100644 index 0000000..db84dca --- /dev/null +++ b/spring-boot-custom-classloader/src/main/java/com/mangmang/PlainTextClassLoader.java @@ -0,0 +1,146 @@ +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); + } +} diff --git a/spring-boot-custom-classloader/src/main/resources/META-INF/spring.factories b/spring-boot-custom-classloader/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..fc61c8d --- /dev/null +++ b/spring-boot-custom-classloader/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.context.ApplicationListener=\ +com.mangmang.PlainTextClassLoader diff --git a/spring-boot-jar-slim-encrypt/pom.xml b/spring-boot-jar-slim-encrypt/pom.xml new file mode 100644 index 0000000..e50c4ab --- /dev/null +++ b/spring-boot-jar-slim-encrypt/pom.xml @@ -0,0 +1,108 @@ + + 4.0.0 + + com.mangmang + learning-nexus + 1.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 + + + + + + + + + diff --git a/spring-boot-jar-slim-encrypt/src/main/java/com/mangmang/JarUtil.java b/spring-boot-jar-slim-encrypt/src/main/java/com/mangmang/JarUtil.java new file mode 100644 index 0000000..1a1d841 --- /dev/null +++ b/spring-boot-jar-slim-encrypt/src/main/java/com/mangmang/JarUtil.java @@ -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"; + /** + * 用于标识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); + } +} diff --git a/spring-boot-jar-slim-encrypt/src/main/java/com/mangmang/SpringBootJarSlimEncryptApplication.java b/spring-boot-jar-slim-encrypt/src/main/java/com/mangmang/SpringBootJarSlimEncryptApplication.java new file mode 100644 index 0000000..90c44f7 --- /dev/null +++ b/spring-boot-jar-slim-encrypt/src/main/java/com/mangmang/SpringBootJarSlimEncryptApplication.java @@ -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文件压缩和加密工具的主类。 + *

+ * 此类提供了一个基于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; + } +} diff --git a/thin-launcher-demo/pom.xml b/thin-launcher-demo/pom.xml new file mode 100644 index 0000000..f36312c --- /dev/null +++ b/thin-launcher-demo/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + + com.mangmang + learning-nexus + 1.0.0 + + + thin-launcher-demo + jar + thin-launcher-demo + http://maven.apache.org + 1.0.0 + + + UTF-8 + 1.0.31.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + com.github.pagehelper + pagehelper + 5.2.1 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + diff --git a/thin-launcher-demo/src/main/java/com/mangmang/ThinLauncherDemoApplication.java b/thin-launcher-demo/src/main/java/com/mangmang/ThinLauncherDemoApplication.java new file mode 100644 index 0000000..be329f3 --- /dev/null +++ b/thin-launcher-demo/src/main/java/com/mangmang/ThinLauncherDemoApplication.java @@ -0,0 +1,13 @@ +package com.mangmang; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ThinLauncherDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(ThinLauncherDemoApplication.class, args); + } + +} diff --git a/thin-launcher-demo/src/main/java/com/mangmang/controller/HelloController.java b/thin-launcher-demo/src/main/java/com/mangmang/controller/HelloController.java new file mode 100644 index 0000000..b967746 --- /dev/null +++ b/thin-launcher-demo/src/main/java/com/mangmang/controller/HelloController.java @@ -0,0 +1,13 @@ +package com.mangmang.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + @GetMapping("/") + public String hello() { + return "Hello from Spring Boot Thin Launcher Demo!"; + } +} diff --git a/thin-launcher-demo/src/main/resources/application.properties b/thin-launcher-demo/src/main/resources/application.properties new file mode 100644 index 0000000..bff116d --- /dev/null +++ b/thin-launcher-demo/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=8055 \ No newline at end of file