工具类加载外部jar(普通jar和springboot jar)class

Posted justry_deng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了工具类加载外部jar(普通jar和springboot jar)class相关的知识,希望对你有一定的参考价值。

特性

  • 支持加载普通jar
  • 支持加载spring-boot jar
  • 支持加载class
  • 支持加载多文件多文件夹
  • 支持过滤class实例

工具类

提示 使用方式见工具类最下方的main中的示例。

import lombok.extern.slf4j.Slf4j;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * 动态加载jar或者class
 *
 * @author JustryDeng
 * @since 2021/6/17 0:31:53
 */
@Slf4j
public final class LoadJarClassUtil {
    
    private static final String JAR_SUFFIX = ".jar";
    
    private static final int JAR_SUFFIX_LENGTH = ".jar".length();
    
    private static final String CLASS_SUFFIX = ".class";
    
    private static final String TMP_DIR_SUFFIX = "__temp__";
    
    private static final int CLASS_SUFFIX_LENGTH = CLASS_SUFFIX.length();
    
    /** 添加资源的方法 */
    private static final Method ADD_URL_METHOD;
    
    /** 类加载器 */
    private static final URLClassLoader CLASS_LOADER;
    
    static {
        try {
            ADD_URL_METHOD = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            ADD_URL_METHOD.setAccessible(true);
            CLASS_LOADER = (URLClassLoader) ClassLoader.getSystemClassLoader();
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * 加载指定的jar文件中的所有class(或: 加载指定目录(含其子孙目录)下的所有jar文件中的所有class)
     * <p>
     * 注:普通的jar包与spring-boot jar包都支持。
     *
     * @param jarOrDirFile
     *         要加载的jar文件(或jar文件所在的目录)
     *         <br/>
     *         注:如果jarOrDir是目录,那么该目录包括其子孙目录下的所有jar都会被加载。
     * @param includePrefixSet
     *         当通过前缀控制是否实例化Class对象
     *         <br />
     *         注: 若includePrefixSet为null或者为空集合,那么默认实例化所有的class
     * @param excludePrefixSet
     *         通过前缀控制是否排除实例化Class对象
     *         <br />
     *         注: excludePrefixSet优先级高于includePrefixSet。
     *
     * @return 已加载了的class实例集合
     */
    public static Set<Class<?>> loadJar(File jarOrDirFile,
                                        Set<String> includePrefixSet,
                                        Set<String> excludePrefixSet) {
        Set<Class<?>> classInstanceSet = new HashSet<>();
        if (jarOrDirFile == null || !jarOrDirFile.exists()) {
            log.warn("jarOrDirFile is null Or jarOrDirFile is non-exist.");
            return classInstanceSet;
        }
        
        List<File> jarFileList = IOUtil.listFileOnly(jarOrDirFile, JAR_SUFFIX);
        List<File> bootJarFileList = new ArrayList<>(16);
        List<File> normalJarFileList = new ArrayList<>(16);
        jarFileList.forEach(jar -> {
            if (isBootJar(jar)) {
                bootJarFileList.add(jar);
            } else {
                normalJarFileList.add(jar);
            }
        });
        classInstanceSet.addAll(loadBootJar(bootJarFileList, includePrefixSet, excludePrefixSet));
        classInstanceSet.addAll(loadNormalJar(normalJarFileList, true, includePrefixSet, excludePrefixSet));
        return classInstanceSet;
    }
    
    /**
     * 加载指定路径下所有class文件
     *
     * @param classLongNameRootDirSet
     *          classLongNameRootDir集合,
     *         其中classLongNameRootDir为顶级包的父目录 <br/>
     *         举例说明:
     *         假设,现有结构/dir1/dir2/com/aaa/bbb/ccc/Qwer.class, 其中Qwer的全类名为 com.aaa.bbb.ccc.Qwer
     *         那么,在这里面,顶级包就是com, classLongNameRootDir就应该是/dir1/dir2/
     * @param includePrefixSet
     *         通过前缀控制是否实例化Class对象
     *         <br />
     *         注: 若includePrefixSet为null或者为空集合,那么默认实例化所有的class
     * @param excludePrefixSet
     *         通过前缀控制是否排除实例化Class对象
     *         <br />
     *         注: excludePrefixSet优先级高于includePrefixSet。
     *
     * @return 已加载了的class实例集合
     */
    public static Set<Class<?>> loadClass(Set<File> classLongNameRootDirSet,
                                          Set<String> includePrefixSet,
                                          Set<String> excludePrefixSet) {
        
        if (classLongNameRootDirSet == null || classLongNameRootDirSet.size() == 0) {
            log.warn("classLongNameRootDirSet is empty.");
            return new HashSet<>();
        }
        classLongNameRootDirSet = classLongNameRootDirSet.stream()
                .filter(x -> x.exists() && x.isDirectory())
                .collect(Collectors.toSet());
        if (classLongNameRootDirSet.isEmpty()) {
            log.warn("Valid classLongNameRootDir is empty.");
            return new HashSet<>();
        }
        // 加载
        classLongNameRootDirSet.forEach(classLongNameRootDir -> {
            try {
                ADD_URL_METHOD.invoke(CLASS_LOADER, classLongNameRootDir.toURI().toURL());
            } catch (IllegalAccessException | InvocationTargetException | MalformedURLException e) {
                throw new RuntimeException(e);
            }
        });
        // (去重)采集所有类全类名
        Set<String> classLongNameSet = new HashSet<>();
        classLongNameRootDirSet.forEach(classLongNameRootDir -> {
            int classLongNameStartIndex = classLongNameRootDir.getAbsolutePath().length() + 1;
            List<File> classFileList = IOUtil.listFileOnly(classLongNameRootDir, CLASS_SUFFIX);
            classLongNameSet.addAll(classFileList.stream()
                    .map(classFile -> {
                        String absolutePath = classFile.getAbsolutePath();
                        // 形如: com/aaa/bbb/ccc/Qwer
                        String classLongPath = absolutePath.substring(classLongNameStartIndex,
                                absolutePath.length() - CLASS_SUFFIX_LENGTH);
                        return classLongPath.replace('\\\\', '.').replace("/", ".");
                    }).filter(classLongName -> {
                        if (excludePrefixSet != null && excludePrefixSet.size() > 0) {
                            if (excludePrefixSet.stream().anyMatch(classLongName::startsWith)) {
                                return false;
                            }
                        }
                        if (includePrefixSet != null && includePrefixSet.size() > 0) {
                            return includePrefixSet.stream().anyMatch(classLongName::startsWith);
                        }
                        return true;
                    })
                    .collect(Collectors.toSet())
            );
        });
        // 转换为class实例
        return classLongNameSet.stream()
                .map(LoadJarClassUtil::createClassInstance)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
    }
    
    /**
     * 加载(spring-boot打包出来的)jar文件(中的所有class)
     * <p>
     * 注: jar文件中,BOOT-INF/lib目录(含其子孙目录)下的所有jar文件,会被当做normal-jar,也一并进行加载。
     * 注: jar文件中其余位置的jar文件(如果有的话)不会被加载.
     *
     * @param jarFileList
     *         要加载的jar文件集合
     * @param includePrefixSet
     *         通过前缀控制是否实例化Class对象
     *         <br />
     *         注: 若includePrefixSet为null或者为空集合,那么默认实例化所有的class
     * @param excludePrefixSet
     *         通过前缀控制是否排除实例化Class对象
     *         <br />
     *         注: excludePrefixSet优先级高于includePrefixSet。
     *
     * @return 已加载了的class文件全类名集合
     */
    private static Set<Class<?>> loadBootJar(List<File> jarFileList,
                                             Set<String> includePrefixSet,
                                             Set<String> excludePrefixSet) {
        Set<Class<?>> classInstanceSet = new HashSet<>();
        if (jarFileList == null || jarFileList.size() == 0) {
            return classInstanceSet;
        }
        verifyJarFile(jarFileList);
    
        Set<File> bootClassRootDirSet = new HashSet<>();
        Set<File> bootLibSet = new HashSet<>();
        Set<File> tmpDirSet = new HashSet<>();
        for (File file : jarFileList) {
            String absolutePath = file.getAbsolutePath();
            String tmpDir = absolutePath.substring(0, absolutePath.length() - JAR_SUFFIX_LENGTH) + TMP_DIR_SUFFIX;
            // 记录临时目录
            tmpDirSet.add(new File(tmpDir));
            JarUtil.unJarWar(absolutePath, tmpDir);
            // 记录bootClassRootDir
            bootClassRootDirSet.add(new File(tmpDir, "BOOT-INF/classes"));
            // 记录bootLib
            List<File> libs = IOUtil.listFileOnly(new File(tmpDir, "BOOT-INF/lib"), JAR_SUFFIX);
            bootLibSet.addAll(libs);
        }
    
        // 加载BOOT-INF/lib/下的.jar
        classInstanceSet.addAll(loadNormalJar(new ArrayList<>(bootLibSet), false, includePrefixSet, excludePrefixSet));
        // 加载BOOT-INF/classes/下的.class
        bootClassRootDirSet.forEach(bootClassRootDir -> {
            Set<File> tmpSet = new HashSet<>();
            tmpSet.add(bootClassRootDir);
            classInstanceSet.addAll(loadClass(tmpSet, includePrefixSet, excludePrefixSet));
            // 删除BOOT-INF目录
            IOUtil.delete(bootClassRootDir.getParentFile());
        });
        // 加载jar中与BOOT-INF平级的其他类
        bootClassRootDirSet.forEach(bootClassRootDir -> {
                    Set<File> tmpSet = new HashSet<>();
                    tmpSet.add(bootClassRootDir.getParentFile().getParentFile());
                    classInstanceSet.addAll(
                            loadClass(tmpSet, includePrefixSet, excludePrefixSet)
                    );
                }
        );
        // 删除临时目录
        tmpDirSet.forEach(IOUtil::delete);
        return classInstanceSet;
    }
    
    /**
     * 加载(普通)jar文件(中的所有class)
     * <p>
     * 注: jar文件中若包含其他的的jar文件,其他的jar文件里面的class是不会被加载的。
     *
     * @param jarFileList
     *         要加载的jar文件集合
     * @param instanceClass
     *         是否实例化Class对象
     * @param includePrefixSet
     *         当instanceClass为true时, 通过前缀控制是否实例化Class对象
     *         <br />
     *         注: 若includePrefixSet为null或者为空集合,那么默认实例化所有的class
     * @param excludePrefixSet
     *         当instanceClass为true时, 通过前缀控制是否排除实例化Class对象
     *         <br />
     *         注: excludePrefixSet优先级高于includePrefixSet。
     *
     * @return 已加载了的class集合
     */
    private static Set<Class<?>> loadNormalJar(List<File> jarFileList,
                                               boolean instanceClass,
                                               Set<String> includePrefixSet,
                                               Set<String> excludePrefixSet) {
        Set<Class<?>> classInstanceSet = new HashSet<>();
        if (jarFileList == null || jarFileList.size() == 0) {
            return classInstanceSet;
        }
        verifyJarFile(jarFileList);
        try {
            for (File jar : jarFileList) {
                URL url = jar.toURI().toURL();
                ADD_URL_METHOD.invoke(CLASS_LOADER, url);
                if (!instanceClass) {
                    continue;
                }
                ZipFile zipFile = null;
                try {
                    zipFile = new ZipFile(jar);
                    Enumeration<? extends ZipEntry> entries = zipFile.entries();
                    while (entries.hasMoreElements()) {
                        ZipEntry zipEntry = entries.nextElement();
                        String zipEntryName = zipEntry.getName();
                        if (!zipEntryName.endsWith(CLASS_SUFFIX)) {
                            continue;
                        }
                        String classLongName = zipEntryName
                                .substring(0, zipEntryName.length() - CLASS_SUFFIX_LENGTH)
                                .replace("/", ".");
                        if (excludePrefixSet != null && excludePrefixSet.size() > 0) {
                            if (excludePrefixSet.stream().anyMatch(classLongName::startsWith)) {
                                continue;
                            }
                        }
                        if (includePrefixSet != null && includePrefixSet.size() > 0) {
                            if (includePrefixSet.stream().noneMatch(classLongName::startsWith)) {
                                continue;
                            }
                        }
                        Class<?> instance = createClassInstance(classLongName);
                        if (instance != null) {
                            classInstanceSet.add(instance);
                        }
                    }
                } finally {
                    IOUtil.close(zipFile);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return classInstanceSet;
    }
    
    /**
     * 校验jar文件合法性(存在 && 是.jar后缀的文件)
     *
     * @param jarFileList
     *            要校验的jar文件
     */
    private static void verifyJarFile(List<File> jarFileList) {
        Objects.requireNonNull(jarFileList, "jarFileList cannot be empty.");
        jarFileList.forEach(file -> {
            if (!file.exists()) {
                throw new IllegalArgumentException("file [" + file.getAbsolutePath() + "] non-exist.");
            }
            if (!file.getName(java可以动态加载一个jar包,并且调用里面的类和方法吗?

Java - 如何用 Class.forName 加载外部 Jar 里的类文件?

Spring Boot在部署到Tomcat期间无法加载外部jar

普通邮件发送工具类总结

外部jar中的类在加载Resource时抛出异常

Jar 中的 @PropertySource 用于类路径上的外部文件