JustryDeng пре 5 година
родитељ
комит
348034b47f
35 измењених фајлова са 4147 додато и 0 уклоњено
  1. 2 0
      .gitignore
  2. 51 0
      class-winter-core/pom.xml
  3. 93 0
      class-winter-core/src/main/java/net/jd/classwinter/Forward.java
  4. 146 0
      class-winter-core/src/main/java/net/jd/classwinter/Reverses.java
  5. 33 0
      class-winter-core/src/main/java/net/jd/classwinter/author/JustryDeng.java
  6. 35 0
      class-winter-core/src/main/java/net/jd/classwinter/exception/ClassWinterException.java
  7. 274 0
      class-winter-core/src/main/java/net/jd/classwinter/executor/DecryptExecutor.java
  8. 792 0
      class-winter-core/src/main/java/net/jd/classwinter/executor/EncryptExecutor.java
  9. 4 0
      class-winter-core/src/main/java/net/jd/classwinter/pakage-info.java
  10. 182 0
      class-winter-core/src/main/java/net/jd/classwinter/util/BashUtil.java
  11. 36 0
      class-winter-core/src/main/java/net/jd/classwinter/util/Cache.java
  12. 150 0
      class-winter-core/src/main/java/net/jd/classwinter/util/Constant.java
  13. 256 0
      class-winter-core/src/main/java/net/jd/classwinter/util/EncryptUtil.java
  14. 42 0
      class-winter-core/src/main/java/net/jd/classwinter/util/ExceptionUtil.java
  15. 444 0
      class-winter-core/src/main/java/net/jd/classwinter/util/IOUtil.java
  16. 328 0
      class-winter-core/src/main/java/net/jd/classwinter/util/JarUtil.java
  17. 80 0
      class-winter-core/src/main/java/net/jd/classwinter/util/JavaagentCmdArgs.java
  18. 311 0
      class-winter-core/src/main/java/net/jd/classwinter/util/JavassistUtil.java
  19. 67 0
      class-winter-core/src/main/java/net/jd/classwinter/util/Logger.java
  20. 60 0
      class-winter-core/src/main/java/net/jd/classwinter/util/Pair.java
  21. 46 0
      class-winter-core/src/main/java/net/jd/classwinter/util/PathUtil.java
  22. 125 0
      class-winter-core/src/main/java/net/jd/classwinter/util/RsaUtil.java
  23. 166 0
      class-winter-core/src/main/java/net/jd/classwinter/util/StrUtil.java
  24. 91 0
      class-winter-core/src/test/java/com/jd/classwinter/ForwardTest.java
  25. 58 0
      class-winter-core/src/test/java/com/jd/classwinter/ProtectedLibTest.java
  26. 91 0
      class-winter-core/src/test/java/com/jd/classwinter/ReversesTest.java
  27. BIN
      class-winter-core/src/test/resources/boot-jar.jar
  28. BIN
      class-winter-core/src/test/resources/my-project-with-encrypted-lib-have-pwd.jar
  29. BIN
      class-winter-core/src/test/resources/my-project-with-encrypted-lib-no-pwd.jar
  30. BIN
      class-winter-core/src/test/resources/my-project-with-normal-lib.jar
  31. BIN
      class-winter-core/src/test/resources/normal-jar.jar
  32. BIN
      class-winter-core/src/test/resources/war-project.war
  33. 59 0
      class-winter-maven-plugin/pom.xml
  34. 102 0
      class-winter-maven-plugin/src/main/java/net/jd/classwinter/plugin/ClassWinterPlugin.java
  35. 23 0
      pom.xml

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+*.iml
+.idea

+ 51 - 0
class-winter-core/pom.xml

@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>class-winter</artifactId>
+        <groupId>com.idea-aedi</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>class-winter-core</artifactId>
+
+    <dependencies>
+        <!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
+        <dependency>
+            <groupId>org.javassist</groupId>
+            <artifactId>javassist</artifactId>
+            <version>3.27.0-GA</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>3.0.0</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <transformers>
+                                <transformer
+                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                                    <manifestEntries>
+                                        <Main-Class>net.jd.classwinter.Forward</Main-Class>
+                                        <Premain-Class>net.jd.classwinter.Reverses</Premain-Class>
+                                    </manifestEntries>
+                                </transformer>
+                            </transformers>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 93 - 0
class-winter-core/src/main/java/net/jd/classwinter/Forward.java

@@ -0,0 +1,93 @@
+package net.jd.classwinter;
+
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.executor.EncryptExecutor;
+import net.jd.classwinter.util.Logger;
+import net.jd.classwinter.util.StrUtil;
+
+import java.util.Objects;
+
+/**
+ * 正向加密
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/5/6 23:25:24
+ */
+public class Forward {
+    
+    /**
+     * 入口
+     *
+     * 假设是这么java -jar -class-winter-core-1.0.0.jar k1=v1 k2=v2 k3=v3  启动项目的,
+     * 那么args的内容形如["k1=v1", "k2=v2", "k3=v3"]
+     */
+    public static void main(String[] args) {
+        // 解析参数
+        String originJarOrWar = parseValueByPrefixFromTail("originJarOrWar=", args);
+        String includePrefix = parseValueByPrefixFromTail("includePrefix=", args);
+        String excludePrefix = parseValueByPrefixFromTail("excludePrefix=", args);
+        String finalName = parseValueByPrefixFromTail("finalName=", args);
+        String includeLibs = parseValueByPrefixFromTail("includeLibs=", args);
+        String alreadyProtectedLibs = parseValueByPrefixFromTail("alreadyProtectedLibs=", args);
+        String debug = parseValueByPrefixFromTail("debug=", args);
+        String tips = parseValueByPrefixFromTail("tips=", args);
+        
+        Logger.ENABLE_DEBUG.set(Boolean.parseBoolean(debug));
+        Logger.debug(Forward.class, "You input arg originJarOrWar -> " + originJarOrWar);
+        Logger.debug(Forward.class, "You input arg includePrefix -> " + includePrefix);
+        Logger.debug(Forward.class, "You input arg excludePrefix -> " + excludePrefix);
+        Logger.debug(Forward.class, "You input arg finalName -> " + finalName);
+        Logger.debug(Forward.class, "You input arg includeLibs -> " + includeLibs);
+        Logger.debug(Forward.class, "You input arg alreadyProtectedLibs -> " + alreadyProtectedLibs);
+        Logger.debug(Forward.class, "You input arg tips -> " + tips);
+        Logger.debug(Forward.class, "You input arg debug -> " + debug);
+        
+        // 构造加密执行器
+        EncryptExecutor encryptExecutor = EncryptExecutor.builder()
+                .originJarOrWar(originJarOrWar)
+                .includePrefix(includePrefix)
+                .finalName(finalName)
+                .excludePrefix(excludePrefix)
+                .includeLibs(includeLibs)
+                .alreadyProtectedLibs(alreadyProtectedLibs)
+                .debug(Boolean.parseBoolean(debug))
+                .tips(tips)
+                .build();
+    
+        Logger.debug(Forward.class, "The encrypted executor generated based on your input is -> " + encryptExecutor);
+        // 执行加密
+        String generatedJar = encryptExecutor.process();
+        // 打印加深后生成的jar的全路径
+        Logger.info(Forward.class, "The absolute path of the obfuscated jar is [" + generatedJar + "]");
+    }
+    
+    /**
+     * 根据前缀解析值
+     * <p>
+     *     如: 数组中某元素的为 k1=v1, 此方法传入的前缀为k1=, 那么此方法会返回v1
+     * </p>
+     *
+     * @param prefix
+     *            前缀
+     * @param args
+     *            参数数组
+     * @return  解析出来的值,若没有则返回null
+     */
+    private static String parseValueByPrefixFromTail(String prefix, String[] args) {
+        Objects.requireNonNull(prefix, "prefix cannot be null.");
+        if (args == null) {
+            return null;
+        }
+        // 从后往前找,即: java -jar k1=v1 k2=v2 k3=v3 -xxx.jar中,若k冲突了,那么取后面的k对应的v
+        for (int i = args.length - 1; i >= 0; i--) {
+            if (args[i] == null || StrUtil.isBlank(args[i])) {
+                continue;
+            }
+            args[i] = args[i].trim();
+            if (args[i].startsWith(prefix)) {
+                return args[i].substring(prefix.length());
+            }
+        }
+        return null;
+    }
+}

+ 146 - 0
class-winter-core/src/main/java/net/jd/classwinter/Reverses.java

@@ -0,0 +1,146 @@
+package net.jd.classwinter;
+
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.executor.DecryptExecutor;
+import net.jd.classwinter.util.Cache;
+import net.jd.classwinter.util.Constant;
+import net.jd.classwinter.util.ExceptionUtil;
+import net.jd.classwinter.util.IOUtil;
+import net.jd.classwinter.util.JarUtil;
+import net.jd.classwinter.util.JavaagentCmdArgs;
+import net.jd.classwinter.util.Logger;
+import net.jd.classwinter.util.StrUtil;
+
+import java.io.File;
+import java.lang.instrument.ClassFileTransformer;
+import java.lang.instrument.Instrumentation;
+import java.nio.charset.StandardCharsets;
+import java.security.ProtectionDomain;
+
+/**
+ * 反向解密(Agent类)
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/5/6 22:38:36
+ */
+public class Reverses {
+    
+    private static JavaagentCmdArgs javaagentCmdArgs  = null;
+    
+    /**
+     * pre-main 入口函数
+     *
+     * @param args
+     *            参数
+     * @param instrumentation
+     *            This class provides services needed to instrumentation Java programming language code
+     */
+    public static void premain(String args, Instrumentation instrumentation) {
+        if (javaagentCmdArgs == null) {
+            javaagentCmdArgs = JavaagentCmdArgs.parseJavaagentCmdArgs(args);
+            // 是否启动debug,同步给Logger
+            Logger.ENABLE_DEBUG.set(javaagentCmdArgs.isDebug());
+        }
+        
+        Logger.debug(Reverses.class, "parse raw args [" + args + "] to javaagentCmdArgs -> " + javaagentCmdArgs);
+        
+        // 在JVM加载class字节码之前,通过ClassFileTransformer修改字节码
+        if (instrumentation != null) {
+            /*
+             * 特别注意: 并不是说jar包中的所有class都会走到下面的逻辑中。
+             *          只有jar包中被用到的class才会走到下面的逻辑中,不被使用的class是不会走到下面的逻辑的。
+             *          也就是说: 如果加密时,加密了一个根本没有使用的class,那么该javaagent加载时,该class根本不会走到下面的逻辑中,进而不会走解密逻辑。
+             */
+            instrumentation.addTransformer(new ClassFileTransformer() {
+                @Override
+                public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
+                    if (className == null || protectionDomain == null || loader == null) {
+                        return classfileBuffer;
+                    }
+                    // 获取类所在的项目运行路径
+                    String projectPath = protectionDomain.getCodeSource().getLocation().getPath();
+                    projectPath = JarUtil.getRootPath(projectPath);
+                    if (StrUtil.isEmpty(projectPath)) {
+                        return classfileBuffer;
+                    }
+                    className = className.replace("/", ".").replace("\\", ".");
+    
+                    // 本项目的印章
+                    if (Cache.sealCache == null) {
+                        byte[] sealByte = IOUtil.readFileFromWorkbenchRoot(new File(projectPath), Constant.SEAL_FILE);
+                        String sealContent = new String(sealByte, StandardCharsets.UTF_8);
+                        Logger.debug(DecryptExecutor.class, "sealContent " + sealContent);
+                        if (StrUtil.isBlank(sealContent)) {
+                            Logger.error(Reverses.class, "Obtain project seal fail.");
+                            // 结束程序
+                            System.exit(-1);
+                        }
+                        Cache.sealCache = sealContent;
+                    }
+                    
+                    // 判断是否应该解密
+                    if (DecryptExecutor.checklistContain(projectPath, className) && DecryptExecutor.verifySeal(classfileBuffer)) {
+                        //noinspection DuplicatedCode
+                        try {
+                            return DecryptExecutor.process(projectPath, null, className, javaagentCmdArgs.getPassword() == null ? null :
+                                    javaagentCmdArgs.getPassword().toCharArray());
+                        } catch (Exception e) {
+                            if (javaagentCmdArgs.isDebug()) {
+                                Logger.error(Reverses.class, ExceptionUtil.getStackTraceMessage(e));
+                            }
+                            Logger.error(Reverses.class, "decrypt class[" + className + "] fail. "
+                                    + (StrUtil.isEmpty(javaagentCmdArgs.getPassword()) ? e.getMessage() : "please ensure your password is correct."));
+                            System.exit(-1);
+                            // 上一步System.exit(-1)就退出程序了,照理说是不会走到下面这里的(,不过为了以防万一,这里打出提醒, class-winter失效)
+                            for (int i = 0; i < 10; i++) {
+                                Logger.error(Reverses.class, "!!!!!!!!!!! class-winter Invalidation. !!!!!!!!!!!");
+                            }
+                            return null;
+                        }
+                    } else if (DecryptExecutor.checklistOfAllLibsContain(projectPath, className)
+                            && DecryptExecutor.verifyLibSeal(projectPath, className, classfileBuffer)) {
+                        String classWinterInfoDir = DecryptExecutor.getLibDirRelativePath(className);
+                        //noinspection DuplicatedCode
+                        try {
+                            // lib中的密码在加密时,都统一处理好了,这里直接传null
+                            return DecryptExecutor.process(projectPath, classWinterInfoDir, className, null);
+                        } catch (Exception e) {
+                            if (javaagentCmdArgs.isDebug()) {
+                                Logger.error(Reverses.class, ExceptionUtil.getStackTraceMessage(e));
+                            }
+                            String lib = parseLib(classWinterInfoDir);
+                            Logger.error(Reverses.class, "decrypt class[" + className + "] fail. \nPlease check:\n"
+                                    + "\t1. Ensure 'Your lib " + lib + " need a input password ?'\n"
+                                    + "\t2. Ensure 'Your lib "+ lib +"'s password is correct ?'");
+                            System.exit(-1);
+                            // 上一步System.exit(-1)就退出程序了,照理说是不会走到下面这里的(,不过为了以防万一,这里打出提醒, class-winter失效)
+                            for (int i = 0; i < 10; i++) {
+                                Logger.error(Reverses.class, "!!!!!!!!!!! class-winter Invalidation. !!!!!!!!!!!");
+                            }
+                            return null;
+                        }
+                    } else {
+                        return classfileBuffer;
+                    }
+                }
+                
+                /**
+                 * 根据classWinterInfoDir解析lib名称
+                 * <p>
+                 *     如: META-INF/winter/abc-1.0.0_jar/解析出来的lib为abc-1.0.0.jar
+                 * </p>
+                 *
+                 * @param classWinterInfoDir
+                 *            lib对应的class-winter信息存储目录, 形如META-INF/winter/abc-1.0.0_jar/
+                 * @return lib名称
+                 */
+                private String parseLib(String classWinterInfoDir) {
+                    classWinterInfoDir = classWinterInfoDir.replace(Constant.DEFAULT_ENCRYPTED_CLASSES_SAVE_DIR, "");
+                    return classWinterInfoDir.replace("_jar/", Constant.JAR_SUFFIX);
+                }
+            });
+        }
+    }
+    
+
+}

+ 33 - 0
class-winter-core/src/main/java/net/jd/classwinter/author/JustryDeng.java

