工具类加载外部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 里的类文件?