@@ -0,0 +1,33 @@
+package net.jd.classwinter.author;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 这是一个自恋的注解
+ *
+ * @author {@link JustryDeng}
+ * @date 2020/4/24 0:25:59
+ */
+@SuppressWarnings("all")
+@Target(value = {ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface JustryDeng {
+
+    /** 姓名 */
+    String[] name() default {"邓沙利文", "亨得帅", "JustryDeng", "邓二洋", "邓帅"};
+
+    /** 座右铭 */
+    String motto() default "我是一只小小小小鸟~嗷!嗷!";
+
+    /** 邮箱 */
+    String[] email() default {"13548417409@163.com", "dengeryanger@gmail.com"};
+
+    /** 微信公众号 */
+    String WECHAT_PUBLIC_NO() default "Java你我他";
+
+    /** 好好学习,天天向上 */
+    String[] DAY_DAY_UP() default {"https://blog.csdn.net/justry_deng", "https://github.com/JustryDeng?tab=repositories"};
+}

+ 35 - 0
class-winter-core/src/main/java/net/jd/classwinter/exception/ClassWinterException.java

@@ -0,0 +1,35 @@
+package net.jd.classwinter.exception;
+
+import net.jd.classwinter.author.JustryDeng;
+
+/**
+ * 代码混淆异常
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/4/23 20:47:24
+ */
+public class ClassWinterException extends RuntimeException{
+    
+    public ClassWinterException() {
+        super();
+    }
+    
+    public ClassWinterException(String message) {
+        super(message);
+    }
+    
+    public ClassWinterException(String message, Throwable cause) {
+        super(message, cause);
+    }
+    
+    public ClassWinterException(Throwable cause) {
+        super(cause);
+    }
+    
+    protected ClassWinterException(String message, Throwable cause,
+                               boolean enableSuppression,
+                               boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+    
+}

+ 274 - 0
class-winter-core/src/main/java/net/jd/classwinter/executor/DecryptExecutor.java

@@ -0,0 +1,274 @@
+package net.jd.classwinter.executor;
+
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.exception.ClassWinterException;
+import net.jd.classwinter.util.Cache;
+import net.jd.classwinter.util.Constant;
+import net.jd.classwinter.util.EncryptUtil;
+import net.jd.classwinter.util.IOUtil;
+import net.jd.classwinter.util.Logger;
+import net.jd.classwinter.util.StrUtil;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * java class解密
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/4/28 22:07:51
+ */
+public class DecryptExecutor {
+    
+    /** 要解密的文件清单,记录已加密的class的全类名的清单文件 */
+    private static Set<String> checklist;
+    
+    /**
+     * 以key-value的形式记录lib中的被混淆的类(key-被混淆的类的无后缀全类名, value-存放采集到的类所在lib的信息的文件夹)。
+     *
+     * key   - com.abc.xyz.Qwer
+     * value - META-INF/winter/abc-1.0.0_jar/
+     */
+    private static Map<String, String> checklistOfAllLibsMap;
+
+    
+    /**
+     * 根据名称解密出一个文件(,得到该文件的byte[])
+     *
+     * @param projectPath
+     *            jar/war文件或目录(如: /tmp/, /tmp/abc.jar等)
+     * @param classWinterInfoDir
+     *            class-winter相关信息的存储目录(如果不指定,则走默认的目录)
+     * @param classLongName
+     *            class全类名(如: com。niantou.winter.core.Abc)
+     * @param password
+     *            密码
+     * @return  解压后的字节(null-表示classLongName所代表的文件不应该被解密)
+     */
+    public static byte[] process(String projectPath, String classWinterInfoDir, String classLongName, char[] password) {
+        classWinterInfoDir = StrUtil.isBlank(classWinterInfoDir) ? Constant.DEFAULT_ENCRYPTED_CLASSES_SAVE_DIR : classWinterInfoDir;
+        // 读取文件
+        byte[] bytes = IOUtil.readFileFromWorkbenchRoot(new File(projectPath), classWinterInfoDir + classLongName);
+        // 检查密码一致性
+        boolean userIfInputPwdWhileDecrypt = checkPwdConsistency(projectPath, classWinterInfoDir, password);
+        // 解密
+        return EncryptUtil.decrypt(bytes, userIfInputPwdWhileDecrypt ? password :
+                obtainAutoGeneratedPwdWhileEncrypt(projectPath, classWinterInfoDir));
+    }
+    
+    /**
+     * 判断classLongName是否应该被解密
+     * <p>
+     *     即: 若classLongName对应的类在加密清单中,说明该文件是加密后的文件,那么就需要对其进行解密;否者不需要进行解密。
+     * </p>
+     *
+     * @param projectPath
+     *            jar/war文件或目录(如: /tmp/, /tmp/abc.jar等)
+     * @param classLongName
+     *            class全类名(如: com。niantou.winter.core.Abc)
+     * @return classLongName是否在(记录已加密类的)清单列表中
+     */
+    public static boolean checklistContain(String projectPath, String classLongName) {
+        if (checklist == null) {
+            byte[] checklistByte = IOUtil.readFileFromWorkbenchRoot(new File(projectPath),
+                    Constant.ALREADY_ENCRYPTED_CLASS_CHECKLIST_CLASSES_SAVE_FILE);
+            if (checklistByte == null) {
+                checklist = new HashSet<>();
+                Logger.debug(DecryptExecutor.class, "checklistByte is null. ");
+            } else {
+                String checklistContent = new String(checklistByte, StandardCharsets.UTF_8);
+                Logger.debug(DecryptExecutor.class, "checklistContent " + checklistContent);
+                checklist = new HashSet<>(Arrays.asList(checklistContent.split(Constant.COMMA)));
+            }
+        }
+        return checklist.contains(classLongName);
+    }
+    
+    /**
+     * 判断classLongName是否应该被解密(判断依据是: 本项目所依赖的所有lib包中的checklist,如果有的话)
+     *
+     * @param projectPath
+     *            jar/war文件或目录(如: /tmp/, /tmp/abc.jar等)
+     * @param classLongName
+     *            无后缀的class全类名(如: com。niantou.winter.core.Abc)
+     * @return classLongName是否在(记录已加密类的)清单列表中
+     */
+    public static boolean checklistOfAllLibsContain(String projectPath, String classLongName) {
+        if (checklistOfAllLibsMap == null) {
+            byte[] checklistOfAllLibsByte = IOUtil.readFileFromWorkbenchRoot(new File(projectPath),
+                    Constant.CHECKLIST_OF_ALL_LIBS);
+            if (checklistOfAllLibsByte == null) {
+                checklistOfAllLibsMap = new HashMap<>(1);
+                Logger.debug(DecryptExecutor.class, "checklistOfAllLibsByte is null. ");
+            } else {
+                checklistOfAllLibsMap = new HashMap<>(128);
+                String checklistOfAllLibsContent = new String(checklistOfAllLibsByte, StandardCharsets.UTF_8);
+                Logger.debug(DecryptExecutor.class, "checklistOfAllLibsContent " + checklistOfAllLibsContent);
+                Arrays.stream(checklistOfAllLibsContent.split(Constant.LINE_SEPARATOR))
+                        .filter(str -> !StrUtil.isBlank(str))
+                        .forEach(info -> {
+                            int idx = info.indexOf("=");
+                            if (idx > 0) {
+                                // libDirRelativePath形如: META-INF/winter/abc-1.0.0_jar/
+                                String libDirRelativePath = info.substring(0, idx);
+                                String checklist = info.substring(idx + 1);
+                                if (StrUtil.isBlank(checklist)) {
+                                    throw new ClassWinterException("lib["
+                                            + libDirRelativePath.substring(0, libDirRelativePath.length() - 4) + Constant.JAR_SUFFIX
+                                            + "]'s checklist is blank.");
+                                }
+                                Arrays.stream(checklist.split(","))
+                                        .map(String::trim)
+                                        .forEach(nonSuffixClassLongName -> {
+                                            checklistOfAllLibsMap.put(nonSuffixClassLongName, libDirRelativePath);
+                                        });
+                            } else {
+                                throw new ClassWinterException("checklistOfAllLibsContent Incorrect format.");
+                            }
+                        });
+            }
+
+        }
+        return checklistOfAllLibsMap.containsKey(classLongName);
+    }
+    
+    /**
+     * 根据类名获取,其所在lib对应的class-winter信息存储目录
+     *
+     * @param classLongName
+     *            无后缀全类名(形如 com.abc.xyz.Qwer)
+     * @return  存放采集到的类所在lib的信息的文件夹(形如 META-INF/winter/abc-1.0.0_jar/)
+     */
+    public static String getLibDirRelativePath(String classLongName) {
+        Objects.requireNonNull(checklistOfAllLibsMap);
+        return checklistOfAllLibsMap.get(classLongName);
+    }
+    
+    /**
+     * 判断类classBytes是否被盖章
+     *
+     * @param classBytes
+     *            类的字节码
+     * @return classBytes中是否存在加密印章
+     */
+    public static boolean verifySeal(byte[] classBytes) {
+        return new String(classBytes, StandardCharsets.UTF_8).contains(Cache.sealCache);
+    }
+    
+    /**
+     * 判断类classBytes是否被盖章
+     *
+     * @param projectPath
+     *            jar/war文件或目录(如: /tmp/, /tmp/abc.jar等)
+     * @param classLongName
+     *            无后缀的class全类名(如: com。niantou.winter.core.Abc)
+     * @param classBytes
+     *            类的字节码
+     * @return classBytes中是否存在加密印章
+     */
+    public static boolean verifyLibSeal(String projectPath, String classLongName, byte[] classBytes) {
+        if (Cache.libSealCache == null) {
+            Cache.libSealCache = new HashMap<>(8);
+            checklistOfAllLibsMap.values().forEach(libDirRelativePath -> {
+                byte[] sealByte = IOUtil.readFileFromWorkbenchRoot(new File(projectPath),
+                        libDirRelativePath + Constant.SEAL_FILE_SIMPLE_NAME);
+                if (sealByte == null) {
+                    throw new ClassWinterException("Lib " + libDirRelativePath.substring(0, libDirRelativePath.length() - 4)
+                            + Constant.JAR_SUFFIX + " missing seal information.");
+                } else {
+                    Cache.libSealCache.put(libDirRelativePath, new String(sealByte, StandardCharsets.UTF_8));
+                }
+            });
+        }
+    
+        // 当且仅当checklistOfAllLibsMap.containsKey(classLongName)为true时才会调用此方法,所以这里libDirRelativePath一定有值的
+        String libDirRelativePath = getLibDirRelativePath(classLongName);
+    
+        String sealContent = Cache.libSealCache.get(libDirRelativePath);
+        Logger.debug(DecryptExecutor.class, "sealContent(from lib) " + sealContent);
+        return new String(classBytes, StandardCharsets.UTF_8).contains(sealContent);
+    }
+    
+    /**
+     * 检查密码一致性,即: 当加密时,用户输入了密码,那么解密时就要求用户输入密码
+     *
+     * @param projectPath
+     *            jar/war文件或目录(如: /tmp/, /tmp/abc.jar等)
+     * @param classWinterInfoDir
+     *            class-winter相关信息的存储目录
+     * @param password
+     *            密码
+     *
+     * @return 在启动项目时(解密时),用户是否输入了密码
+     */
+    private static boolean checkPwdConsistency(String projectPath, String classWinterInfoDir, char[] password) {
+        // 查看加密时,用户是否主动输入了密码
+        boolean userIfInputPwdWhileEncrypt = Boolean.parseBoolean(
+                new String(IOUtil.readFileFromWorkbenchRoot(new File(projectPath),
+                classWinterInfoDir + Constant.USER_IF_INPUT_PWD_SIMPLE_NAME))
+        );
+        // 解密时,用户是否主动输入了密码
+        boolean userIfInputPwdWhileDecrypt = !StrUtil.isEmpty(password);
+        // 如果加密时输入了密码,那么解密时就要求用户输入密码
+        if (userIfInputPwdWhileEncrypt && !userIfInputPwdWhileDecrypt) {
+            throw new ClassWinterException("Please input password while starting project.");
+        }
+        return userIfInputPwdWhileDecrypt;
+    }
+    
+    /**
+     * 获取class-winter加密时自动生成的密码
+     *
+     * @param projectPath
+     *            jar/war文件或目录(如: /tmp/, /tmp/abc.jar等)
+     * @param classWinterInfoDir
+     *            class-winter相关信息的存储目录
+     * @return  密码
+     */
+    private static char[] obtainAutoGeneratedPwdWhileEncrypt(String projectPath, String classWinterInfoDir) {
+        // 项目本身
+        if (classWinterInfoDir.equals(Constant.DEFAULT_ENCRYPTED_CLASSES_SAVE_DIR)) {
+            if (Cache.passwordCache != null) {
+                // 从缓存读取
+                return Cache.passwordCache;
+            }
+            // 读取文件
+            byte[] passwordByte = IOUtil.readFileFromWorkbenchRoot(new File(projectPath), Constant.PWD_WINTER);
+            if (passwordByte == null) {
+                // jar包中没有Constant.PWD_WINTER指向的文件,说明在加密时,用户主动指定了密码,没有采用自动生成密码的机制
+                throw new ClassWinterException("please input password.");
+                
+            }
+            // 存时,是加密存进去的; 这里读取时,解密一下
+            passwordByte = EncryptUtil.decrypt(new String(passwordByte, StandardCharsets.UTF_8),
+                    Cache.sealCache.toCharArray()).getBytes(StandardCharsets.UTF_8);
+            String password = new String(passwordByte, StandardCharsets.UTF_8);
+            char[] passwordCharArr = password.toCharArray();
+            // 放入缓存
+            Cache.passwordCache = passwordCharArr;
+            return passwordCharArr;
+        } else {
+            // lib包
+            if (Cache.libPasswordCache != null) {
+                // 从缓存读取
+                return Cache.libPasswordCache.get(classWinterInfoDir);
+            }
+            Cache.libPasswordCache = new HashMap<>(8);
+            // 读取文件
+            byte[] passwordByte = IOUtil.readFileFromWorkbenchRoot(new File(projectPath), classWinterInfoDir + Constant.PWD_WINTER_SIMPLE_NAME);
+            // 在加密时就将lib的密码写入了的,这里不可能为null的
+            Objects.requireNonNull(passwordByte);
+            // 存时,是加密存进去的; 这里读取时,解密一下
+            char[] passwordCharArr = EncryptUtil.decrypt(new String(passwordByte, StandardCharsets.UTF_8),
+                    Cache.sealCache.toCharArray()).toCharArray();
+            Cache.libPasswordCache.put(classWinterInfoDir, passwordCharArr);
+            return passwordCharArr;
+        }
+    }
+}

+ 792 - 0
class-winter-core/src/main/java/net/jd/classwinter/executor/EncryptExecutor.java

@@ -0,0 +1,792 @@
+package net.jd.classwinter.executor;
+
+import javassist.CannotCompileException;
+import javassist.ClassPool;
+import javassist.NotFoundException;
+import net.jd.classwinter.Reverses;
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.exception.ClassWinterException;
+import net.jd.classwinter.util.Cache;
+import net.jd.classwinter.util.Constant;
+import net.jd.classwinter.util.EncryptUtil;
+import net.jd.classwinter.util.IOUtil;
+import net.jd.classwinter.util.JarUtil;
+import net.jd.classwinter.util.JavassistUtil;
+import net.jd.classwinter.util.Logger;
+import net.jd.classwinter.util.Pair;
+import net.jd.classwinter.util.PathUtil;
+import net.jd.classwinter.util.StrUtil;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * java class加密
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/4/28 22:07:51
+ */
+public class EncryptExecutor {
+    
+    /** 本项目(class-winter)需要打包的代码 */
+    public static final Set<String> CLASS_WINTER_FILES = new HashSet<>();
+    
+    static {
+        CLASS_WINTER_FILES.add("Cache.class");
+        CLASS_WINTER_FILES.add("JustryDeng.class");
+        CLASS_WINTER_FILES.add("ClassWinterException.class");
+        CLASS_WINTER_FILES.add("DecryptExecutor.class");
+        CLASS_WINTER_FILES.add("Constant.class");
+        CLASS_WINTER_FILES.add("EncryptUtil.class");
+        CLASS_WINTER_FILES.add("ExceptionUtil.class");
+        CLASS_WINTER_FILES.add("IOUtil.class");
+        CLASS_WINTER_FILES.add("JarUtil.class");
+        CLASS_WINTER_FILES.add("JavaagentCmdArgs.class");
+        CLASS_WINTER_FILES.add("JavassistUtil.class");
+        CLASS_WINTER_FILES.add("Logger.class");
+        CLASS_WINTER_FILES.add("Pair.class");
+        CLASS_WINTER_FILES.add("PathUtil.class");
+        CLASS_WINTER_FILES.add("StrUtil.class");
+        CLASS_WINTER_FILES.add("Reverses.class");
+        CLASS_WINTER_FILES.add("Reverses$1.class");
+    }
+    
+    /** 待加密的的jar/war包路径(如" /tmp/abc.jar) */
+    private final String originJarOrWar;
+    
+    /** true-originJarOrWar是jar包;false-originJarOrWar是war包 */
+    private final boolean originIsJar;
+    
+    /** 加密后生成的jar(or war)包的名称(如:abc-encrypted) */
+    private final String finalName;
+    
+    /** 解压jar/war的根目录*/
+    private final String targetRootDir;
+    
+    /** 解压的jar/war包里,lib的根目录*/
+    private final String targetLibDir;
+    
+    /** 解压的jar/war包里,class文件的根目录*/
+    private final String targetClassesDir;
+    
+    /**
+     * 加密密码(若为空,则系统会自动生成)
+     */
+    private final String password;
+    
+    /**
+     * 要加密的类的全类名前缀(也可以精确匹配)
+     */
+    private final Set<String> includePrefixSet;
+    
+    /**
+     * 要加密的lib包(若不指定,则加密筛选范围默认仅从项目代码本身进行筛选加密)。
+     * <p>
+     *  提示: 不论项目里面的依赖的传递关系是怎样的,这里只需要指定需要加密的jar包全称即可。
+     *  说明:假设我们项目的pom依赖关系为:  myProject -> a -> b  -> c
+     *      即: 我们的项目myProject直接依赖的是a,我们在pom中主动指定依赖的也是a,没有主动指定需要依赖b和c,但是由于maven依赖传递的原因,
+     *          我们的项目实际上也依赖了b和c, 当我们把项目myProject打成jar包时,也会把a、b、c都打进jar包里。
+     *           并且,将 a -> b -> c这样的层级关系平铺开了, 为
+     *           │
+     *           ├─BOOT-INF
+     *           │  │
+     *           │  └─lib
+     *           │     └─a.jar
+     *           │     └─b.jar
+     *           │     └─c.jar
+     *           这里,如果需要对a、b、c进行加密的话,只需要使includeLibSet值为["a.jar","b.jar","c.jar"]即可
+     */
+    private final Set<String> includeLibSet;
+    
+    /**
+     * 不需要加密的类的全类名前缀(也可以精确匹配)
+     * <p>
+     *     includePrefixSet匹配的类中再排除excludePrefixSet匹配的类,即为最终会被加密的类
+     */
+    private final Set<String> excludePrefixSet;
+    
+    /**
+     * 已经被class-winter加密过了的lib的信息
+     * <p>
+     *    每个pair对象的属性介绍:
+     *        pair-左: lib包名,如: abc.jar
+     *        pair-右: 加密该lib时的密码
+     * <p>
+     *  想一想这种case:
+     *      第三方给你提供了一个被class-winter加密过的jar包,你把这个jar包依赖进你的项目your-project中,
+     *      你想正常的运行起来项目,那么你就需要用到这个属性了。
+     */
+    private final Set<Pair<String, String>> alreadyProtectedLibSet;
+    
+    /**
+     * 加密时,额外往加密上下文中添加的jar文件(或者jar文件所在的目录)。
+     * <p>
+     * 在进行加密时,某些类可能会因为缺失必要的依赖类而导致加密失败。就可以通过指定此字段的值(值应为某个jar或者某个目录的全路径),
+     * 这样一来,class-winter混淆目标jar时,除了加载要加载的jar/war内部的所有jar外,还会额外加载这里指定的jar(或者这里指定的目录下的所有子孙jar),
+     * 进而保证在加密时,不会因为确实依赖类而加密失败。
+     * <p>
+     * 注: 某些项目在打jar包时,是没有把其依赖的lib打进jar包中的,所以会出现[某些类可能会因为缺失必要的依赖类而导致加密失败]的情况。
+     * <p>
+     * 示例说明:
+     * 假设加密时日志报warn说
+     *    2021-06-11 20:24:31 [ WARN] net.jd.classwinter.executor.EncryptExecutor: Ignore clear-method-body for  className [com.aspire.ssm.handler.impl.DefaultAesPreHandlerImpl], Cannot find 'org.apache.ibatis.executor.parameter.ParameterHandler'
+     *    2021-06-11 20:24:31 [ WARN] net.jd.classwinter.executor.EncryptExecutor: Ignore clear-method-body for className [com.aspire.ssm.interceptor.CustomizeMybatisPlugin], Cannot find 'org.apache.ibatis.plugin.Invocation'
+     *  这说明了
+     *      DefaultAesPreHandlerImpl类中使用到了org.apache.ibatis.executor.parameter.ParameterHandler类,
+     *      DCustomizeMybatisPlugin类中使用到了org.apache.ibatis.plugin.Invocation类,
+     *   进一步说明了
+     *       要加密的jar包没有把mybatis的lib包打进去。
+     *   这时,我们就可以通过指定此字段的值为mybatisjar包的全路径名(如/tmp/lib/mybatis-3.5.1.jar)来解决问题。
+     */
+    private final String supportFile;
+    
+    /**
+     * 要加密的lib jar文件的绝对路径  &  其对应的加密临时目录
+     * 如:  /xyz/abc.jar     与 /xyz/abc__temp__
+     */
+    private final Map<String, String> libJarAndTmpDirMap = new HashMap<>(8);
+    
+    private EncryptExecutor(String originJarOrWar, boolean originIsJar, String finalName, String targetRootDir,
+                            String targetLibDir, String targetClassesDir, String password, Set<String> includePrefixSet,
+                            Set<String> excludePrefixSet, Set<String> includeLibSet, Set<Pair<String, String>> alreadyProtectedLibSet, String supportFile) {
+        this.originJarOrWar = originJarOrWar;
+        this.originIsJar = originIsJar;
+        this.finalName = finalName;
+        this.targetRootDir = targetRootDir;
+        this.targetLibDir = targetLibDir;
+        this.targetClassesDir = targetClassesDir;
+        this.password = password;
+        this.includePrefixSet = includePrefixSet;
+        this.excludePrefixSet = excludePrefixSet;
+        this.includeLibSet = includeLibSet;
+        this.alreadyProtectedLibSet = alreadyProtectedLibSet;
+        this.supportFile = supportFile;
+    }
+    
+    /**
+     * do
+     *
+     * @return  加密处理后生成的jar/war文件的位置
+     */
+    public String process() {
+        // step0. 解压jar到jarRootDir下
+        JarUtil.unJarWar(originJarOrWar, targetRootDir);
+        // step1. jar中的某些lib可能也需要加密,这里先(将那些需要加密的lib)进行解压,(解压到对应的临时目录,以便后面进行加密处理)
+        unLibJar();
+        // step2. 找到需要加密混淆的class文件
+        File targetRootDirFile = new File(targetRootDir);
+        List<File> allFiles = IOUtil.listSubFile(targetRootDirFile, 1);
+        List<File> allNeedEncryptedClassFileList = filterClasses(allFiles);
+        // step3.1. 根据原.class文件,生成加密后的.class文件,并存至Constant.ENCRYPTED_CLASSES_SAVE_DIR
+        List<String> allAlreadyEncryptedClassFileList = encryptClasses(allNeedEncryptedClassFileList, new File(targetRootDir,
+                Constant.DEFAULT_ENCRYPTED_CLASSES_SAVE_DIR));
+        // step3.2. 生成记录已加密class的全类名的清单文件
+        generateChecklistFile(allAlreadyEncryptedClassFileList);
+        
+        // step4. 记录印章(在解密时,可通过 checklist + 印章 判断一个class是否是被class-winter加密过)
+        generateSealFile();
+
+        // step5. 混淆原.class文件(即:清空方法体)
+        clearClassMethod(allNeedEncryptedClassFileList);
+        
+        // step6. 添加class-winter的用于javaagent解密的代码
+        addClassWinterAgent();
+    
+        // step7. 汇总那些在本次加密前本身就已经是被class-winter混淆的lib的相关信息到当前项目中
+        collectAlreadyProtectedLibInfo();
+        
+        // step8. 与step1项对应,将临时目录还原为原来的lib(此时得到的lib是加密后的)
+        doLibJar();
+        
+        // step9. 压缩targetRootDir目录为绝对文件路径名为generatedJarWar的jar(or war)包
+        String generatedJarWar = generateJarWarPath(this.originIsJar);
+        JarUtil.doJarWar(targetRootDir, generatedJarWar);
+        
+        // step10. 删除临时目录
+        IOUtil.delete(targetRootDirFile);
+        
+        return generatedJarWar;
+    }
+    
+    /**
+     * 采集本来就已经被class-winter混淆了的lib的信息
+     */
+    private void collectAlreadyProtectedLibInfo() {
+        if (alreadyProtectedLibSet == null ||alreadyProtectedLibSet.size() == 0) {
+            return;
+        }
+    
+        /*
+         * k - 创建出来的jar包对应的文件夹 如:  META-INF/winter/abc-1.0.0_jar/
+         * v - jar包的checklist信息, 如: abc-1.0.0.jar对应的libChecklist值
+         */
+        Map<String, String> allChecklistInfoMap = new HashMap<>(8);
+        alreadyProtectedLibSet.forEach(pair -> {
+            String libJarName = pair.getLeft();
+            String password = pair.getRight();
+            File lib = new File(targetLibDir, libJarName);
+            if (!lib.exists()) {
+                return;
+            }
+            // 形如: META-INF/winter/abc-1.0.0_jar/
+            String dirRelativePathForLib = Constant.DEFAULT_ENCRYPTED_CLASSES_SAVE_DIR + libJarName.substring(0, libJarName.length() - 4) + "_jar" + Constant.LINUX_FILE_SEPARATOR;
+            String dirAbsolutePathForLib = this.targetRootDir + File.separator + Constant.DEFAULT_ENCRYPTED_CLASSES_SAVE_DIR + libJarName.substring(0, libJarName.length() - 4) + "_jar" + File.separator;
+    
+            /*
+             * my-project依赖的lib包本身可以是被class-winter混淆后的,但是这个lib包依赖的lib包不能是被class-winter混淆后
+             * <p>
+             * 注: 也就是说对lib的处理,只往下一层,不继续往下了。
+             * 注: 所以如果符合预期的话,lib中的对应位置是没有Constant.CHECKLIST_OF_ALL_LIBS文件的,这时
+             *     IOUtil.readFileFromWorkbenchRoot(lib, Constant.CHECKLIST_OF_ALL_LIBS)会返回null
+             */
+            byte[] checklistOfAllLibsByte = IOUtil.readFileFromWorkbenchRoot(lib, Constant.CHECKLIST_OF_ALL_LIBS);
+            if (checklistOfAllLibsByte != null) {
+                throw new ClassWinterException(libJarName+"'s lib cannot be protected by class-winter.");
+            }
+            
+            // 清单文件
+            byte[] checklistByte = IOUtil.readFileFromWorkbenchRoot(lib, Constant.ALREADY_ENCRYPTED_CLASS_CHECKLIST_CLASSES_SAVE_FILE);
+            if (checklistByte == null) {
+                // 清单文件不存在,说明这个lib中没有任何类被混淆
+                return;
+            } else {
+                String libChecklist = new String(checklistByte, StandardCharsets.UTF_8);
+                IOUtil.writeContentToFile(libChecklist, new File(dirAbsolutePathForLib, Constant.CHECKLIST_FILE_SIMPLE_NAME));
+    
+                // 所有lib的checklist汇总至checklistOfAllLibs
+                allChecklistInfoMap.put(dirRelativePathForLib, libChecklist);
+                
+                // 将清单文件指向的加密后的class文件,从lib中挪动至当前项目的dirForLib下
+                String[] libClassesArr = libChecklist.split(",");
+                byte[] libClazzByte;
+                for (String nonSuffixClassLongName : libClassesArr) {
+                    libClazzByte = IOUtil.readFileFromWorkbenchRoot(lib, Constant.DEFAULT_ENCRYPTED_CLASSES_SAVE_DIR + nonSuffixClassLongName);
+                    IOUtil.toFile(libClazzByte, new File(dirAbsolutePathForLib, nonSuffixClassLongName), true);
+                }
+            }
+    
+            // 印章
+            byte[] libSealByte = IOUtil.readFileFromWorkbenchRoot(lib, Constant.SEAL_FILE);
+            if (libSealByte == null) {
+                // lib中没有印章文件
+                throw new ClassWinterException("Cannot find seal in lib [" + libJarName + "]. This lib is not "
+                        + "protected by class-winter.");
+            } else {
+                IOUtil.writeContentToFile(new String(libSealByte, StandardCharsets.UTF_8), new File(dirAbsolutePathForLib,
+                        Constant.SEAL_FILE_SIMPLE_NAME));
+            }
+            
+            // 密码
+            boolean libUserIfInputPwd = Boolean.parseBoolean(new String(IOUtil.readFileFromWorkbenchRoot(lib, Constant.USER_IF_INPUT_PWD)));
+            Logger.debug(EncryptExecutor.class, "libUserIfInputPwd info of lib [" + libJarName + "] -> " + libUserIfInputPwd);
+            if (libUserIfInputPwd && StrUtil.isBlank(password)) {
+                // 加密lib时,用户主动指定了密码,那么这里password就不应该为空
+                throw new ClassWinterException("Password is required to decrypt lib [" + libJarName + "]");
+            }
+            if (StrUtil.isBlank(password)) {
+                byte[] libEncryptedPwdByte = IOUtil.readFileFromWorkbenchRoot(lib, Constant.PWD_WINTER);
+                if (libEncryptedPwdByte == null) {
+                    // lib中没有密码文件,请指定此lib的密码
+                    throw new ClassWinterException("Cannot find pwd from lib [" + libJarName + "]");
+                } else {
+                    // 因为写之前是加密写进去的,那么这里解密一下得到明文
+                    // 存时,是加密存进去的; 这里读取时,(用lib的印章)解密一下
+                    password = EncryptUtil.decrypt(new String(libEncryptedPwdByte, StandardCharsets.UTF_8),
+                            new String(libSealByte, StandardCharsets.UTF_8).toCharArray());
+                }
+            }
+            // 存时,加密存进去
+            String encryptedPassword =EncryptUtil.encrypt(password, Cache.sealCache.toCharArray());
+            IOUtil.writeContentToFile(encryptedPassword, new File(dirAbsolutePathForLib, Constant.PWD_WINTER_SIMPLE_NAME));
+            // 由于这里已经处理了,将所有lib的密码(不论当初加密lib时密码是用户主动输入的还是系统自动产生的),都当做系统自动产生的进行录入,所以这里写死为false
+            IOUtil.writeContentToFile("false", new File(dirAbsolutePathForLib, Constant.USER_IF_INPUT_PWD_SIMPLE_NAME));
+
+        });
+        
+        // 校验 - 当多个lib之间,发生加密类冲突时,快速失败
+        Map<String, String> tmpMap = new HashMap<>(128);
+        StringBuilder checklistOfAllLibs = new StringBuilder(64);
+        allChecklistInfoMap.forEach((k, v) -> {
+            if (StrUtil.isBlank(v)) {
+                return;
+            }
+            // 当多个lib之间,发生加密类冲突时,快速失败
+            Arrays.stream(v.split(",")).filter(item -> !StrUtil.isBlank(item)).forEach(nonSuffixClassLongName -> {
+                String libDir = tmpMap.get(nonSuffixClassLongName);
+                if (!StrUtil.isBlank(libDir)) {
+                    // 当多个lib之间,发生加密类冲突时,快速失败
+                    throw new ClassWinterException(String.format("protected class[%s] conflict. one lib is [%s], another lib is [%s]",
+                            nonSuffixClassLongName, libDir, k));
+                }
+                tmpMap.put(nonSuffixClassLongName, k);
+            });
+            checklistOfAllLibs.append(k).append("=").append(v).append(Constant.LINE_SEPARATOR);
+        });
+        if (checklistOfAllLibs.length() > 0) {
+            IOUtil.writeContentToFile(checklistOfAllLibs.toString(), new File(this.targetRootDir, Constant.CHECKLIST_OF_ALL_LIBS));
+        }
+    }
+    
+    /**
+     * 解压jar包中需要加密的lib包
+     */
+    private void unLibJar() {
+        if (includeLibSet == null || includeLibSet.size() == 0) {
+            return;
+        }
+        // 罗列出targetRootDir下的所有.jar文件
+        List<File> allJarFiles = IOUtil.listFileOnly(new File(targetRootDir), Constant.JAR_SUFFIX);
+        // 筛选出需要加密的lib包
+        List<File> neededEncryptedLibs =  allJarFiles.stream().filter(jarFile -> includeLibSet.contains(jarFile.getName())).collect(Collectors.toList());
+    
+        // 依次解密
+        neededEncryptedLibs.forEach(jar -> {
+            // 假设: jarAbsolutePath为  /xyz/abc.jar
+            String jarAbsolutePath = jar.getAbsolutePath();
+            // 那么: temDir就为  /xyz/abc__temp__
+            String temDir = jarAbsolutePath.substring(0, jarAbsolutePath.length() - Constant.JAR_SUFFIX.length()) + Constant.TMP_DIR_SUFFIX;
+            JarUtil.unJarWar(jarAbsolutePath, temDir);
+            libJarAndTmpDirMap.put(jarAbsolutePath, temDir);
+            // 删除原来的lib包
+            IOUtil.delete(new File(jarAbsolutePath));
+        });
+    }
+    
+    /**
+     * 将unLibJar()中解压出来的临时目录打包成jar包
+     */
+    private void doLibJar() {
+        if (libJarAndTmpDirMap.isEmpty()) {
+            return;
+        }
+        libJarAndTmpDirMap.forEach((jarFilePath, tmpDirPath) -> {
+            JarUtil.doJarWar(tmpDirPath, jarFilePath);
+            // 删除临时目录
+            IOUtil.delete(new File(tmpDirPath));
+        });
+    }
+    
+    /**
+     * 生成记录已加密class的全类名的清单文件
+     *
+     * @param classLongNameList
+     *            已加密class的全类名集合
+     */
+    private void generateChecklistFile(List<String> classLongNameList) {
+        if (classLongNameList == null || classLongNameList.isEmpty()) {
+            return;
+        }
+        String content = String.join(Constant.COMMA, classLongNameList);
+        // 将加密后的内容存储到指定位置下
+        IOUtil.writeContentToFile(content, new File(targetRootDir, Constant.ALREADY_ENCRYPTED_CLASS_CHECKLIST_CLASSES_SAVE_FILE));
+    }
+    
+    /**
+     * 生成记录本次加密印章的文件
+     */
+    private void generateSealFile() {
+        // 将加密后的内容存储到指定位置下
+        IOUtil.writeContentToFile(Constant.SEAL, new File(targetRootDir, Constant.SEAL_FILE));
+    }
+    
+    /**
+     * 添加class-winter的用于javaagent解密的代码
+     */
+    private void addClassWinterAgent() {
+        String classWinterProjectRootDir = PathUtil.getProjectRootDir(this.getClass());
+        File classWinterProjectRootDirFile = new File(classWinterProjectRootDir);
+        int classWinterProjectRootDirLength = classWinterProjectRootDir.length();
+        // 当class-winter未打成jar包时
+        if (classWinterProjectRootDir.endsWith(Constant.CLASSES_DIR)) {
+            List<File> allFileList = IOUtil.listSubFile(new File(classWinterProjectRootDir), 0);
+            allFileList.forEach(file -> {
+                String classLongNamePath = file.getAbsolutePath().substring(classWinterProjectRootDirLength);
+                File destFile = new File(this.originIsJar ? this.targetRootDir : this.targetClassesDir, classLongNamePath);
+                if (file.isFile() && CLASS_WINTER_FILES.contains(file.getName())) {
+                    byte[] bytes = IOUtil.toBytes(file);
+                    IOUtil.toFile(bytes, destFile, true);
+                }
+            });
+        } else {
+            // 当class-winter打包成jar包时
+            if (classWinterProjectRootDir.endsWith(Constant.JAR_SUFFIX)) {
+                if (this.originIsJar) {
+                    JarUtil.unJarWar(classWinterProjectRootDir, targetRootDir, false, CLASS_WINTER_FILES);
+                } else {
+                    byte[] bytes = IOUtil.toBytes(classWinterProjectRootDirFile);
+                    IOUtil.toFile(bytes, new File(this.targetLibDir, classWinterProjectRootDirFile.getName()), true);
+                }
+            } else {
+                throw new ClassWinterException("Execute method addClassWinterAgent() fail. Cannot parse classWinterProjectRootDir [" + classWinterProjectRootDir + "].");
+            }
+        }
+        
+        // 把javaagent信息加入到MANIFEST.MF
+        File manifest = new File(this.targetRootDir, "META-INF/MANIFEST.MF");
+        String preMain = "Premain-Class: " + Reverses.class.getName();
+        String[] origin = {};
+        if (manifest.exists()) {
+            origin = IOUtil.readContentFromFile(manifest).split(System.lineSeparator());
+        }
+        // 在原来的启动函数行后面,插入pre-main指令
+        String str = StrUtil.insertStrAfterLine(origin, preMain, "Main-Class:");
+        str = str + System.lineSeparator();
+        IOUtil.writeContentToFile(str, manifest);
+    }
+    
+    /**
+     * 获取生成的jar(or war)包的全路径名(如:/tmp/abc.jar)
+     *
+     * @param originIsJar
+     *            true-生成jar路径; false-生成war路径
+     *
+     * @return  生成的jar(or war)包的全路径名(如:/tmp/abc.jar)
+     */
+    private String generateJarWarPath(boolean originIsJar) {
+        int endIndex = originJarOrWar.lastIndexOf(Constant.LINUX_FILE_SEPARATOR);
+        String jarRootParentDir;
+        if (endIndex > 0) {
+            jarRootParentDir = originJarOrWar.substring(0, endIndex + Constant.LINUX_FILE_SEPARATOR.length());
+        } else {
+            jarRootParentDir = Constant.LINUX_FILE_SEPARATOR;
+        }
+        return jarRootParentDir + finalName + (originIsJar ? Constant.JAR_SUFFIX : Constant.WAR_SUFFIX);
+    }
+    
+    /**
+     * 清空class文件的方法体,并保留参数信息
+     *
+     * @param classFiles
+     *            需要清空方法体的class文件
+     */
+    private void clearClassMethod(List<File> classFiles) {
+        // step0. 初始化javassist
+        ClassPool pool = ClassPool.getDefault();
+        // step1. 把所有可能涉及到的jar、.class添加进加载路径
+        try {
+            JavassistUtil.loadJar(pool, this.targetRootDir);
+            JavassistUtil.loadClass(pool, this.targetRootDir);
+            if (!StrUtil.isBlank(this.supportFile)) {
+                JavassistUtil.loadJar(pool, this.supportFile);
+            }
+        } catch (NotFoundException e) {
+            throw new ClassWinterException(e);
+        }
+        // step2. 修改class方法体,并保存文件
+        classFiles.forEach(classFile -> {
+            String className = JavassistUtil.resolveClassName(classFile.getAbsolutePath(), true);
+            try {
+                byte[] bts = JavassistUtil.clearMethodBody(pool, className, true);
+                IOUtil.toFile(bts, classFile, true);
+            } catch (NotFoundException e) {
+                // ignore
+                Logger.warn(EncryptExecutor.class, "Ignore clear-method-body for className [" + className + "], Cannot find '" + e.getMessage() + "'");
+            } catch (CannotCompileException e) {
+                throw new ClassWinterException(e);
+            }
+        });
+        pool.clearImportedPackages();
+    }
+    
+    /**
+     * 加密class文件,并将加密后的class文件放在savaDir里
+     *
+     * @param classFiles
+     *            需要加密的class文件
+     * @param savaDir
+     *            加密后的class文件的存储目录
+     * @return  已经加密了的类的全类名(如: com.aaa.bbb.Abc)
+     */
+    private List<String> encryptClasses(List<File> classFiles, File savaDir) {
+        // 保证目录存在
+        JarUtil.guarantyDirExist(savaDir);
+        classFiles.stream().map(File::getName).forEach(name -> {
+            if (!name.endsWith(Constant.CLASS_SUFFIX)) {
+                throw new ClassWinterException("classFiles must all be class file. file [" + name + "] is illegal.");
+            }
+        });
+        // 加密后存储的位置
+        List<String> encryptClasses = new ArrayList<>();
+        // 加密,并将得到的加密后的class文件另存到savaDir目录中
+        classFiles.forEach(classFile -> {
+            String className = JavassistUtil.resolveClassName(classFile.getAbsolutePath(), true);
+            byte[] bytes = IOUtil.toBytes(classFile);
+            bytes = EncryptUtil.encrypt(bytes, obtainPassword());
+            // 将加密后的内容存储到指定位置下
+            IOUtil.toFile(bytes, new File(savaDir, className), true);
+            encryptClasses.add(className);
+        });
+        return encryptClasses;
+    }
+    
+    /**
+     * 获取密码
+     *
+     * @return  密码
+     */
+    private char[] obtainPassword() {
+        if (Cache.passwordCache != null) {
+            // 从缓存读取
+            return Cache.passwordCache;
+        }
+        boolean passwordIsBlank = StrUtil.isBlank(this.password);
+        if (passwordIsBlank) {
+            char[] generatedPwd = EncryptUtil.generateCharArr(new SecureRandom().nextInt(500) + 100);
+            // 将自动生成的密码记录到文件中
+            // 存时,加密存进去
+            String encryptedGeneratedPwd = EncryptUtil.encrypt(new String(generatedPwd), Constant.SEAL.toCharArray());
+            IOUtil.writeContentToFile(encryptedGeneratedPwd, new File(targetRootDir, Constant.PWD_WINTER));
+            // 放入缓存
+            Cache.passwordCache = generatedPwd;
+        } else {
+            Cache.passwordCache = this.password.toCharArray();
+        }
+        // 记录加密时,用户是否输入了密码
+        IOUtil.writeContentToFile(passwordIsBlank ? "false" : "true", new File(targetRootDir, Constant.USER_IF_INPUT_PWD));
+        return Cache.passwordCache;
+    }
+    
+    /**
+     * 找出所有需要加密的class文件
+     *
+     * @param allFileList
+     *            所有文件
+     * @return  需要加密的class文件
+     */
+    private List<File> filterClasses(List<File> allFileList) {
+        return allFileList.stream()
+                .filter(file -> file.getName().endsWith(Constant.CLASS_SUFFIX))
+                .filter(file -> {
+                    // 全类名
+                    String classLongName = JavassistUtil.resolveClassName(file.getAbsolutePath(), true);
+                    if (excludePrefixSet != null) {
+                        for (String excludePrefix : excludePrefixSet) {
+                            if (classLongName.startsWith(excludePrefix)) {
+                                // 排除
+                                return false;
+                            }
+                        }
+        
+                    }
+                    if (includePrefixSet != null) {
+                        for (String includePrefix : includePrefixSet) {
+                            if (classLongName.startsWith(includePrefix)) {
+                                // 包含
+                                return true;
+                            }
+                        }
+                    }
+                    return false;
+                }).collect(Collectors.toList());
+    }
+    
+    
+    @Override
+    public String toString() {
+        return "EncryptExecutor{" +
+                "originJarOrWar='" + originJarOrWar + '\'' +
+                ", originIsJar=" + originIsJar +
+                ", finalName='" + finalName + '\'' +
+                ", targetRootDir='" + targetRootDir + '\'' +
+                ", targetLibDir='" + targetLibDir + '\'' +
+                ", targetClassesDir='" + targetClassesDir + '\'' +
+                // 密码脱敏
+                ", password='" + (StrUtil.isBlank(password) ? null : "******") + '\'' +
+                ", supportFile='" + supportFile + '\'' +
+                ", includePrefixSet=" + includePrefixSet +
+                ", includeLibSet=" + includeLibSet +
+                ", excludePrefixSet=" + excludePrefixSet +
+                ", alreadyProtectedLibSet=" + alreadyProtectedLibSet +
+                '}';
+    }
+    
+    /**
+     * builder for EncryptExecutor
+     */
+    public static EncryptExecutor.Builder builder() {
+        return new EncryptExecutor.Builder();
+    }
+    
+    public static class Builder {
+        
+        private String originJarOrWar;
+        
+        private String finalName;
+        
+        private String password;
+        
+        private String includePrefix;
+        
+        private String excludePrefix;
+        
+        private String includeLibs;
+        
+        private String alreadyProtectedLibs;
+        
+        private String supportFile;
+        
+        private Boolean debug = false;
+        
+        private String tips;
+    
+        public Builder originJarOrWar(String originJarOrWar) {
+            this.originJarOrWar = originJarOrWar;
+            return this;
+        }
+    
+        public Builder finalName(String finalName) {
+            this.finalName = finalName;
+            return this;
+        }
+        
+        public Builder password(String password) {
+            this.password = password;
+            return this;
+        }
+        
+        public Builder includePrefix(String includePrefix) {
+            this.includePrefix = includePrefix;
+            return this;
+        }
+        
+        public Builder excludePrefix(String excludePrefix) {
+            this.excludePrefix = excludePrefix;
+            return this;
+        }
+        
+        public Builder includeLibs(String includeLibs) {
+            this.includeLibs = includeLibs;
+            return this;
+        }
+        
+        public Builder alreadyProtectedLibs(String alreadyProtectedLibs) {
+            this.alreadyProtectedLibs = alreadyProtectedLibs;
+            return this;
+        }
+        
+        
+        public Builder supportFile(String supportFile) {
+            this.supportFile = supportFile;
+            return this;
+        }
+        
+        public Builder debug(Boolean debug) {
+            this.debug = debug;
+            return this;
+        }
+        
+        public Builder tips(String tips) {
+            this.tips = tips;
+            return this;
+        }
+        
+        public EncryptExecutor build() {
+            // ---- 其他配置
+            // 1. debug模式
+            Logger.ENABLE_DEBUG.set(this.debug != null && this.debug);
+            // 2. 错误启动jar时,System.out输出的提示信息
+            if (!StrUtil.isEmpty(tips)) {
+                JavassistUtil.TIPS = tips;
+            }
+            
+            // ---- 参数校验
+            // 1. originJarOrWar不能为空
+            if (StrUtil.isEmpty(originJarOrWar)) {
+                throw new ClassWinterException("originJarOrWar cannot be empty.");
+            }
+            // 2. originJarOrWar必须是jar文件或者是war文件
+            if (!originJarOrWar.endsWith(Constant.JAR_SUFFIX) && !originJarOrWar.endsWith(Constant.WAR_SUFFIX)) {
+                throw new ClassWinterException("originJarOrWar must be Jar or War file.");
+            }
+            // 3. includePrefix不能为空
+            if (StrUtil.isEmpty(includePrefix)) {
+                throw new ClassWinterException("includePrefix cannot be empty.");
+            }
+            // 4. supportFile如果不为空, 那么必须存在且只能为件文夹或者.jar文件
+            if (!StrUtil.isBlank(supportFile)) {
+                File tmpFile = new File(supportFile);
+                if (!tmpFile.exists()) {
+                    throw new ClassWinterException("supportFile ["+supportFile+"] non-exist.");
+                }
+                // 文件的话,必须是.jar文件
+                if (tmpFile.isFile() && !supportFile.endsWith(Constant.JAR_SUFFIX)) {
+                    throw new ClassWinterException("supportFile must be dir or a jar file.");
+                }
+                
+            }
+            
+            // ---- 构建EncryptExecutor对象
+            File originFile = new File(originJarOrWar);
+            if (!originFile.exists()) {
+                throw new ClassWinterException("cannot find file [" + originJarOrWar + "].");
+            }
+            // 文件路径分隔符统一为 /
+            originJarOrWar = originFile.getAbsolutePath().replace(File.separator, Constant.LINUX_FILE_SEPARATOR);
+    
+            // 判断originJarOrWar代表的文件是jar还是war
+            boolean originIsJar = JarUtil.isJarOrWar(originJarOrWar);
+            
+            if (StrUtil.isEmpty(this.finalName)) {
+                String originFileName = originFile.getName();
+                int idx = originFileName.lastIndexOf(".");
+                this.finalName(originFileName.substring(0, idx) + "-encrypted");
+            }
+            // 无论是.war还是.jar,长度都是4
+            String targetRootDir = originJarOrWar.substring(0, originJarOrWar.length() - 4) + Constant.TMP_DIR_SUFFIX;
+            String targetLibDir = String.join(File.separator, targetRootDir, originIsJar ? Constant.BOOT_INF :
+                    Constant.WEB_INF, Constant.LIB);
+            String targetClassesDir = String.join(File.separator, targetRootDir, originIsJar ? Constant.BOOT_INF :
+                    Constant.WEB_INF, Constant.CLASSES);
+
+            Set<String> includePrefixSet = StrUtil.strToSet(includePrefix);
+            Set<String> excludePrefixSet = StrUtil.strToSet(excludePrefix);
+            Set<String> includeLibSet = StrUtil.strToSet(includeLibs);
+            Set<Pair<String, String>> alreadyProtectedLibSet = parseAlreadyProtectedLibs();
+            
+            // 如果这次需要加密的lib本身就已经是被加密了的,那么这次不再对其进行加密
+            alreadyProtectedLibSet.stream().map(Pair::getLeft).forEach(lib -> {
+                if (includeLibSet.contains(lib)) {
+                    Logger.warn(EncryptExecutor.class, "Ignore includeLibs item [" + lib + "], because this item already be protected by class-winter.");
+                    includeLibSet.remove(lib);
+                }
+            });
+            
+            for (String jarFile : includeLibSet) {
+                // jarFile 形如 abc-1.0.0.jar
+                if (jarFile.endsWith(Constant.JAR_SUFFIX)) {
+                    continue;
+                }
+                throw new ClassWinterException("includeLibs format must be shaped like xxx1.jar[,xxx2.jar,xxx3.jar]");
+            }
+            return new EncryptExecutor(this.originJarOrWar, originIsJar, this.finalName, targetRootDir, targetLibDir, targetClassesDir,
+                    password, includePrefixSet, excludePrefixSet, includeLibSet, alreadyProtectedLibSet, this.supportFile);
+        }
+    
+        /**
+         * 解析alreadyProtectedLibs信息为Set<Pair<String, String>>
+         */
+        private Set<Pair<String, String>> parseAlreadyProtectedLibs() {
+            Set<Pair<String, String>> alreadyProtectedLibSet = new HashSet<>();
+            Set<String> alreadyProtectedLibInfoSet = StrUtil.strToSet(alreadyProtectedLibs);
+            for (String info : alreadyProtectedLibInfoSet) {
+                int idx = info.indexOf(":");
+                if (idx >= 0) {
+                    alreadyProtectedLibSet.add(Pair.of(info.substring(0, idx), info.substring(idx + 1)));
+                } else {
+                    alreadyProtectedLibSet.add(Pair.of(info, null));
+                }
+            }
+            return alreadyProtectedLibSet;
+        }
+    }
+}

+ 4 - 0
class-winter-core/src/main/java/net/jd/classwinter/pakage-info.java

@@ -0,0 +1,4 @@
+package net.jd.classwinter;
+/*
+ * 对class-final进行整理、改造、优化、定制
+ */

+ 182 - 0
class-winter-core/src/main/java/net/jd/classwinter/util/BashUtil.java

@@ -0,0 +1,182 @@
+package net.jd.classwinter.util;
+
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.exception.ClassWinterException;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+import java.util.Scanner;
+
+/**
+ * bash/sh 工具类
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/6/1 23:41:21
+ */
+public final class BashUtil {
+    
+    public static final String PORTS_PLACEHOLDER = "ports_placeholder";
+    
+    /**
+     * 当前操作系统是否是windows
+     */
+    private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows");
+    
+    public static final String ORIGIN_KILL_PROCESS_BY_PORT_BAT = ""
+            + "@echo off & setlocal EnableDelayedExpansion\n"
+            + "\n"
+            + "title kill process by port\n"
+            + "\n"
+            + "for %%a in (" + PORTS_PLACEHOLDER + ") do (\n"
+            + "    set pid=0\n"
+            + "    for /f \"tokens=2,5\" %%b in ('netstat -ano ^| findstr \":%%a\"') do (\n"
+            + "        set temp=%%b\n"
+            + "        for /f \"usebackq delims=: tokens=1,2\" %%i in (`set temp`) do (\n"
+            + "            if %%j==%%a (\n"
+            + "                taskkill /f /pid %%c\n"
+            + "                set pid=%%c\n"
+            + "                echo Port [%%a] related process has been killed.\n"
+            + "            )\n"
+            + "        )\n"
+            + "    )\n"
+            + "    if !pid!==0 (\n"
+            + "       echo Port [%%a] is not occupied.\n"
+            + "    )\n"
+            + ")\n"
+            + "\n"
+            + "echo Operation has completed.\n";
+    
+    /**
+     * 运行命令
+     *
+     * @param cmd
+     *            命令
+     * @return 结果
+     */
+    public static String runCmd(String cmd) {
+        return runCmd(cmd, 0);
+    }
+    
+    /**
+     * 运行命令
+     *
+     * @param cmd
+     *            命令
+     * @param line
+     *           返回第几行结果,<=0,则返回所有
+     * @return 结果
+     */
+    public static String runCmd(String cmd, int line) {
+        Logger.info(BashUtil.class, "cmd -> " + cmd + ", line -> " + line);
+        if (cmd == null || cmd.length() == 0) {
+            throw new ClassWinterException("cmd cannot be empty.");
+        }
+        Process process;
+        Scanner sc = null;
+        StringBuilder sb = new StringBuilder();
+        try {
+            process = Runtime.getRuntime().exec(cmd);
+            process.getOutputStream().close();
+            sc = new Scanner(process.getInputStream(), IS_WINDOWS ? "GBK" : StandardCharsets.UTF_8.name());
+            int i = 0;
+            while (sc.hasNextLine()) {
+                i++;
+                String str = sc.nextLine();
+                if (line <= 0) {
+                    sb.append(str).append(System.lineSeparator());
+                } else if (i == line) {
+                    return str.trim();
+                }
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            throw new ClassWinterException(e);
+        } finally {
+            IOUtil.close(sc);
+        }
+    }
+    
+    /**
+     * 运行命令并输出到控制台
+     *
+     * @param cmd
+     *            命令
+     */
+    public static void runCmdAndPrint(String cmd) {
+        Logger.info(BashUtil.class, "cmd -> " + cmd);
+        if (cmd == null || cmd.length() == 0) {
+            throw new ClassWinterException("cmd cannot be empty.");
+        }
+        Process process;
+        Scanner sc = null;
+        try {
+            process = Runtime.getRuntime().exec(cmd);
+            process.getOutputStream().close();
+            sc = new Scanner(process.getInputStream(), IS_WINDOWS ? "GBK" : StandardCharsets.UTF_8.name());
+            while (sc.hasNextLine()) {
+                System.out.println(sc.nextLine());
+            }
+        } catch (Exception e) {
+            throw new ClassWinterException(e);
+        } finally {
+            IOUtil.close(sc);
+        }
+    }
+    
+    /**
+     * 要执行的bat文件内容
+     * <p>
+     *     only for windows
+     * </p>
+     */
+    public static String killProcessByPorts(String... port) {
+        if (port == null || port.length == 0) {
+            return "";
+        }
+        String batContent = ORIGIN_KILL_PROCESS_BY_PORT_BAT.replace(PORTS_PLACEHOLDER, String.join(",", port));
+        Process process = null;
+        InputStream inputStream = null;
+        BufferedReader bufferedReader = null;
+        StringBuilder sb = new StringBuilder(64);
+        try {
+            File tmpBatFile = new File("/mp_" + System.currentTimeMillis() + ".bat");
+            IOUtil.writeContentToFile(batContent, tmpBatFile);
+            process = Runtime.getRuntime().exec(tmpBatFile.getAbsolutePath());
+            inputStream = process.getInputStream();
+            bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                line = line.trim();
+                if (!line.startsWith("Port") && !line.startsWith("Operation")) {
+                    continue;
+                }
+                sb.append(line).append("\t");
+            }
+            IOUtil.delete(tmpBatFile);
+        }catch (IOException e) {
+            throw new ClassWinterException(e);
+        } finally {
+            if (process != null) {
+                try {
+                    process.waitFor();
+                } catch (Exception e) {
+                    // ignore
+                }
+            }
+            IOUtil.close(inputStream, bufferedReader);
+            if (process != null) {
+                try {
+                    process.destroy();
+                } catch (Exception e) {
+                    // ignore
+                }
+            }
+        }
+        return sb.toString();
+    }
+}

+ 36 - 0
class-winter-core/src/main/java/net/jd/classwinter/util/Cache.java

@@ -0,0 +1,36 @@
+package net.jd.classwinter.util;
+
+import net.jd.classwinter.author.JustryDeng;
+
+import java.util.Map;
+
+/**
+ * 简单缓存
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/5/27 23:15:57
+ */
+public final class Cache {
+    
+    public static char[] passwordCache = null;
+    
+    
+    /** 本项目加密时的印章 */
+    public static String sealCache = null;
+    
+    /**
+     * 以key-value的形式记录lib的印章。(key-存放采集到的类所在lib的信息的文件夹, value-印章字符串)
+     *
+     * key   - META-INF/winter/abc-1.0.0_jar/
+     * value - 印章字符串
+     */
+    public static Map<String, String> libSealCache;
+    
+    /**
+     * 以key-value的形式记录各个libs密码的印章。(key-lib对应的class-winter信息文件夹, value-密码)
+     *
+     * key   - 形如: META-INF/winter/abc-1.0.0_jar/
+     * value - 密码
+     */
+    public static Map<String, char[]> libPasswordCache = null;
+}

+ 150 - 0
class-winter-core/src/main/java/net/jd/classwinter/util/Constant.java

@@ -0,0 +1,150 @@
+package net.jd.classwinter.util;
+
+import net.jd.classwinter.author.JustryDeng;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 常量类
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/4/23 0:36:14
+ */
+public interface Constant {
+    
+    /**
+     * 印章, 若class中存在此印章内容,也能说明该class被class-winter加密过
+     */
+    String SEAL = "At " + LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME) + "\t" + "Random-Character " + new String(EncryptUtil.generateCharArr(32));
+    
+    /**
+     * 解压jar/war包时,临时目录后缀名
+     */
+    String TMP_DIR_SUFFIX = "__temp__";
+    
+    /**
+     * lib
+     */
+    String LIB = "lib";
+    
+    /**
+     * classes
+     */
+    String CLASSES = "classes";
+    
+    /**
+     * 默认的class-winter相关信息的存储目录
+     */
+    String DEFAULT_ENCRYPTED_CLASSES_SAVE_DIR = "META-INF/winter/";
+    
+    /**
+     * 记录已加密.class的全类名的清单文件
+     */
+    String ALREADY_ENCRYPTED_CLASS_CHECKLIST_CLASSES_SAVE_FILE = "META-INF/winter/checklist.winter";
+    
+    /**
+     * 记录已加密.class的全类名的清单文件
+     */
+    String CHECKLIST_FILE_SIMPLE_NAME = "checklist.winter";
+    
+    /**
+     * 记录本次加密印章的文件
+     */
+    String SEAL_FILE = "META-INF/winter/seal.winter";
+    
+    /**
+     * 记录本次加密印章的文件
+     */
+    String SEAL_FILE_SIMPLE_NAME = "seal.winter";
+    
+    /**
+     * 汇总记录项目中那些本身就已经被class-winter混淆了的lib的checklist
+     */
+    String CHECKLIST_OF_ALL_LIBS = "META-INF/winter/checklist-of-all-libs.winter";
+    
+    /**
+     * 当用户不主动指定密码时,class_winter会自动生成加密密码,并存至此处
+     */
+    String PWD_WINTER = "META-INF/winter/pwd.winter";
+    
+    /**
+     * 当用户不主动指定密码时,class_winter会自动生成加密密码,并存至此处
+     */
+    String PWD_WINTER_SIMPLE_NAME = "pwd.winter";
+    
+    /**
+     * 记录加密时,用户是否输入了密码
+     */
+    String USER_IF_INPUT_PWD = "META-INF/winter/userIfInputPwd.winter";
+    
+    /**
+     * 记录加密时,用户是否输入了密码
+     */
+    String USER_IF_INPUT_PWD_SIMPLE_NAME = "userIfInputPwd.winter";
+    
+    /**
+     * BOOT-INF
+     */
+    String BOOT_INF = "BOOT-INF";
+    
+    /**
+     * WEB-INF
+     */
+    String WEB_INF = "WEB-INF";
+    
+    /**
+     * class文件后缀
+     */
+    String CLASS_SUFFIX = ".class";
+    
+    /**
+     * jar包后缀
+     */
+    String JAR_SUFFIX = ".jar";
+    
+    /**
+     * war包后缀
+     */
+    String WAR_SUFFIX = ".war";
+    
+    /**
+     * jar协议
+     */
+    String JAR_PROTOCOL = "jar:";
+    
+    /**
+     * war协议
+     */
+    String WAR_PROTOCOL = "war:";
+    
+    /**
+     * file协议
+     */
+    String FILE_PROTOCOL = "file:";
+    
+    /**
+     * 文件路径分隔符
+     */
+    String LINUX_FILE_SEPARATOR = "/";
+    
+    /**
+     * /classes/
+     */
+    String CLASSES_DIR = "/classes/";
+    
+    /**
+     * 逗号
+     */
+    String COMMA = ",";
+    
+    /**
+     * jar包中文件URL有专用格式 jar:!/{jar-entry}
+     */
+    String JAR_FILE_URL_SPECIAL_SIGN = "!";
+    
+    /**
+     * 换行符
+     */
+    String LINE_SEPARATOR = "\r\n";
+}

+ 256 - 0
class-winter-core/src/main/java/net/jd/classwinter/util/EncryptUtil.java

@@ -0,0 +1,256 @@
+package net.jd.classwinter.util;
+
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.exception.ClassWinterException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Random;
+
+/**
+ * 加解密工具类
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/4/23 20:34:09
+ */
+public final class EncryptUtil {
+    
+    /** 盐 */
+    private static final char[] SALT = {'c', 'h', 'i', '@', 'm', 'i', 'a', 'n', '#', 'y', 'a', 'o', '%', 'f', 'a', 'n', 'g', '^', 'y', 'a', 'n'};
+    
+    /** AES 算法/模式/补码方式 */
+    private static final  String AES_MODE = "AES/ECB/PKCS5Padding";
+    
+    /**
+     * 加密
+     * <p>
+     *     与{@link EncryptUtil#decrypt(byte[], char[])}成对使用
+     * </p>
+     *
+     * @param source
+     *            要加密的内容
+     * @param password
+     *            用户密钥
+     * @return  加密后的内容
+     */
+    public static byte[] encrypt(byte[] source, char[] password) {
+        try {
+            String pwd = md5(StrUtil.toBytes(StrUtil.mergeChar(password, SALT)), true, true);
+            return encryptByAes(source, pwd.toCharArray());
+        } catch (NoSuchAlgorithmException e) {
+            throw new ClassWinterException(e);
+        }
+    }
+    
+    /**
+     * 解密
+     * <p>
+     *     与{@link EncryptUtil#encrypt(byte[], char[])}成对使用
+     * </p>
+     *
+     * @param source
+     *            要解密的内容
+     * @param password
+     *            用户密钥
+     * @return  解密后的内容
+     */
+    public static byte[] decrypt(byte[] source, char[] password) {
+        try {
+            String pwd = md5(StrUtil.toBytes(StrUtil.mergeChar(password, SALT)), true, true);
+            return decryptByAes(source, pwd.toCharArray());
+        } catch (NoSuchAlgorithmException e) {
+            throw new ClassWinterException(e);
+        }
+    }
+    
+    
+    /**
+     * 加密
+     * <p>
+     *     与{@link EncryptUtil#decrypt(String, char[])}成对使用
+     * </p>
+     * @param source
+     *            要加密的内容
+     * @param password
+     *            用户密钥
+     * @return  加密后的内容
+     */
+    public static String encrypt(String source, char[] password) {
+        Objects.requireNonNull(source, "source cannot be null.");
+        return Base64.getEncoder().encodeToString(encrypt(source.getBytes(StandardCharsets.UTF_8), password));
+    }
+    
+    /**
+     * 解密
+     * <p>
+     *     与{@link EncryptUtil#encrypt(String, char[])}成对使用
+     * </p>
+     * @param source
+     *            要解密的内容
+     * @param password
+     *            用户密钥
+     * @return  解密后的内容
+     */
+    public static String decrypt(String source, char[] password) {
+        Objects.requireNonNull(source, "source cannot be null.");
+        return new String(decrypt(Base64.getDecoder().decode(source), password), StandardCharsets.UTF_8);
+    }
+    
+    /**
+     * 生成指定长度的随机字符串
+     *
+     * @param length
+     *            长度
+     * @return  字符数组
+     */
+    public static char[] generateCharArr(int length) {
+        char[] result = new char[length];
+        Character[] chars = {
+                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+                'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+                'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+                'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+                'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+                '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '=', '_', '+', '.'
+        };
+        List<Character> list = Arrays.asList(chars);
+        // "洗牌"
+        Collections.shuffle(list);
+        Random random = new SecureRandom();
+        for (int i = 0; i < length; i++) {
+            if (i < chars.length) {
+                result[i] = list.get(i);
+            } else {
+                result[i] = list.get(random.nextInt(chars.length));
+            }
+        }
+        return result;
+    }
+    
+    /**
+     * 进行MD5加密
+     * <p>
+     *     摘录自 <a href="https://www.jianshu.com/p/b419163272c1" />
+     * </p>
+     *  注:MD5加密得到的是长度固定的为126bit的二进制串(一堆0和1,一共128),为了更友好的表示结果,
+     *     一般都将128位的二进制串转换为32个16进制位或16个16进制位。(16位的结果是取32位的结果值的中间部分,即32位中第8~24位的片段)
+     *
+     * @param source
+     *            要加密的内容
+     * @param abbreviation
+     *            true-返回16位结果;false-返回32位结果
+     * @param upperCase
+     *            true-结果大写;false-结果小写
+     * @return MD5加密结果
+     * @throws NoSuchAlgorithmException 无此算法时抛出
+     */
+    private static String md5(byte[] source, boolean abbreviation, boolean upperCase) throws NoSuchAlgorithmException {
+        // 1. 获取MessageDigest对象
+        MessageDigest digest = MessageDigest.getInstance("md5");
+        // 2. 执行加密操作
+        // 在MD5算法这,得到的目标字节数组的特点是:长度固定为16
+        byte[] targetBytes = digest.digest(source);
+        // 3. 声明字符数组
+        char[] characters = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
+        // 4. 遍历targetBytes
+        StringBuilder builder = new StringBuilder();
+        for (byte b : targetBytes) {
+            // 5. 取出b的高四位的值
+            // 先把高四位通过右移操作拽到低四位
+            int high = (b >> 4) & 15;
+            // 6. 取出b的低四位的值
+            int low = b & 15;
+            // 7. 以high为下标从characters中取出对应的十六进制字符
+            char highChar = characters[high];
+            // 8. 以low为下标从characters中取出对应的十六进制字符
+            char lowChar = characters[low];
+            builder.append(highChar).append(lowChar);
+        }
+        String result = builder.toString();
+        if (abbreviation) {
+            result = result.substring(8, 24);
+        }
+        if (upperCase) {
+            result = result.toUpperCase(Locale.ENGLISH);
+        }
+        return result;
+    }
+    
+    /**
+     * AES加密
+     *
+     * @param source
+     *            要加密的内容
+     * @param key
+     *            密钥
+     * @return  加密后的内容
+     */
+    private static byte[] encryptByAes(byte[] source, char[] key) {
+        try {
+            byte[] raw = StrUtil.toBytes(key);
+            SecretKeySpec secretKeySpec = new SecretKeySpec(raw, "AES");
+            Cipher cipher = Cipher.getInstance(AES_MODE);
+            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
+            return cipher.doFinal(source);
+        } catch (NoSuchAlgorithmException|NoSuchPaddingException|InvalidKeyException|IllegalBlockSizeException|BadPaddingException e) {
+            throw new ClassWinterException(e);
+        }
+    }
+    
+    /**
+     * AES解密
+     *
+     * @param source
+     *            要解密的内容
+     * @param key
+     *            密钥
+     * @return  解密后的内容
+     */
+    private static byte[] decryptByAes(byte[] source, char[] key) {
+        try {
+            byte[] raw = StrUtil.toBytes(key);
+            SecretKeySpec secretKeySpec = new SecretKeySpec(raw, "AES");
+            Cipher cipher = Cipher.getInstance(AES_MODE);
+            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
+            return cipher.doFinal(source);
+        } catch (NoSuchAlgorithmException|NoSuchPaddingException|InvalidKeyException|IllegalBlockSizeException|BadPaddingException e) {
+            throw new ClassWinterException(e);
+        }
+    }
+    
+    /**
+     * 测试
+     */
+    public static void main(String[] args) {
+//        // => 测试一
+//        String originStr = "嘿~boy~";
+//        byte[] encryptedContent = encrypt(originStr.getBytes(StandardCharsets.UTF_8), "好想吃烧烤JustryDeng123...".toCharArray());
+//        String plaintext = new String(decrypt(encryptedContent, "好想吃烧烤JustryDeng123...".toCharArray()), StandardCharsets.UTF_8);
+//        System.err.println("原文:" + originStr);
+//        System.err.println("加密后:" + encryptedContent);
+//        ///System.err.println("加密后:" + new String(Base64.getEncoder().encode(encryptedContent)));
+//        System.err.println("解密后:" + plaintext);
+        
+        // => 测试二
+        String originStr = "嘿~boy~";
+        String encryptedContent = encrypt(originStr, "好想吃烧烤JustryDeng123...".toCharArray());
+        String plaintext = decrypt(encryptedContent, "好想吃烧烤JustryDeng123...".toCharArray());
+        System.err.println("原文:" + originStr);
+        System.err.println("加密后:" + encryptedContent);
+        System.err.println("解密后:" + plaintext);
+    }
+}

+ 42 - 0
class-winter-core/src/main/java/net/jd/classwinter/util/ExceptionUtil.java

@@ -0,0 +1,42 @@
+package net.jd.classwinter.util;
+
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.exception.ClassWinterException;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * 异常工具类
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/6/2 0:22:53
+ */
+
+public class ExceptionUtil {
+    
+    /**
+     * 将异常堆栈 信息 转换为字符串
+     *
+     * @param e
+     *            异常
+     * @return 该异常的错误堆栈信息
+     */
+    public static String getStackTraceMessage(Exception e) {
+        StringWriter sw = null;
+        PrintWriter pw = null;
+        try {
+            sw = new StringWriter();
+            pw = new PrintWriter(sw);
+            // 将异常的的堆栈信息输出到printWriter中
+            e.printStackTrace(pw);
+            pw.flush();
+            sw.flush();
+            return sw.toString();
+        } catch (Exception exception) {
+            throw new ClassWinterException(exception);
+        } finally {
+            IOUtil.close(pw, sw);
+        }
+    }
+}

+ 444 - 0
class-winter-core/src/main/java/net/jd/classwinter/util/IOUtil.java

@@ -0,0 +1,444 @@
+package net.jd.classwinter.util;
+
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.exception.ClassWinterException;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.zip.CRC32;
+
+/**
+ * IO工具类
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/4/23 20:40:47
+ */
+public final class IOUtil {
+    
+    /**
+     * 将srcFileBytes写出为destFile文件
+     * <p>
+     *     注: 若源文件存在,则会覆盖原有的内容。
+     * </p>
+     *
+     * @param srcFileBytes
+     *            字节
+     * @param destFile
+     *            文件
+     * @param createIfNecessary
+     *            如果需要的话,创建文件
+     */
+    public static void toFile(byte[] srcFileBytes, File destFile, boolean createIfNecessary) {
+        OutputStream os = null;
+        try {
+            if (destFile.isDirectory()) {
+                throw new ClassWinterException("destFile [" + destFile.getAbsolutePath() + "] must be file rather than dir.");
+            }
+            
+            if (createIfNecessary && !destFile.exists()) {
+                if (!destFile.getParentFile().exists()) {
+                    //noinspection ResultOfMethodCallIgnored
+                    destFile.getParentFile().mkdirs();
+                }
+                //noinspection ResultOfMethodCallIgnored
+                destFile.createNewFile();
+            } else if (!destFile.exists()) {
+                throw new IllegalArgumentException("destFile [" + destFile.getAbsolutePath() + "] non exist.");
+            }
+            os = new FileOutputStream(destFile);
+            os.write(srcFileBytes, 0, srcFileBytes.length);
+            os.flush();
+        } catch (IOException e) {
+            throw new ClassWinterException(e);
+        } finally {
+            close(os);
+        }
+    }
+    
+    /**
+     * 读取文件
+     *
+     * @param file
+     *            文件
+     * @return  字节
+     */
+    public static byte[] toBytes(File file) {
+        try {
+            FileInputStream inputStream = new FileInputStream(file);
+            return toBytes(inputStream);
+        } catch (IOException e) {
+            throw new ClassWinterException(e);
+        }
+    }
+    
+    /**
+     * 将inputStream转换为byte[]
+     * <p>
+     *     注:此方法会释放inputStream
+     * </p>
+     *
+     * @param inputStream
+     *            输入流
+     * @return  字节
+     */
+    public static byte[] toBytes(InputStream inputStream) throws IOException {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        try {
+            byte[] buffer = new byte[4096];
+            int n;
+            while (-1 != (n = inputStream.read(buffer))) {
+                output.write(buffer, 0, n);
+            }
+            return output.toByteArray();
+        } finally {
+            close(output, inputStream);
+        }
+    }
+    /**
+     * 只罗列文件(即:只返回文件)
+     * <p>
+     *     注:dirOrFile对象本身也会被作为罗列对象。
+     * </p>
+     *
+     * @param dirOrFile
+     *            要罗列的文件夹(或者文件)
+     * @param suffix
+     *            要筛选的文件的后缀(若suffix为null, 则不作筛选)
+     *
+     * @return  罗列结果
+     */
+    public static List<File> listFileOnly(File dirOrFile, String... suffix) {
+        if (!dirOrFile.exists()) {
+            throw new IllegalArgumentException("listFileOnly [" + dirOrFile.getAbsolutePath() + "] non exist.");
+        }
+        return listFile(dirOrFile, 1).stream()
+                .filter(file -> {
+                    if (suffix == null) {
+                        return true;
+                    }
+                    String fileName = file.getName();
+                    return Arrays.stream(suffix).anyMatch(fileName::endsWith);
+                }).collect(Collectors.toList());
+    }
+    
+    /**
+     * 罗列所有子孙文件文件夹(不包含dirOrFile对象本身)
+     *
+     * @param dirOrFile
+     *            要罗列的文件夹(或者文件)
+     * @param mode
+     *            罗列模式(0-罗列文件和文件夹; 1-只罗列文件; 2-只罗列文件夹)
+     *
+     * @return  罗列结果
+     */
+    public static List<File> listSubFile(File dirOrFile, int mode) {
+        List<File> fileContainer = listFile(dirOrFile, mode);
+        String absolutePath = dirOrFile.getAbsolutePath();
+        Objects.requireNonNull(absolutePath, "absolutePath cannot be null.");
+        fileContainer = fileContainer.stream()
+                .filter(file -> !absolutePath.equals(file.getAbsolutePath()))
+                .collect(Collectors.toList());
+        return fileContainer;
+    }
+    
+    /**
+     * 罗列所有文件文件夹
+     * <p>
+     *     注:dirOrFile对象本身也会被作为罗列对象。
+     * </p>
+     *
+     * @param dirOrFile
+     *            要罗列的文件夹(或者文件)
+     * @param mode
+     *            罗列模式(0-罗列文件和文件夹; 1-只罗列文件; 2-只罗列文件夹)
+     *
+     * @return  罗列结果
+     */
+    public static List<File> listFile(File dirOrFile, int mode) {
+        List<File> fileContainer = new ArrayList<>(16);
+        listFile(dirOrFile, fileContainer, mode);
+        return fileContainer;
+    }
+    
+    /**
+     * 罗列所有文件文件夹
+     * <p>
+     *     注:dirOrFile对象本身也会被作为罗列对象。
+     * </p>
+     *
+     * @param dirOrFile
+     *            要罗列的文件夹(或者文件)
+     * @param fileContainer
+     *            罗列结果
+     * @param mode
+     *            罗列模式(0-罗列文件和文件夹; 1-只罗列文件; 2-只罗列文件夹)
+     */
+    public static void listFile(File dirOrFile, List<File> fileContainer, int mode) {
+        if (!dirOrFile.exists()) {
+            return;
+        }
+        if (mode != 0 && mode != 1 && mode != 2) {
+            throw new IllegalArgumentException("mode [" + mode + "] is non-supported. 0,1,2is only support.");
+        }
+        if (dirOrFile.isDirectory()) {
+            File[] files = dirOrFile.listFiles();
+            if (files != null) {
+                for (File f : files) {
+                    listFile(f, fileContainer, mode);
+                }
+            }
+            if (mode == 0 || mode == 2) {
+                fileContainer.add(dirOrFile);
+            }
+        } else {
+            if (mode == 0 || mode == 1) {
+                fileContainer.add(dirOrFile);
+            }
+        }
+    }
+    
+    /**
+     * 删除文件/文件夹
+     *
+     * @param dirOrFile
+     *            要删的除文件/文件夹
+     */
+    public static void delete(File dirOrFile) {
+        if (!dirOrFile.exists()) {
+            return;
+        }
+        if (dirOrFile.isFile()) {
+            boolean success = dirOrFile.delete();
+            if (!success) {
+                Logger.debug(IOUtil.class, "delete file [" + dirOrFile.getAbsolutePath() + "] fail.");
+            }
+        } else {
+            File[] files = dirOrFile.listFiles();
+            if (files != null) {
+                for (File f : files) {
+                    delete(f);
+                }
+            }
+        }
+        //noinspection ResultOfMethodCallIgnored
+        dirOrFile.delete();
+    }
+    
+    /**
+     * 赋值输入流数据至输出流
+     * <p>
+     *     注:src可以被多次copy。
+     *     注:若dest所代表的“文件”已存在,则会覆盖原来的数据。
+     * </p>
+     *
+     * @param src
+     *            输入流
+     * @param dest
+     *            输出流
+     * @return  复制了的字节数(字节大小)
+     */
+    public static int copy(InputStream src, OutputStream dest) throws IOException {
+        byte[] buffer = new byte[4096];
+        int count = 0;
+        int n;
+        while (-1 != (n = src.read(buffer))) {
+            dest.write(buffer, 0, n);
+            count += n;
+        }
+        return count;
+    }
+    
+    /**
+     * 计算字节的CRC32
+     *
+     * @param bytes
+     *            字节
+     * @return  CRC32值
+     */
+    public static long computeCrc32(byte[] bytes) {
+        CRC32 crc32 = new CRC32();
+        crc32.update(bytes);
+        return crc32.getValue();
+    }
+    
+    /**
+     * 将内容写入到文件
+     * <p>
+     *     注:若原文件存在,则会覆盖原文件中的内容。
+     * </p>
+     *
+     * @param content
+     *            内容
+     * @param file
+     *            文件
+     */
+    public static void writeContentToFile(String content, File file) {
+        BufferedWriter out = null;
+        try {
+            // 保证目录存在
+            if (!file.getParentFile().exists()) {
+                //noinspection ResultOfMethodCallIgnored
+                file.getParentFile().mkdirs();
+            }
+            // 保证文件存在
+            if (!file.exists()) {
+                //noinspection ResultOfMethodCallIgnored
+                file.createNewFile();
+            }
+            out = new BufferedWriter(new FileWriter(file));
+            out.write(content);
+            out.flush();
+        } catch (IOException e) {
+            throw new ClassWinterException(e);
+        } finally {
+            close(out);
+        }
+    }
+    
+    /**
+     * 读取文件内容
+     *
+     * @param file
+     *            文件
+     *
+     * @return 内容
+     */
+    public static String readContentFromFile(File file) {
+        StringBuilder content = new StringBuilder();
+        FileInputStream fileInputStream = null;
+        InputStreamReader read = null;
+        BufferedReader bufferedReader = null;
+        try {
+            fileInputStream = new FileInputStream(file);
+            read = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
+            bufferedReader = new BufferedReader(read);
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                content.append(line).append(System.lineSeparator());
+            }
+        } catch (IOException e) {
+            throw new ClassWinterException(e);
+        } finally {
+            close(bufferedReader, read, fileInputStream);
+        }
+        return content.toString();
+    }
+    
+    /**
+     * 关闭流
+     *
+     * @param ioArr
+     *            待关闭的io
+     */
+    public static void close(Closeable... ioArr) {
+        if (ioArr == null) {
+            return;
+        }
+        for (Closeable io : ioArr) {
+            if (io == null) {
+                continue;
+            }
+            try {
+                io.close();
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+    }
+    
+    /**
+     * 合并byte[]
+     *
+     * @param byteArr
+     *            字节数组
+     *
+     * @return 合并后的字节
+     */
+    public static byte[] mergeByte(byte[]... byteArr) {
+        int length = 0;
+        for (byte[] b : byteArr) {
+            length += b.length;
+        }
+        byte[] bt = new byte[length];
+        int lastLength = 0;
+        for (byte[] b : byteArr) {
+            System.arraycopy(b, 0, bt, lastLength, b.length);
+            lastLength += b.length;
+        }
+        return bt;
+    }
+    
+    
+    /**
+     * 从jar文件或目录中读取文件字节
+     *
+     * @param workbenchRoot
+     *            jar文件或目录(如: /tmp/, /tmp/abc.jar等)
+     * @param relativeFilePath
+     *            要读取的文件的相对路径(如:META-INF/winter/xyz.txt)
+     * @return  文件字节数组
+     */
+    public static byte[] readFileFromWorkbenchRoot(File workbenchRoot, String relativeFilePath) {
+        byte[] bytes;
+        // jar文件
+        if (workbenchRoot.isFile()) {
+            String workDirName = workbenchRoot.getName();
+            if (!workDirName.endsWith(Constant.JAR_SUFFIX)) {
+                throw new ClassWinterException("workDirName [" + workDirName + "] is not support.");
+            }
+            bytes = JarUtil.getFileFromZip(workbenchRoot, relativeFilePath);
+        } else {
+            // war解压的目录
+            File file = new File(workbenchRoot, relativeFilePath);
+            if (!file.exists()) {
+                throw new ClassWinterException("file [" + relativeFilePath + "] non-exist, at workbenchRoot [" + workbenchRoot + "].");
+            }
+            bytes = IOUtil.toBytes(file);
+        }
+        return bytes;
+    }
+    
+    /**
+     * 判断字节流bytes的内容是否是以CAFEBABE打头的
+     * 注:
+     * 每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件。所有Class文件,魔数均为0xCAFEBABE, 即: 这四个字节分别是 CA FE BA BE
+     * 十六进制的ca 等价于 十进制的-54
+     * 十六进制的fe 等价于 十进制的-2
+     * 十六进制的ba 等价于 十进制的-70
+     * 十六进制的be 等价于 十进制的-66
+     * 注:一个字节即为8个bit,大小上限为2的8次方, 16的2次方, 所以一个字节,可以通过8位二进制表示,也可以通过2位16进制表示,如上面的 CA FE BA BE就分别表示了4个字节。
+     */
+    public static boolean startWithCAFEBABE(byte[] bytes) {
+        if (bytes == null || bytes.length < 4) {
+            return false;
+        }
+        /*
+         * 转换后,空缺的高位会用f填充,而我们只需要比较后面的两位就行
+         * Integer.toHexString(-54) 结果为 ffffffca
+         * Integer.toHexString(-2) 结果为 fffffffe
+         * Integer.toHexString(-70) 结果为 ffffffba
+         * Integer.toHexString(-66) 结果为 ffffffbe
+         */
+        return Integer.toHexString(bytes[0]).toUpperCase(Locale.ENGLISH).endsWith("CA")
+               && Integer.toHexString(bytes[0]).toUpperCase(Locale.ENGLISH).endsWith("FE")
+                && Integer.toHexString(bytes[0]).toUpperCase(Locale.ENGLISH).endsWith("BA")
+                && Integer.toHexString(bytes[0]).toUpperCase(Locale.ENGLISH).endsWith("BE");
+    }
+}

+ 328 - 0
class-winter-core/src/main/java/net/jd/classwinter/util/JarUtil.java

@@ -0,0 +1,328 @@
+package net.jd.classwinter.util;
+
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.exception.ClassWinterException;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * jar/war操作工具类
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/4/25 21:58:52
+ */
+public final class JarUtil {
+    
+    /** 打包时需要忽略的(可能由操作系统产生的)文件 */
+    private static final String[] IGNORE_FILE_SUFFIX = {".DS_Store", "Thumbs.db"};
+    
+    /**
+     * 把目录压缩成jar(or war)
+     *
+     * @param srcDir
+     *            需要打包的目录(如 /tmp/demo-1.0.0/)
+     * @param targetJarOrWar
+     *            打包出的jar/war文件路径(如 /tmp/abc.jar)
+     * @return  打包出的jar/war文件路径
+     */
+    public static String doJarWar(String srcDir, String targetJarOrWar) {
+        File jarDirFile = new File(srcDir);
+        // 枚举jarDir下的所有文件以及目录
+        List<File> files = IOUtil.listSubFile(jarDirFile, 0);
+        ZipOutputStream zos = null;
+        OutputStream out = null;
+        try {
+            File generatedJar = new File(targetJarOrWar);
+            // 如果原来的jar已存在,则先删除原来的jar
+            if (generatedJar.exists()) {
+                IOUtil.delete(generatedJar);
+            }
+            // jar包里面的文件的起始"root"位置
+            int rootStartIndex = jarDirFile.getAbsolutePath().length() + 1;
+            out = new FileOutputStream(generatedJar);
+            zos = new ZipOutputStream(out);
+            for (File file : files) {
+                if (isIgnore(file)) {
+                    continue;
+                }
+                String fileName = file.getAbsolutePath().substring(rootStartIndex);
+                fileName = fileName.replace(File.separator, Constant.LINUX_FILE_SEPARATOR);
+                // 目录,添加一个目录entry
+                if (file.isDirectory()) {
+                    ZipEntry zipEntry = new ZipEntry(fileName + Constant.LINUX_FILE_SEPARATOR);
+                    zipEntry.setTime(System.currentTimeMillis());
+                    zos.putNextEntry(zipEntry);
+                    zos.closeEntry();
+                }
+                // jar文件, 需要写CRC32信息
+                else if (fileName.endsWith(Constant.JAR_SUFFIX)) {
+                    byte[] bytes = IOUtil.toBytes(file);
+                    ZipEntry ze = new ZipEntry(fileName);
+                    ze.setMethod(ZipEntry.STORED);
+                    ze.setSize(bytes.length);
+                    ze.setTime(System.currentTimeMillis());
+                    ze.setCrc(IOUtil.computeCrc32(bytes));
+                    zos.putNextEntry(ze);
+                    zos.write(bytes);
+                    zos.closeEntry();
+                }
+                // 其它文件直接写入
+                else {
+                    ZipEntry zipEntry = new ZipEntry(fileName);
+                    zipEntry.setTime(System.currentTimeMillis());
+                    zos.putNextEntry(zipEntry);
+                    byte[] bytes = IOUtil.toBytes(file);
+                    zos.write(bytes);
+                    zos.closeEntry();
+                }
+            }
+        } catch (IOException e) {
+            throw new ClassWinterException(e);
+        } finally {
+            IOUtil.close(zos, out);
+        }
+        return targetJarOrWar;
+    }
+    
+    /**
+     * 解压jar(or war)至指定的目录
+     *
+     * @see JarUtil#unJarWar(String, String, boolean, Collection)
+     */
+    public static <T extends Collection<String>> List<String> unJarWar(String jarWarPath, String targetDir) {
+        return unJarWar(jarWarPath, targetDir, true, null);
+    }
+    
+    /**
+     * 解压jar(or war)至指定的目录
+     *
+     * @param jarWarPath
+     *            待解压的jar(or war)文件
+     * @param targetDir
+     *            解压后文件放置的文件夹
+     * @param delOldTargetDirIfAlreadyExist
+     *            若targetDir已存在,是否先将原来的targetDir进行删除
+     * @param includeFiles
+     *            只解压指定的这些文件(若为null或者长度为0, 则解压所有文件)   如: ["MANIFEST.MF", "Abc.class"]
+     * @return  解压出来的文件(包含目录)的完整路径
+     */
+    public static <T extends Collection<String>> List<String> unJarWar(String jarWarPath, String targetDir,
+                                                                       boolean delOldTargetDirIfAlreadyExist,
+                                                                       T includeFiles) {
+        List<String> list = new ArrayList<>();
+        File target = new File(targetDir);
+        if (delOldTargetDirIfAlreadyExist) {
+            IOUtil.delete(target);
+        }
+        guarantyDirExist(target);
+        
+        ZipFile zipFile = null;
+        try {
+            zipFile = new ZipFile(new File(jarWarPath));
+            ZipEntry entry;
+            File targetFile;
+            Enumeration<? extends ZipEntry> entries = zipFile.entries();
+            while (entries.hasMoreElements()) {
+                entry = entries.nextElement();
+                if (entry.isDirectory()) {
+                    targetFile = new File(target, entry.getName());
+                    guarantyDirExist(targetFile);
+                } else {
+                    // 有时遍历时,文件先于文件夹出来,所以也得保证目录存在
+                    int lastSeparatorIndex = entry.getName().lastIndexOf(Constant.LINUX_FILE_SEPARATOR);
+                    if (lastSeparatorIndex > 0) {
+                        guarantyDirExist(new File(target, entry.getName().substring(0, lastSeparatorIndex)));
+                    }
+                    // 解压文件
+                    targetFile = new File(target, entry.getName());
+                    // 若includeFiles不为空,则跳过未包含的文件
+                    if (includeFiles != null && includeFiles.size() > 0 && !includeFiles.contains(targetFile.getName())) {
+                        continue;
+                    }
+                    byte[] bytes = IOUtil.toBytes(zipFile.getInputStream(entry));
+                    IOUtil.toFile(bytes, targetFile, true);
+                    list.add(targetFile.getAbsolutePath());
+                }
+            }
+        } catch (IOException e) {
+            throw new ClassWinterException(e);
+        } finally {
+            IOUtil.close(zipFile);
+        }
+        return list;
+    }
+    
+    /**
+     * 保证目录存在
+     *
+     * @param dir
+     *            目录
+     */
+    public static void guarantyDirExist(File dir) {
+        if (!dir.exists()) {
+            //noinspection ResultOfMethodCallIgnored
+            dir.mkdirs();
+        }
+    }
+    
+    /**
+     * 从(zip/jar)压缩文件中获取文件
+     * <p>
+     *     注:jar其实也是zip。
+     * </p>
+     * <p>
+     *     示例
+     *     <code>
+     *         String path = JarUtil.getFileFromJar("/tmp/demo.jar", "META-INF/services/com.niantou.filter.HttpExtFilter", new File("/root/HttpExtFilter.txt"));
+     *     </code>
+     * </p>
+     *
+     * @param zip
+     *            压缩文件
+     * @param fileName
+     *            压缩文件的(相对压缩文件的root的)相对路径文件名
+     * @param targetFile
+     *            获取出来的目标文件
+     * @return  获取出来的目标文件的绝对路径
+     */
+    public static String getFileFromZip(File zip, String fileName, File targetFile) {
+        byte[] bytes = getFileFromZip(zip, fileName);
+        IOUtil.toFile(bytes, targetFile, true);
+        return targetFile.getAbsolutePath();
+    }
+    
+    /**
+     * 从(zip/jar/war)压缩文件中获取一个文件的字节
+     * <p>
+     *     注:jar其实也是zip。 war虽然不是zip,但是也是可以使用压缩/解压zip的方式来进行压缩解压的。
+     * </p>
+     * <p>
+     *     示例
+     *     <code>
+     *         byte[] bytes = JarUtil.getFileFromJar("/tmp/demo.jar", "META-INF/services/com.niantou.filter.HttpExtFilter");
+     *     </code>
+     * </p>
+     *
+     * @param zip
+     *            压缩文件
+     * @param fileName
+     *            压缩文件的(相对压缩文件的root的)相对路径文件名
+     * @return  文件字节
+     */
+    public static byte[] getFileFromZip(File zip, String fileName) {
+        ZipFile zipFile = null;
+        InputStream is = null;
+        try {
+            if (!zip.exists()) {
+                return null;
+            }
+            zipFile = new ZipFile(zip);
+            ZipEntry zipEntry = zipFile.getEntry(fileName);
+            if (zipEntry == null) {
+                return null;
+            }
+            is = zipFile.getInputStream(zipEntry);
+            return IOUtil.toBytes(is);
+        } catch (IOException e) {
+            throw new ClassWinterException(e);
+        } finally {
+            IOUtil.close(is, zipFile);
+        }
+    
+    }
+    
+    /**
+     * 判断originJarOrWar是jar文件还是war文件
+     *
+     * @param originJarOrWar
+     *            jar或者war的文件名(或全路径文件名)
+     * @return  true-jar包; false-war包
+     * @throws  IllegalArgumentException 当originJarOrWar既不是jar文件又不是war文件时抛出
+     */
+    public static boolean isJarOrWar(String originJarOrWar) throws IllegalArgumentException {
+        if (originJarOrWar.endsWith(Constant.JAR_SUFFIX)) {
+            return true;
+        }
+        if (originJarOrWar.endsWith(Constant.WAR_SUFFIX)) {
+            return false;
+        }
+        throw new IllegalArgumentException("suffix-file [" + originJarOrWar + "] is not support.");
+    }
+    
+    /**
+     * 是否忽略这个文件
+     *
+     * @param file
+     *            文件
+     * @return  是否忽略这个文件
+     */
+    private static boolean isIgnore(File file) {
+        for (String suffix : IGNORE_FILE_SUFFIX) {
+            if (file.getAbsolutePath().endsWith(suffix)) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * 获取当前类对应的其所在的classes目录全路径名(或其所在jar包/war包文件全路径名)
+     *
+     * @param projectPath
+     *            当前类对应的项目路径
+     * @return  当前类对应的其所在的classes目录全路径名(或其所在jar包/war包文件全路径名)
+     */
+    public static String getRootPath(String projectPath) {
+        Objects.requireNonNull(projectPath, "projectPath cannot be null.");
+        try {
+            projectPath = URLDecoder.decode(projectPath, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException e) {
+            // ignore
+        }
+        if (projectPath.startsWith(Constant.JAR_PROTOCOL) || projectPath.startsWith(Constant.WAR_PROTOCOL)) {
+            // jar协议/war协议的协议声明长度为4
+            projectPath = projectPath.substring(4);
+        }
+        if (projectPath.startsWith(Constant.FILE_PROTOCOL)) {
+            // file协议的协议声明长度为5
+            projectPath = projectPath.substring(5);
+        }
+        // 没解压的war包
+        if (projectPath.contains("*")) {
+            return projectPath.substring(0, projectPath.indexOf("*"));
+        } 
+        // war包解压后的WEB-INF目录
+        else if (projectPath.contains(Constant.WEB_INF)) {
+            return projectPath.substring(0, projectPath.indexOf(Constant.WEB_INF));
+        }
+        // jar(jar包中文件URL有专用的格式jar:!/{entry}, 所以如果包含!,说明也是jar包)
+        else if (projectPath.contains(Constant.JAR_FILE_URL_SPECIAL_SIGN)) {
+            return projectPath.substring(0, projectPath.indexOf(Constant.JAR_FILE_URL_SPECIAL_SIGN));
+        }
+        // 普通jar/war
+        else if (projectPath.endsWith(Constant.JAR_SUFFIX) || projectPath.endsWith(Constant.WAR_SUFFIX)) {
+            return projectPath;
+        }
+        // no (项目还未打包时,存放于/classes/下)
+        else if (projectPath.contains(Constant.CLASSES_DIR)) {
+            return projectPath.substring(0, projectPath.indexOf(Constant.CLASSES_DIR) + Constant.CLASSES_DIR.length());
+        }
+        throw new ClassWinterException("cannot identify projectPath [" + projectPath + "].");
+    }
+}

+ 80 - 0
class-winter-core/src/main/java/net/jd/classwinter/util/JavaagentCmdArgs.java

@@ -0,0 +1,80 @@
+package net.jd.classwinter.util;
+
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.exception.ClassWinterException;
+
+/**
+ * 在使用class-winter进行解密时,如果不需要输入参数,那么可以直接java -javaagent:xxx.jar -jar xxx.jar
+ * 如果需要传参,下面两种方式都可以
+ *     java -javaagent:xxx.jar="k1=v1,k2=v2" -jar xxx.jar
+ *     java -javaagent:xxx.jar=k1=v1,k2=v2 -jar xxx.jar
+ *
+ *  注: 这里k...只支持本类中的属性password和debug。
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/6/1 22:15:28
+ */
+public class JavaagentCmdArgs {
+    
+    private String password;
+    
+    private boolean debug;
+    
+    public String getPassword() {
+        return password;
+    }
+    
+    public void setPassword(String password) {
+        this.password = password;
+    }
+    
+    public boolean isDebug() {
+        return debug;
+    }
+    
+    public void setDebug(boolean debug) {
+        this.debug = debug;
+    }
+    
+    @Override
+    public String toString() {
+        return "JavaagentCmdArgs{" +
+                "password='" + password + '\'' +
+                ", debug=" + debug +
+                '}';
+    }
+    
+    /**
+     * 解析java -javaagent:xxx.jar="k1=v1,k2=v2" -jar xxx.jar中的k1=v1,k2=v2字符串成JavaagentCmdArgs对象
+     *
+     * <p>
+     *  注:根据本方法的逻辑,如果key重复,那么后面的value值会覆盖前面的。
+     *  注:根据本方法的逻辑,如果debug的值不为true,那么其值就为false。
+     *
+     * @param args
+     *            使用javaagent代理时,输入的参数, 如: k1=v1,k2=v2
+     * @return 解析出来的对象(这个对象本身一定不为null)
+     */
+    public static JavaagentCmdArgs parseJavaagentCmdArgs(String args) {
+        if (args == null) {
+            return new JavaagentCmdArgs();
+        }
+        if (args.contains(" ")) {
+            throw new ClassWinterException("-javaagent args [" + args + "] cannot contain whitespace.");
+        }
+        // args形如 k1=v1,k2=v2
+        String[] keyValueArr = args.split(",");
+        JavaagentCmdArgs instance = new JavaagentCmdArgs();
+        for (String keyValue : keyValueArr) {
+            keyValue = keyValue.trim();
+            if (keyValue.startsWith("password=")) {
+                instance.setPassword(keyValue.substring("password=".length()));
+                continue;
+            }
+            if (keyValue.startsWith("debug=")) {
+                instance.setDebug(Boolean.parseBoolean(keyValue.substring("debug=".length())));
+            }
+        }
+        return instance;
+    }
+}

+ 311 - 0
class-winter-core/src/main/java/net/jd/classwinter/util/JavassistUtil.java

@@ -0,0 +1,311 @@
+package net.jd.classwinter.util;
+
+import javassist.CannotCompileException;
+import javassist.ClassPool;
+import javassist.CtClass;
+import javassist.CtMethod;
+import javassist.Modifier;
+import javassist.NotFoundException;
+import javassist.bytecode.BadBytecode;
+import javassist.bytecode.Bytecode;
+import javassist.bytecode.CodeAttribute;
+import javassist.bytecode.CodeIterator;
+import javassist.bytecode.Descriptor;
+import javassist.bytecode.ExceptionTable;
+import javassist.compiler.CompileError;
+import javassist.compiler.Javac;
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.exception.ClassWinterException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 字节码操作工具类
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/4/25 20:54:30
+ */
+public final class JavassistUtil {
+    
+    /**
+     * 被清空了方法体的方法内部的提示信息
+     */
+    public static String TIPS = "ERROR !!!!!!!!!!! Jar(or War) has been protected by class-winter. Please use javaagent re-start project. !!!!!!!!!!!";
+    
+    /**
+     * 清空类中的方法体,并简单处理一下main,使提示密码无效
+     *
+     * @param classPool
+     *            javassist的classPool
+     * @param className
+     *            要修改的class类的全类名
+     * @param keepOriginArgsName
+     *            是否保留原方法的参数名
+     *
+     * @return  处理后的类的字节
+     * @throws NotFoundException 当清空的类中涉及到了某些其他的类,但是根本就没有引入(这些其他的类)相应的依赖时,就会抛出此异常(即:这个类本身就报错来着)
+     *                           注:在编写项目A时,如果引入的依赖B的scope范围是provided时,那么当另一个项目X,引入A的依赖时,B是不会被依赖传递到X的,
+     *                               就会出现上面的情况。
+     *                           注:在某些其它情况下,也会抛出此异常。
+     * @throws  CannotCompileException 统一封装异常
+     */
+    public static byte[] clearMethodBody(ClassPool classPool, String className, boolean keepOriginArgsName) throws CannotCompileException, NotFoundException {
+        CtClass ctClass;
+        try {
+            ctClass = classPool.getCtClass(className);
+            CtMethod[] methods = ctClass.getDeclaredMethods();
+            for (CtMethod ctMethod : methods) {
+                // 当前类中的方法 && 非构造方法
+                if (ctMethod.getLongName().startsWith(className) && !ctMethod.getName().contains("<")) {
+                    if (keepOriginArgsName) {
+                        CodeAttribute codeAttribute = ctMethod.getMethodInfo().getCodeAttribute();
+                        // 如果是接口,那么codeAttribute就是null; 方法体本来就是空的,codeAttribute.getCode()[0]就是-79
+                        if (codeAttribute != null && codeAttribute.getCodeLength() != 1 && codeAttribute.getCode()[0] != 79) {
+                            clearMethodBodyKeepOriginArgName(ctMethod);
+                        }
+                    } else {
+                        ctMethod.setBody(null);
+                    }
+                    /// 顺带处理一下main方法:提示jar包已经被保护,请使用javaagent启动项目
+                    /// if (isMain(ctMethod)) {
+                    ///     ctMethod.insertBefore(
+                    ///             "System.out.println(\"\\n " + TIPS +" \\n\");"
+                    ///                     + "\nSystem.exit(-1);"
+                    ///     );
+                    /// }
+                    try {
+                        ctMethod.insertBefore(
+                                "System.out.println(\"\\n " + TIPS +" \\n\");"
+                                + "\nSystem.exit(-1);"
+                                // 运行时,这一行内容不会被输出的
+                                + "\nSystem.out.println(\"" + Constant.SEAL +"\");"
+                        );
+                    } catch (CannotCompileException e) {
+                        if ("no method body".equals(e.getMessage())) {
+                            Logger.debug(JavassistUtil.class, "[" + ctMethod.getLongName() + "] no method body. ignore to add tips info.");
+                            // ignore
+                        } else {
+                            throw e;
+                        }
+                    }
+                }
+            }
+            return ctClass.toBytecode();
+        } catch (IOException e) {
+            throw new ClassWinterException(e);
+        }
+    }
+    
+    /**
+     * 加载paths指定的.jar文件(或加载对应子孙目录下的所有jar包)
+     *
+     * @param classPool
+     *            javassist的classPool
+     * @param paths
+     *            要加载的文件夹或jar包
+     */
+    public static void loadJar(ClassPool classPool, String... paths) throws NotFoundException {
+        if (paths == null) {
+            return;
+        }
+        for (String path : paths) {
+            loadJar(classPool, new File(path));
+        }
+    }
+    
+    /**
+     * 加载指定的.class文件(或加载对应子孙目录下的所有.class文件)
+     *
+     * @param classPool
+     *            javassist的classPool
+     * @param paths
+     *            要加载的文件夹或.class文件
+     */
+    public static void loadClass(ClassPool classPool, String... paths) throws NotFoundException {
+        if (paths == null) {
+            return;
+        }
+        for (String path : paths) {
+            loadClasses(classPool, new File(path));
+        }
+    }
+    
+    /**
+     * 判断ctMethod是否代表main方法
+     */
+    private static boolean isMain(CtMethod ctMethod) throws NotFoundException {
+        // 返回值校验
+        boolean returnValueValid = "void".equalsIgnoreCase(ctMethod.getReturnType().getName());
+        // 方法名、参数类型校验
+        boolean nameArgTypeValid = ctMethod.getLongName().endsWith(".main(java.lang.String[])");
+        // 访问修饰符校验
+        boolean accessFlag = ctMethod.getMethodInfo().getAccessFlags() == 9;
+        return returnValueValid && nameArgTypeValid && accessFlag;
+    }
+    
+    
+    /**
+     * 清空方法体,并且保留原参数名
+     *
+     * @param ctMethod
+     *            javassist的方法对象
+     * @throws CannotCompileException 编译异常
+     * @throws NotFoundException 确实类异常
+     */
+    private static void clearMethodBodyKeepOriginArgName(CtMethod ctMethod) throws CannotCompileException, NotFoundException {
+        CtClass ctClass = ctMethod.getDeclaringClass();
+        if (ctClass.isFrozen()) {
+            throw new ClassWinterException(ctClass.getName() + " class is frozen.");
+        }
+        CodeAttribute codeAttribute = ctMethod.getMethodInfo().getCodeAttribute();
+        if (codeAttribute == null) {
+            throw new ClassWinterException("no method body.");
+        } else {
+            CodeIterator iterator = codeAttribute.iterator();
+            Javac jv = new Javac(ctClass);
+            try {
+                int nvars = jv.recordParams(ctMethod.getParameterTypes(), Modifier.isStatic(ctMethod.getModifiers()));
+                jv.recordParamNames(codeAttribute, nvars);
+                jv.recordLocalVariables(codeAttribute, 0);
+                jv.recordReturnType(Descriptor.getReturnType(ctMethod.getMethodInfo().getDescriptor(),
+                        ctClass.getClassPool()), false);
+                Bytecode bytecode = jv.compileBody(ctMethod, null);
+                int maxStack = bytecode.getMaxStack();
+                if (maxStack > codeAttribute.getMaxStack()) {
+                    codeAttribute.setMaxStack(maxStack);
+                }
+                int maxLocals = bytecode.getMaxLocals();
+                if (maxLocals > codeAttribute.getMaxLocals()) {
+                    codeAttribute.setMaxLocals(maxLocals);
+                }
+                iterator.insertEx(bytecode.get());
+                /*
+                 * 移除ExceptionTable中的所有项.
+                 *
+                 * 注: 因为都把方法体置空了,里面的所有异常(try-catch)处理都没了,那么方法体当然需要要置空。
+                 * 注: 一个方法的exception table中的entry数量等于这个方法内部catch了多少次异常,
+                 *      如: ...catch (AbcException e)... ,那么entry个数为1
+                 *      如: ...catch (AbcException|XyzException e)... ,那么entry个数为2
+                 *      如: ...catch (AbcException e)... ,   ...catch (QwerException e)..那么entry个数为2
+                 * <p>
+                 * exception table 表示异常表,异常表是用于存储代码中涉及到的所有异常,每个类编译后,都会跟随一个异常表,如果发生异常,首先在异常表中查找对应的行(即代码中相应的 try{}catch(){}代码块),如果找到,则跳转到异常处理代码执行,如果没有找到,则返回(执行 finally 之后),并 copy 异常的应用给父调用者,接着查询父调用的异常表,以此类推。
+                 */
+                ExceptionTable exceptionTable = codeAttribute.getExceptionTable();
+                if (exceptionTable != null) {
+                    int size = exceptionTable.size();
+                    for (int i = size - 1; i >= 0; i--) {
+                        exceptionTable.remove(i);
+                    }
+                }
+                ctMethod.getMethodInfo().rebuildStackMapIf6(ctClass.getClassPool(), ctClass.getClassFile2());
+            } catch (CompileError compileError) {
+                throw new CannotCompileException(compileError);
+            } catch (BadBytecode badBytecode) {
+                throw new CannotCompileException(badBytecode);
+            }
+        }
+    }
+    
+    /**
+     * 加载指定的jar包(或加载对应子孙目录下的所有jar包)
+     *
+     * @param pool
+     *            javassist的ClassPool
+     * @param dirOrFile
+     *            要加载的文件夹或.jar文件
+     */
+    private static void loadJar(ClassPool pool, File dirOrFile) throws NotFoundException {
+        if (dirOrFile == null || !dirOrFile.exists()) {
+            return;
+        }
+        List<File> jars = IOUtil.listFileOnly(dirOrFile, Constant.JAR_SUFFIX);
+        for (File jar : jars) {
+            pool.insertClassPath(jar.getAbsolutePath());
+        }
+    }
+    
+    /**
+     * 加载指定的.class文件(或加载对应子孙目录下的所有.class文件)
+     *
+     * @param pool
+     *            javassist的ClassPool
+     * @param dirOrFile
+     *            要加载的文件夹或.class文件
+     */
+    private static void loadClasses(ClassPool pool, File dirOrFile) throws NotFoundException {
+        if (dirOrFile == null || !dirOrFile.exists()) {
+            return;
+        }
+        /*
+         * 获取.class全类名对应的目录的根目录
+         * 假设class文件全路径为/tmp/classes/com/niantou/iwork/core/Abc.class
+         * 那么这里获取到的就是/tmp/classes
+         */
+        Set<String> classesRootDirSet = IOUtil.listFileOnly(dirOrFile, Constant.CLASS_SUFFIX)
+                .stream().map(x -> resolveClassName(x.getAbsolutePath(), false))
+                .collect(Collectors.toSet());
+        for (String rootDir : classesRootDirSet) {
+            /*
+             * pathname – the path name of the directory or jar file. It must not end with a path separator ("/").
+             * If the path name ends with "/*", then all the jar files matching the path name are inserted
+             */
+            pool.insertClassPath(rootDir);
+        }
+    }
+
+    /**
+     * 根据class的绝对路径解析出class全类名(或class全类名文件所在的目录路径)
+     * <p>
+     *     假设文件的全路径名是这样的/tmp/class-winter-core/src/main/java/com/niantou/iwork/core/Abc.class,
+     *     那么, 解析出来的class全类名即为com.niantou.iwork.core.Abc
+     *          解析出来的class全类名文件所在的目录路径即为/tmp/class-winter-core/src/main/java
+     *
+     * </p>
+     *
+     * @param fileName
+     *            class文件的绝对路径
+     * @param classOrPath
+     *            true-解析全类名;false-解析全类名文件所在的目录路径
+     * @return  class所代表类的全类名(如com.aaa.bbb.Abc) 或者路径(如: /tmp/class-winter-core/src/main/java)
+     */
+    public static String resolveClassName(String fileName, boolean classOrPath) {
+        // 去除后缀名
+        String nonSuffixFileName = fileName.substring(0, fileName.length() - Constant.CLASS_SUFFIX.length());
+        String classesFlag = File.separator + "classes" + File.separator;
+        String libFlag = File.separator + "lib" + File.separator;
+        String classPath;
+        String className;
+        int libFlagIndex = nonSuffixFileName.indexOf(libFlag, nonSuffixFileName.indexOf(Constant.TMP_DIR_SUFFIX));
+        int classesFlagIndex = nonSuffixFileName.indexOf(classesFlag, nonSuffixFileName.indexOf(Constant.TMP_DIR_SUFFIX));
+        // lib内的jar包
+        if (libFlagIndex >= 0) {
+            /*
+             * 如果是对jar包(如my-project-1.0.0.jar)中的某些lib(如cglib-3.1.jar)还需要进行加密的话,那么根据本项目class-winter的解压逻辑,
+             * 解压后的目录就会存在形如下面这样的解压临时路径
+             * /tmp/abc/my-project-1.0.0__temp__/BOOT-INF/lib/cglib-3.1__temp__/com/aaa/bbb/Abc.class;
+             * 所以要从lib开始找__temp__,
+             * 然后再跳过__temp__本身的长度
+             */
+            className = nonSuffixFileName.substring(nonSuffixFileName.indexOf(Constant.TMP_DIR_SUFFIX, libFlagIndex) + Constant.TMP_DIR_SUFFIX.length() + 1);
+        }
+        // jar/war包xxx-INF/classes下的class文件
+        else if (classesFlagIndex >= 0) {
+            /*
+             * nonSuffixFileName为/tmp/abc/my-project-1.0.0__temp__/BOOT-INF/classes/com/aspire/ssm/Aaaaaaaaa
+             * 获取到的className为
+             */
+            className = nonSuffixFileName.substring(classesFlagIndex + classesFlag.length());
+        }
+        // jar包下的class文件
+        else {
+            className = nonSuffixFileName.substring(nonSuffixFileName.indexOf(Constant.TMP_DIR_SUFFIX) + Constant.TMP_DIR_SUFFIX.length() + 1);
+        }
+        classPath = nonSuffixFileName.substring(0, nonSuffixFileName.length() - className.length() - 1);
+        return classOrPath ? className.replace(File.separator, ".") : classPath;
+    }
+
+}

+ 67 - 0
class-winter-core/src/main/java/net/jd/classwinter/util/Logger.java

@@ -0,0 +1,67 @@
+package net.jd.classwinter.util;
+
+import net.jd.classwinter.author.JustryDeng;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * 日志
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/4/26 23:06:15
+ */
+public final class Logger {
+    
+    private Logger() {
+    }
+    
+    public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    
+    /** debug模式 */
+    public static final AtomicBoolean ENABLE_DEBUG = new AtomicBoolean(false);
+    
+    /**
+     * 输出debug信息
+     *
+     * @param msg
+     *            信息
+     */
+    public static void debug(Class<?> clazz, String msg) {
+        if (ENABLE_DEBUG.get()) {
+            String log = DATE_TIME_FORMATTER.format(LocalDateTime.now()) + " [DEBUG] " + clazz.getName() + ": " + msg;
+            System.out.println(log);
+        }
+    }
+    
+    /**
+     * 输出信息
+     *
+     * @param msg
+     *            信息
+     */
+    public static void info(Class<?> clazz, String msg) {
+        System.out.println(DATE_TIME_FORMATTER.format(LocalDateTime.now()) + " [ INFO] " + clazz.getName() + ": " + msg);
+    }
+    
+    /**
+     * 输出信息
+     *
+     * @param msg
+     *            信息
+     */
+    public static void warn(Class<?> clazz, String msg) {
+        System.out.println(DATE_TIME_FORMATTER.format(LocalDateTime.now()) + " [ WARN] " + clazz.getName() + ": " + msg);
+    }
+    
+    /**
+     * 输出信息
+     *
+     * @param msg
+     *            信息
+     */
+    public static void error(Class<?> clazz, String msg) {
+        System.out.println(DATE_TIME_FORMATTER.format(LocalDateTime.now()) + " [ERROR] " + clazz.getName() + ": " + msg);
+    }
+}

+ 60 - 0
class-winter-core/src/main/java/net/jd/classwinter/util/Pair.java

@@ -0,0 +1,60 @@
+package net.jd.classwinter.util;
+
+import net.jd.classwinter.author.JustryDeng;
+
+import java.util.Objects;
+
+/**
+ * (non-javadoc)
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/6/5 11:19:21
+ */
+public class Pair<K, V> {
+    
+    private final K left;
+    
+    private final V right;
+    
+    private Pair(K left, V right) {
+        this.left = left;
+        this.right = right;
+    }
+    
+    public K getLeft() {
+        return this.left;
+    }
+    
+    public V getRight() {
+        return this.right;
+    }
+    
+    public static <L, R> Pair<L, R> of(final L left, final R right) {
+        return new Pair<>(left, right);
+    }
+    
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        Pair<?, ?> pair = (Pair<?, ?>) o;
+        return Objects.equals(left, pair.left) && Objects.equals(right, pair.right);
+    }
+    
+    @Override
+    public int hashCode() {
+        return Objects.hash(left, right);
+    }
+    
+    @Override
+    public String toString() {
+        return "Pair{" +
+                "left=" + left +
+                ", right=" + right +
+                '}';
+    }
+}

+ 46 - 0
class-winter-core/src/main/java/net/jd/classwinter/util/PathUtil.java

@@ -0,0 +1,46 @@
+package net.jd.classwinter.util;
+
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.exception.ClassWinterException;
+
+import java.io.File;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 路径util
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/4/26 23:11:04
+ */
+public final class PathUtil {
+    
+    /**
+     * 获取clazz类的全类名对应包的根路径
+     *
+     * <ul>
+     *     <li>如果编译后未打包时直接调用这个方法(即:直接在开发工具里调用此方法),那么结果形如:   D:/资料整理/demo模板/class-winter/class-winter-core/target/classes/</li>
+     *     <li>如果是jar包运行时,(clazz位于jar包中,)调用这个方法,(假设jar包位置是D:/资料整理/demo模板/abc.jar)那么结果形如:   D:/资料整理/demo模板/abc.jar</li>
+     * </ul>
+     *
+     * @return  项目根目录
+     */
+    public static String getProjectRootDir(Class<?> clazz) {
+        URL url = clazz.getProtectionDomain().getCodeSource().getLocation();
+        String filePath;
+        try {
+            filePath = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException e) {
+            throw new ClassWinterException(e);
+        }
+        File file = new File(filePath);
+        filePath = file.getAbsolutePath().replace("\\", "/");
+        // 如果file是文件夹,那么保证filePath是以/结尾的
+        if (file.isDirectory() && !filePath.endsWith("/")) {
+            filePath = filePath + "/";
+        }
+        return filePath;
+    }
+}

+ 125 - 0
class-winter-core/src/main/java/net/jd/classwinter/util/RsaUtil.java

@@ -0,0 +1,125 @@
+package net.jd.classwinter.util;
+
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.exception.ClassWinterException;
+
+import javax.crypto.Cipher;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+import java.util.Objects;
+
+/**
+ * RSA 3072加解密
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/6/5 11:08:40
+ */
+public class RsaUtil {
+    
+    /**
+     * 随机生成密钥对
+     *
+     * @return left-公钥;right-私钥
+     */
+    public static Pair<String, String> genKeyPair() {
+        KeyPairGenerator keyPairGen;
+        try {
+            // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象
+            keyPairGen = KeyPairGenerator.getInstance("RSA");
+        } catch (NoSuchAlgorithmException e) {
+            throw new ClassWinterException(e);
+        }
+        // 初始化密钥对生成器,密钥大小为3072位
+        keyPairGen.initialize(3072, new SecureRandom());
+        // 生成一个密钥对,保存在keyPair中
+        KeyPair keyPair = keyPairGen.generateKeyPair();
+        // 得到私钥
+        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
+        // 得到公钥
+        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
+        // 得到私钥字符串
+        String publicKeyString = new String(Base64.getEncoder().encode(publicKey.getEncoded()), StandardCharsets.UTF_8);
+        String privateKeyString = new String(Base64.getEncoder().encode((privateKey.getEncoded())), StandardCharsets.UTF_8);
+        return Pair.of(publicKeyString, privateKeyString);
+    }
+    
+    /**
+     * RSA公钥加密
+     *
+     * @param str
+     *            要加密的字符串
+     * @param publicKey
+     *            公钥
+     *
+     * @return 密文
+     */
+    public static String encrypt(String str, String publicKey) {
+        Objects.requireNonNull(str, "str cannot be empty.");
+        Objects.requireNonNull(publicKey, "publicKey cannot be empty.");
+        //base64编码的公钥
+        byte[] decoded = Base64.getDecoder().decode(publicKey);
+        try {
+            RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
+            //RSA加密
+            Cipher cipher = Cipher.getInstance("RSA");
+            cipher.init(Cipher.ENCRYPT_MODE, pubKey);
+            return Base64.getEncoder().encodeToString(cipher.doFinal(str.getBytes(StandardCharsets.UTF_8)));
+        } catch (Exception e) {
+            throw new ClassWinterException(e);
+        }
+    }
+    
+    /**
+     * RSA私钥解密
+     *
+     * @param cipherText
+     *            密文
+     * @param privateKey
+     *            私钥
+     *
+     * @return 明文
+     */
+    public static String decrypt(String cipherText, String privateKey) {
+        Objects.requireNonNull(cipherText, "cipherText cannot be empty.");
+        Objects.requireNonNull(privateKey, "privateKey cannot be empty.");
+        // 64位解码加密后的字符串
+        byte[] inputByte = Base64.getDecoder().decode(cipherText.getBytes(StandardCharsets.UTF_8));
+        // base64编码的私钥
+        byte[] decoded = Base64.getDecoder().decode(privateKey);
+        try {
+            RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
+            //RSA解密
+            Cipher cipher = Cipher.getInstance("RSA");
+            cipher.init(Cipher.DECRYPT_MODE, priKey);
+            return new String(cipher.doFinal(inputByte), StandardCharsets.UTF_8);
+        } catch (Exception e) {
+           throw new ClassWinterException(e);
+        }
+    }
+    
+//    /**
+//     * test
+//     */
+//    public static void main(String[] args) throws Exception {
+//        //生成公钥和私钥
+//        Pair<String, String> keyPair = genKeyPair();
+//        //加密字符串
+//        String message = "hello JustryDeng";
+//
+//        System.out.println("随机生成的公钥为: " + keyPair.getLeft());
+//        System.out.println("随机生成的私钥为: " + keyPair.getRight());
+//        String cipherText = encrypt(message, keyPair.getLeft());
+//        System.out.println(message + "\t加密后的字符串为: " + cipherText);
+//        String plainText = decrypt(cipherText, keyPair.getRight());
+//        System.out.println("还原后的字符串为: " + plainText);
+//    }
+}

+ 166 - 0
class-winter-core/src/main/java/net/jd/classwinter/util/StrUtil.java

@@ -0,0 +1,166 @@
+package net.jd.classwinter.util;
+
+import net.jd.classwinter.author.JustryDeng;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 字符串工具类
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/4/26 23:26:34
+ */
+public final class StrUtil {
+    
+    /**
+     * 判断字符串是否为空
+     *
+     * @param str
+     *            字符串
+     * @return  是否为空
+     */
+    public static boolean isEmpty(String str) {
+        return str == null || str.length() == 0;
+    }
+    
+    /**
+     * <p>Checks if a CharSequence is empty (""), null or whitespace only.</p>
+     *
+     * <p>Whitespace is defined by {@link Character#isWhitespace(char)}.</p>
+     *
+     * <pre>
+     * StringUtils.isBlank(null)      = true
+     * StringUtils.isBlank("")        = true
+     * StringUtils.isBlank(" ")       = true
+     * StringUtils.isBlank("bob")     = false
+     * StringUtils.isBlank("  bob  ") = false
+     * </pre>
+     *
+     * @param cs  the CharSequence to check, may be null
+     * @return {@code true} if the CharSequence is null, empty or whitespace only
+     */
+    public static boolean isBlank(final CharSequence cs) {
+        int strLen;
+        if (cs == null || (strLen = cs.length()) == 0) {
+            return true;
+        }
+        for (int i = 0; i < strLen; i++) {
+            if (!Character.isWhitespace(cs.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+    
+    /**
+     * 判断字符串是否为空
+     *
+     * @param str
+     *            字符串
+     * @return  是否为空
+     */
+    public static boolean isEmpty(char[] str) {
+        return str == null || str.length == 0;
+    }
+    
+    /**
+     * 在字符串的某行后插入字符串
+     *
+     * <p>
+     *     注:若origin中,存在多行的前缀是linePrefix,那么在第一个匹配行后面进行插入。
+     *     注:若origin中,不存在任何行的前缀是linePrefix,那么在最后插入。
+     * </p>
+     *
+     * @param origin
+     *            原(按行拆分后的)字符串数组
+     * @param insertStr
+     *            要插入的字符串
+     * @param linePrefix
+     *            定位insertStrAfterLine中Line的行前缀
+     * @return  插入后的字符串
+     */
+    public static String insertStrAfterLine(String[] origin, String insertStr, String linePrefix) {
+        StringBuilder newStr = new StringBuilder();
+        boolean alreadyInsert = false;
+        for (String str : origin) {
+            newStr.append(str).append(System.lineSeparator());
+            if (str.startsWith(linePrefix)) {
+                newStr.append(insertStr).append(System.lineSeparator());
+                alreadyInsert = true;
+            }
+        }
+        // 若origin中,不存在任何行的前缀是linePrefix,那么在最后插入
+        if (!alreadyInsert) {
+            newStr.append(insertStr).append(System.lineSeparator());
+        }
+        return newStr.toString();
+    }
+    
+    /**
+     * 将由逗号分割的信息分割到set里面
+     *
+     * @param str
+     *            逗号分割的字符串
+     * @return  承载各个前缀信息的集合
+     */
+    public static Set<String> strToSet(String str) {
+        Set<String> set = new HashSet<>();
+        if (!StrUtil.isBlank(str)) {
+            str = str.replace(" ", "");
+            String[] arr = str.split(",");
+            for (String item : arr) {
+                if (StrUtil.isEmpty(item)) {
+                    continue;
+                }
+                set.add(item);
+            }
+        }
+        return set;
+    }
+    
+    /**
+     * 合并char[]
+     *
+     * @param charArr
+     *            要合并的字符数组们
+     * @return  合并后的字符数组
+     */
+    public static char[] mergeChar(char[]... charArr) {
+        int length = 0;
+        for (char[] c : charArr) {
+            length += c.length;
+        }
+        char[] chars = new char[length];
+        int lastLength = 0;
+        for (char[] c : charArr) {
+            System.arraycopy(c, 0, chars, lastLength, c.length);
+            lastLength += c.length;
+        }
+        return chars;
+    }
+    
+    /**
+     * 字符数组转换成字节数组
+     *
+     * @param charArr
+     *            字符数组
+     * @return  字节数组
+     */
+    public static byte[] toBytes(char[] charArr) {
+        char[] chars0 = new char[charArr.length];
+        System.arraycopy(charArr, 0, chars0, 0, charArr.length);
+        CharBuffer charBuffer = CharBuffer.wrap(chars0);
+        ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
+        byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
+        // release resources \u0000 即代表一个空格
+        Arrays.fill(charBuffer.array(), '\u0000');
+        // release resources
+        Arrays.fill(byteBuffer.array(), (byte) 0);
+        return bytes;
+    }
+}

+ 91 - 0
class-winter-core/src/test/java/com/jd/classwinter/ForwardTest.java

@@ -0,0 +1,91 @@
+package com.jd.classwinter;
+
+import jdk.nashorn.internal.ir.annotations.Ignore;
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.executor.EncryptExecutor;
+import net.jd.classwinter.util.Constant;
+import net.jd.classwinter.util.Logger;
+import net.jd.classwinter.util.PathUtil;
+
+/**
+ * 正向加密
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/5/6 23:25:24
+ */
+public class ForwardTest {
+    
+    public static void main(String[] args) {
+        Logger.info(ForwardTest.class, encryptTest());
+    }
+    
+    /**
+     * 加密测试
+     */
+    public static String encryptTest() {
+        String projectRootDir = PathUtil.getProjectRootDir(ForwardTest.class);
+        
+        EncryptExecutor encryptExecutor = EncryptExecutor.builder()
+                //(必须)-> 加密spring-boot可执行jar包
+//                .originJarOrWar(projectRootDir + Constant.LINUX_FILE_SEPARATOR + "boot-jar.jar")
+                // 加密普通jar包
+                 .originJarOrWar(projectRootDir + "normal-jar.jar")
+                // 加密war包
+                // .originJarOrWar(projectRootDir + "war-project.war")
+                
+                // (必须)-> 前缀匹配的方式,指定需要加密的class
+                // 逗号后面有没有空格都可以
+                //.includePrefix("com.aspire.ssm.config,com.aspire.ssm.util,com.zaxxer.hikari,com.fasterxml.jackson")
+                .includePrefix("com.aspire.ssm")
+                
+                // (可选)-> 指定加密后的jar包的名称, 会在同目录下生成abc-encrypted.jar
+                // 若与原jar(or war)同名,则会覆盖原来的jar(or war)包.
+                // 若不指定,则生成的 加密后的jar(or war)包名为 = 原名-encrypted.jar
+                //.finalName("abc-encrypted")
+                
+                // (可选)-> 指定加密密码(若不指定,则系统会自动生成)
+                .password("qwer123~")
+                
+                // (可选)-> 前缀匹配的方式,指定需要忽略加密的class,用于对includePrefix指定的范围中的目标进行排除
+                // 逗号后面有没有空格都可以
+                //.excludePrefix("com.aspire.ssm.util.EmployeePO,com.aspire.ssm.util.ClazzDumpCustomAgent")
+                
+                // (可选)-> 前缀匹配的方式,指定还需额外对jar的依赖包libs中的哪些jar进行加密,更多解释详见net.jd.classwinter.executor.EncryptExecutor.includeLibSet上面的javadoc
+                // 逗号后面有没有空格都可以
+                //.includeLibs("HikariCP-3.2.0.jar,jackson-core-2.9.9.jar")
+                
+                // (可选)-> 提示信息,当用户直接启动项目而不使用javaagent启动时,程序提示tips信息后会停止
+                //.tips("方法已经被class-winter加密保护了,请不要直接使用java -jar xxx.jar启动项目,请使用java -javaagent:xxx.jar -jar xxx.jar启动项目.")
+        
+                // (可选)-> 指明当前项依赖来的lib中,有哪些lib是本身就已经是被class-winter混淆了的
+                //  格式形如 xxx.jar:password, 多个之间使用逗号分割, 如果当初对该lib进行混淆时没有主动指定密码,那么可以不写
+                //.alreadyProtectedLibs("encrypted-lib-no-pwd.jar")
+                //.alreadyProtectedLibs("aa.jar,bb.jar:pwd123,cc.jar:")
+        
+                // (可选)-> 加密时,额外往加密上下文中添加的jar文件(或者jar文件所在的目录)
+                // <p>
+                // 在进行加密时,某些类可能会因为缺失必要的依赖类而导致加密失败。就可以通过指定此字段的值(值应为某个jar或者某个目录的全路径),
+                // 这样一来,class-winter混淆目标jar时,除了加载要加载的jar/war内部的所有jar外,还会额外加载这里指定的jar(或者这里指定的目录下的所有子孙jar),
+                // 进而保证在加密时,不会因为确实依赖类而加密失败。
+                // <p>
+                // 注: 某些项目在打jar包时,是没有把其依赖的lib打进jar包中的,所以会出现[某些类可能会因为缺失必要的依赖类而导致加密失败]的情况。
+                // <p>
+                // 示例说明:
+                // 假设加密时日志报warn说
+                //     2021-06-11 20:24:31 [ WARN] net.jd.classwinter.executor.EncryptExecutor: Ignore clear-method-body for  className [com.aspire.ssm.handler.impl.DefaultAesPreHandlerImpl], Cannot find 'org.apache.ibatis.executor.parameter.ParameterHandler'
+                //     2021-06-11 20:24:31 [ WARN] net.jd.classwinter.executor.EncryptExecutor: Ignore clear-method-body for className [com.aspire.ssm.interceptor.CustomizeMybatisPlugin], Cannot find 'org.apache.ibatis.plugin.Invocation'
+                // 这说明了
+                //     DefaultAesPreHandlerImpl类中使用到了org.apache.ibatis.executor.parameter.ParameterHandler类,
+                //     DCustomizeMybatisPlugin类中使用到了org.apache.ibatis.plugin.Invocation类,
+                // 进一步说明了
+                //     要加密的jar包没有把mybatis的lib包打进去。
+                // 这时,我们就可以通过指定此字段的值为mybatisjar包的全路径名(如/tmp/lib/mybatis-3.5.1.jar)来解决问题。
+//                .supportFile("E:\\Maven\\Repository\\org\\mybatis\\mybatis\\3.5.1\\mybatis-3.5.1.jar")
+                
+                // (可选)-> 是否开启debug模型以获取更多日执行信息(默认为false)
+                .debug(true)
+                
+                .build();
+        return encryptExecutor.process();
+    }
+}

+ 58 - 0
class-winter-core/src/test/java/com/jd/classwinter/ProtectedLibTest.java

@@ -0,0 +1,58 @@
+package com.jd.classwinter;
+
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.executor.EncryptExecutor;
+import net.jd.classwinter.util.BashUtil;
+import net.jd.classwinter.util.Constant;
+import net.jd.classwinter.util.PathUtil;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * (non-javadoc)
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/6/5 16:40:06
+ */
+public class ProtectedLibTest {
+    
+    public static void main(String[] args) {
+        String projectRootDir = PathUtil.getProjectRootDir(ForwardTest.class);
+        EncryptExecutor encryptExecutor = EncryptExecutor.builder()
+//                .originJarOrWar(projectRootDir + "my-project-with-encrypted-lib-no-pwd.jar")
+                .originJarOrWar(projectRootDir + Constant.LINUX_FILE_SEPARATOR + "my-project-with-encrypted-lib-have-pwd.jar")
+                
+                .includePrefix("non-exist")
+        
+                // (可选)-> 指明当前项依赖来的lib中,有哪些lib是本身就已经是被class-winter混淆了的
+                //  格式形如 xxx.jar:password, 多个之间使用逗号分割, 如果当初对该lib进行混淆时没有主动指定密码,那么可以不写
+//                .alreadyProtectedLibs("encrypted-lib-no-pwd-1.0.0.jar")
+                .alreadyProtectedLibs("encrypted-lib-have-pwd-1.0.0.jar:qwer12~")
+                //.alreadyProtectedLibs("aa.jar,bb.jar:pwd123,cc.jar:")
+                
+                .debug(true)
+                .build();
+        
+        String encryptedJar = encryptExecutor.process();
+    
+        System.out.println(encryptedJar);
+//        /// - 不使用javaagent, 直接启动 - 测试
+//        /*
+//         * encryptedJar本身的代码是没有被混淆的,不过其依赖的lib [encrypted-lib-no-pwd-1.0.0.jar]被混淆了
+//         */
+//        BashUtil.runCmdAndPrint(String.format("java -jar %s",  encryptedJar));
+
+//        /// - 密码不正确 - 测试
+//        // 使用javaagent运行加密后的jar,测试
+//        String javaagentArgs = "";
+//        //String javaagentArgs = "=debug=true,password=xxx";
+//        BashUtil.runCmdAndPrint(String.format("java -javaagent:%s%s -jar %s", encryptedJar, javaagentArgs, encryptedJar));
+//
+//        try {
+//            TimeUnit.SECONDS.sleep(3);
+//        } catch (InterruptedException e) {
+//            // ignore
+//        }
+//        BashUtil.killProcessByPorts("8080");
+    }
+}

+ 91 - 0
class-winter-core/src/test/java/com/jd/classwinter/ReversesTest.java

@@ -0,0 +1,91 @@
+package com.jd.classwinter;
+
+import net.jd.classwinter.author.JustryDeng;
+import net.jd.classwinter.util.BashUtil;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 正向加密
+ *
+ * <p>
+ *     ********** 启动加密后的jar包 **********
+ *     java -javaagent:xxx.jar -jar xxx.jar
+ *     如果需要传参,下面两种方式都可以
+ *     java -javaagent:xxx.jar=k1=v1,k2=v2 -jar xxx.jar
+ *     java -javaagent:xxx.jar="k1=v1,k2=v2" -jar xxx.jar
+ * </p>
+ *
+ *
+ * <p>
+ * ********** 启动加密后的war包 **********
+ * for linux,
+ *     方式一: 在tomcat/bin目录下创建setenv.sh文件,并写上 set JAVA_OPTS="-javaagent:/xxx.jar=k1=v1,k2=v2"
+ *     方式二: 编辑tomcat/bin/catalina.sh文件,在内容的第一行加上
+ *            set CATALINA_OPTS="-javaagent:D:/xxx.jar=k1=v1,k2=v2"
+ *
+ * for windows,
+ *     方式一: 在tomcat/bin目录下创建setenv.bat文件,并写上 set JAVA_OPTS="-javaagent:D:/xxx.jar=k1=v1,k2=v2"
+ *     方式二: 编辑tomcat/bin/catalina.bat文件,在内容的第一行(即:@echo off后面的第一行)加上
+ *            set CATALINA_OPTS="-javaagent:D:/xxx.jar=k1=v1,k2=v2"
+ *
+ * 注: 对于方式二,其实不一定非要加在第一行的。
+ *
+ * 注: 其中,xxx.jar是一个agent jar, 首先class-winter-core本身是一个代理jar;其次,class-winter在做加密时,
+ *          也同时把class-winter-core里面的相关代理类拷贝到了加密后的jar中,所以加密后的jar也属于代理jar。
+ *     所以,这里 xxx.jar可以是 class-winter-core.jar, 也可以是 your-project-encrypted.jar
+ *
+ * 注: 启动tomcat时,除了在tomcat/conf/server.xml中配置项目基本信息(不论你使用不使用代理,这个都是要配置的)外,记得使用tomcat/bin/下的startup.sh或startup
+ * .bat进行启动tomcat。
+ * </p>
+ *
+ *
+ * @author {@link JustryDeng}
+ * @since 2021/5/6 23:25:24
+ */
+public class ReversesTest {
+    
+    /**
+     * 入口
+     * java -javaagent:/tmp/boot-jar-encrypted.jar -jar /tmp/boot-jar-encrypted.jar
+     * java -javaagent:/tmp/boot-jar-encrypted.jar=debug=true,password=qwer123~ -jar /tmp/boot-jar-encrypted.jar
+     */
+    public static void main(String[] args) {
+        // 先生成加密后的jar
+        String encryptedJar = ForwardTest.encryptTest();
+        encryptedJar = new File(encryptedJar).getAbsolutePath();
+
+        /// - 解密正常 - 测试
+        // 使用javaagent运行加密后的jar,测试
+        String javaagentArgs = "=debug=true,password=qwer123~";
+        BashUtil.runCmdAndPrint(String.format("java -javaagent:%s%s -jar %s", encryptedJar, javaagentArgs, encryptedJar));
+        // 提示:正常启动后,jar包正常运行,那么这个cmd拉起的进程一直不会结束,需要主动停止下这个main方法来试图杀死拉起的进程(有时,由于权限
+        //      等原因,即便停止main方法后,其拉起的进程也没有被kill掉,这时就需要用其它的方式来杀进程了)
+
+    
+//        /// - 不使用javaagent,直接启动 - 测试
+//        //
+//        BashUtil.runCmdAndPrint(String.format("java -jar %s", encryptedJar));
+        
+        
+//        /// - 加密时,使用了密码, 解密时不输入密码 - 测试
+//        // 使用javaagent运行加密后的jar,测试
+//        String javaagentArgs = "";
+//        BashUtil.runCmdAndPrint(String.format("java -javaagent:%s%s -jar %s", encryptedJar, javaagentArgs, encryptedJar));
+        
+//        /// - 密码不正确 - 测试
+//        // 使用javaagent运行加密后的jar,测试
+//        String javaagentArgs = "=password=xxx";
+//        //String javaagentArgs = "=debug=true,password=xxx";
+//        BashUtil.runCmdAndPrint(String.format("java -javaagent:%s%s -jar %s", encryptedJar, javaagentArgs, encryptedJar));
+    
+    
+        try {
+            TimeUnit.SECONDS.sleep(3);
+        } catch (InterruptedException e) {
+            // ignore
+        }
+        BashUtil.killProcessByPorts("8080");
+    }
+}

BIN
class-winter-core/src/test/resources/boot-jar.jar


BIN
class-winter-core/src/test/resources/my-project-with-encrypted-lib-have-pwd.jar


BIN
class-winter-core/src/test/resources/my-project-with-encrypted-lib-no-pwd.jar


BIN
class-winter-core/src/test/resources/my-project-with-normal-lib.jar


BIN
class-winter-core/src/test/resources/normal-jar.jar


BIN
class-winter-core/src/test/resources/war-project.war


+ 59 - 0
class-winter-maven-plugin/pom.xml

@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>class-winter</artifactId>
+        <groupId>com.idea-aedi</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>class-winter-maven-plugin</artifactId>
+    <packaging>maven-plugin</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.niantou</groupId>
+            <artifactId>class-winter-core</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-plugin-api</artifactId>
+            <version>3.8.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.maven.plugin-tools</groupId>
+            <artifactId>maven-plugin-annotations</artifactId>
+            <version>3.6.1</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.maven</groupId>
+            <artifactId>maven-project</artifactId>
+            <version>2.2.1</version>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-plugin-plugin</artifactId>
+                <version>3.5</version>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 102 - 0
class-winter-maven-plugin/src/main/java/net/jd/classwinter/plugin/ClassWinterPlugin.java

@@ -0,0 +1,102 @@
+package net.jd.classwinter.plugin;
+
+import net.jd.classwinter.Forward;
+import net.jd.classwinter.executor.EncryptExecutor;
+import net.jd.classwinter.util.ExceptionUtil;
+import net.jd.classwinter.util.Logger;
+import org.apache.maven.model.Build;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+
+import java.io.File;
+
+/**
+ * 加密jar/war文件的maven插件
+ *
+ * @author JustryDeng
+ */
+@Mojo(name = "class-winter", defaultPhase = LifecyclePhase.PACKAGE)
+public class ClassWinterPlugin extends AbstractMojo {
+
+    @Parameter(defaultValue = "${project}", readonly = true, required = true)
+    private MavenProject project;
+    
+    @SuppressWarnings("FieldCanBeLocal")
+    private String originJarOrWar;
+    
+    @Parameter(required = true)
+    private String includePrefix;
+    
+    @Parameter
+    private String finalName;
+    
+    @Parameter
+    private String password;
+    
+    @Parameter
+    private String excludePrefix;
+    
+    @Parameter
+    private String includeLibs;
+    
+    @Parameter
+    private String alreadyProtectedLibs;
+    
+    @Parameter
+    private String supportFile;
+    
+    @Parameter
+    private String tips;
+    
+    @Parameter(defaultValue = "false")
+    private Boolean debug;
+    
+    @Override
+    public void execute() throws MojoExecutionException, MojoFailureException {
+        Logger.info(ClassWinterPlugin.class,"---------------------------< class-winter-plugin start >---------------------------");
+        
+        Logger.ENABLE_DEBUG.set(debug != null && debug);
+        Logger.debug(Forward.class, "You config arg originJarOrWar -> " + originJarOrWar);
+        Logger.debug(Forward.class, "You config arg includePrefix -> " + includePrefix);
+        Logger.debug(Forward.class, "You config arg excludePrefix -> " + excludePrefix);
+        Logger.debug(Forward.class, "You config arg finalName -> " + finalName);
+        Logger.debug(Forward.class, "You config arg includeLibs -> " + includeLibs);
+        Logger.debug(Forward.class, "You config arg alreadyProtectedLibs -> " + alreadyProtectedLibs);
+        Logger.debug(Forward.class, "You config arg tips -> " + tips);
+        Logger.debug(Forward.class, "You config arg debug -> " + debug);
+        
+        Build build = project.getBuild();
+        // 要加密的jar/war文件的绝对路径
+        originJarOrWar = build.getDirectory() + File.separator + build.getFinalName() + "." + project.getPackaging();
+        // 创建加密对象类
+        EncryptExecutor encryptExecutor = EncryptExecutor.builder()
+                .originJarOrWar(originJarOrWar)
+                .finalName(finalName)
+                .password(password)
+                .includePrefix(includePrefix)
+                .excludePrefix(excludePrefix)
+                .includeLibs(includeLibs)
+                .alreadyProtectedLibs(alreadyProtectedLibs)
+                .supportFile(supportFile)
+                .debug(debug)
+                .tips(tips)
+                .build();
+    
+        Logger.debug(ClassWinterPlugin.class, "The encrypted executor generated based on your configuration is -> " + encryptExecutor);
+        String encryptedJarOrWar = null;
+        try {
+            encryptedJarOrWar = encryptExecutor.process();
+            Logger.info(ClassWinterPlugin.class, "The absolute path of the obfuscated jar is [" + encryptedJarOrWar + "]");
+        } catch (Exception e) {
+            Logger.error(ClassWinterPlugin.class, ExceptionUtil.getStackTraceMessage(e));
+            throw e;
+        }
+        Logger.info(ClassWinterPlugin.class,"---------------------------< class-winter-plugin  end  >---------------------------");
+    }
+
+}

+ 23 - 0
pom.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <packaging>pom</packaging>
+    <modules>
+        <module>class-winter-core</module>
+        <module>class-winter-maven-plugin</module>
+    </modules>
+    <groupId>com.idea-aedi</groupId>
+    <artifactId>class-winter</artifactId>
+    <version>1.0.0</version>
+    <name>class-winter</name>
+    <description>class混淆、加密</description>
+    <properties>
+        <java.version>1.8</java.version>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+        <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+</project>