com.ideaaedi.commonds.clazz.LoadJarClassHelper Maven / Gradle / Ivy
package com.ideaaedi.commonds.clazz;
import lombok.Getter;
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.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 1.0.0
*/
@Slf4j
public final class LoadJarClassHelper {
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 final Method ADD_URL_METHOD;
/** 类加载器 */
@Getter
private final URLClassLoader classLoader;
{
try {
ADD_URL_METHOD = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
ADD_URL_METHOD.setAccessible(true);
URLClassLoader currClassLoader = (URLClassLoader)LoadJarClassHelper.class.getClassLoader();
classLoader = new URLClassLoader(currClassLoader.getURLs(), currClassLoader.getParent());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
/**
* @see LoadJarClassHelper#loadJar(File, boolean, Set, Set)
*/
public Set> loadJar(File jarOrDirFile, Set includePrefixSet, Set excludePrefixSet) {
return this.loadJar(jarOrDirFile, false, includePrefixSet, excludePrefixSet);
}
/**
* 加载指定的jar文件中的所有class(或: 加载指定目录(含其子孙目录)下的所有jar文件中的所有class)
*
* 注:普通的jar包与spring-boot jar包都支持。
*
* @param jarOrDirFile
* 要加载的jar文件(或jar文件所在的目录)
*
* 注:如果jarOrDir是目录,那么该目录包括其子孙目录下的所有jar都会被加载。
* @param instanceLibClass
* 是否实例化lib中的Class对象
* @param includePrefixSet
* 当通过前缀控制是否实例化Class对象
*
* 注: 若includePrefixSet为null或者为空集合,那么默认实例化所有的class
* @param excludePrefixSet
* 通过前缀控制是否排除实例化Class对象
*
* 注: excludePrefixSet优先级高于includePrefixSet。
*
* @return 已加载了的class实例集合
*/
public Set> loadJar(File jarOrDirFile,
boolean instanceLibClass,
Set includePrefixSet,
Set excludePrefixSet) {
Set> classInstanceSet = new HashSet<>();
if (jarOrDirFile == null || !jarOrDirFile.exists()) {
log.warn("jarOrDirFile is null Or jarOrDirFile is non-exist.");
return classInstanceSet;
}
List jarFileList = IOUtil.listFileOnly(jarOrDirFile, JAR_SUFFIX);
List bootJarFileList = new ArrayList<>(16);
List normalJarFileList = new ArrayList<>(16);
jarFileList.forEach(jar -> {
if (isBootJar(jar)) {
bootJarFileList.add(jar);
} else {
normalJarFileList.add(jar);
}
});
classInstanceSet.addAll(loadBootJar(bootJarFileList, instanceLibClass, includePrefixSet, excludePrefixSet));
classInstanceSet.addAll(loadNormalJar(normalJarFileList, true, includePrefixSet, excludePrefixSet));
return classInstanceSet;
}
/**
* 加载指定路径下所有class文件
*
* @param classLongNameRootDirSet
* classLongNameRootDir集合,
* 其中classLongNameRootDir为顶级包的父目录
* 举例说明:
* 假设,现有结构/dir1/dir2/com/aaa/bbb/ccc/Qwer.class, 其中Qwer的全类名为 com.aaa.bbb.ccc.Qwer
* 那么,在这里面,顶级包就是com, classLongNameRootDir就应该是/dir1/dir2/
* @param includePrefixSet
* 通过前缀控制是否实例化Class对象
*
* 注: 若includePrefixSet为null或者为空集合,那么默认实例化所有的class
* @param excludePrefixSet
* 通过前缀控制是否排除实例化Class对象
*
* 注: excludePrefixSet优先级高于includePrefixSet。
*
* @return 已加载了的class实例集合
*/
public Set> loadClass(Set classLongNameRootDirSet,
Set includePrefixSet,
Set 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(classLoader, classLongNameRootDir.toURI().toURL());
} catch (IllegalAccessException | InvocationTargetException | MalformedURLException e) {
throw new RuntimeException(e);
}
});
// (去重)采集所有类全类名
Set classLongNameSet = new HashSet<>();
classLongNameRootDirSet.forEach(classLongNameRootDir -> {
int classLongNameStartIndex = classLongNameRootDir.getAbsolutePath().length() + 1;
List 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(this::createClassInstance)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
/**
* 加载(spring-boot打包出来的)jar文件(中的所有class)
*
* 注: jar文件中,BOOT-INF/lib目录(含其子孙目录)下的所有jar文件,会被当做normal-jar,也一并进行加载。
* 注: jar文件中其余位置的jar文件(如果有的话)不会被加载.
*
* @param jarFileList
* 要加载的jar文件集合
* @param instanceLibClass
* 是否实例化lib中的Class对象
* @param includePrefixSet
* 通过前缀控制是否实例化Class对象
*
* 注: 若includePrefixSet为null或者为空集合,那么默认实例化所有的class
* @param excludePrefixSet
* 通过前缀控制是否排除实例化Class对象
*
* 注: excludePrefixSet优先级高于includePrefixSet。
*
* @return 已加载了的class文件全类名集合
*/
public Set> loadBootJar(List jarFileList, boolean instanceLibClass,
Set includePrefixSet, Set excludePrefixSet) {
Set> classInstanceSet = new HashSet<>();
if (jarFileList == null || jarFileList.size() == 0) {
return classInstanceSet;
}
verifyJarFile(jarFileList);
Set bootClassRootDirSet = new HashSet<>();
Set bootLibSet = new HashSet<>();
Set tmpDirSet = new HashSet<>();
for (File file : jarFileList) {
String absolutePath = file.getAbsolutePath();
String tmpDir = absolutePath.substring(0, absolutePath.length() - JAR_SUFFIX_LENGTH) + TMP_DIR_SUFFIX + System.currentTimeMillis() + "__";
// 记录临时目录
tmpDirSet.add(new File(tmpDir));
JarUtil.unJarWar(absolutePath, tmpDir);
// 记录bootClassRootDir
bootClassRootDirSet.add(new File(tmpDir, "BOOT-INF/classes"));
// 记录bootLib
List libs = IOUtil.listFileOnly(new File(tmpDir, "BOOT-INF/lib"), JAR_SUFFIX);
bootLibSet.addAll(libs);
}
// 加载BOOT-INF/lib/下的.jar
classInstanceSet.addAll(loadNormalJar(new ArrayList<>(bootLibSet), instanceLibClass, includePrefixSet, excludePrefixSet));
// 加载BOOT-INF/classes/下的.class
bootClassRootDirSet.forEach(bootClassRootDir -> {
Set 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 tmpSet = new HashSet<>();
tmpSet.add(bootClassRootDir.getParentFile().getParentFile());
classInstanceSet.addAll(
loadClass(tmpSet, includePrefixSet, excludePrefixSet)
);
}
);
// 删除临时目录
tmpDirSet.forEach(IOUtil::delete);
return classInstanceSet;
}
/**
* 加载(普通)jar文件(中的所有class)
*
* 注: jar文件中若包含其他的的jar文件,其他的jar文件里面的class是不会被加载的。
*
* @param jarFileList
* 要加载的jar文件集合
* @param instanceClass
* 是否实例化Class对象
* @param includePrefixSet
* 当instanceClass为true时, 通过前缀控制是否实例化Class对象
*
* 注: 若includePrefixSet为null或者为空集合,那么默认实例化所有的class
* @param excludePrefixSet
* 当instanceClass为true时, 通过前缀控制是否排除实例化Class对象
*
* 注: excludePrefixSet优先级高于includePrefixSet。
*
* @return 已加载了的class集合
*/
public Set> loadNormalJar(List jarFileList, boolean instanceClass,
Set includePrefixSet, Set excludePrefixSet) {
Set> 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(classLoader, 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;
}
/**
* 根据全类名创建class实例
*
* @param classLongName
* 全类名
* @return class实例(注: 当创建异常时,返回null)
*/
public Class> createClassInstance(String classLongName) {
Class> instance = null;
try {
instance = classLoader.loadClass(classLongName);
} catch (Throwable e) {
log.warn("create instance [{}] fail. throwable -> {}, message -> {}",
classLongName, e.getClass().getSimpleName(), e.getMessage());
}
return instance;
}
/**
* 校验jar文件合法性(存在 && 是.jar后缀的文件)
*
* @param jarFileList
* 要校验的jar文件
*/
private void verifyJarFile(List 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().endsWith(JAR_SUFFIX)) {
throw new IllegalArgumentException("file [" + file.getAbsolutePath() + "] is not a jar file.");
}
});
}
/**
* 判断jar文件是否是boot-jar文件
*
* @param jar
* 带判断的jar文件
* @return true-是boot-jar, false-普通jar
*/
private boolean isBootJar(File jar) {
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.startsWith("BOOT-INF/classes")) {
return true;
}
}
return false;
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
IOUtil.close(zipFile);
}
}
/**
* IO工具类
*
* @author JustryDeng
* @since 1.0.0
*/
@SuppressWarnings("AlibabaClassNamingShouldBeCamel")
private static final class IOUtil {
/**
* 只罗列文件(即:只返回文件)
*
* 注:dirOrFile对象本身也会被作为罗列对象。
*
*
* @param dirOrFile
* 要罗列的文件夹(或者文件)
* @param suffix
* 要筛选的文件的后缀(若suffix为null, 则不作筛选)
*
* @return 罗列结果
*/
public static List 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 || suffix.length == 0) {
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 listFile(File dirOrFile, int mode) {
List fileContainer = new ArrayList<>(16);
listFile(dirOrFile, fileContainer, mode);
return fileContainer;
}
/**
* 罗列所有文件文件夹
*
* 注:dirOrFile对象本身也会被作为罗列对象。
*
*
* @param dirOrFile
* 要罗列的文件夹(或者文件)
* @param fileContainer
* 罗列结果
* @param mode
* 罗列模式(0-罗列文件和文件夹; 1-只罗列文件; 2-只罗列文件夹)
*/
public static void listFile(File dirOrFile, List fileContainer, int mode) {
if (!dirOrFile.exists()) {
return;
}
int onlyDirMode = 2;
if (mode != 0 && mode != 1 && mode != onlyDirMode) {
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 == onlyDirMode) {
fileContainer.add(dirOrFile);
}
} else {
if (mode == 0 || mode == 1) {
fileContainer.add(dirOrFile);
}
}
}
/**
* 将srcFileBytes写出为destFile文件
*
* 注: 若源文件存在,则会覆盖原有的内容。
*
*
* @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 RuntimeException("destFile [" + destFile.getAbsolutePath() + "] must be file rather than dir.");
}
if (createIfNecessary && !destFile.exists()) {
File parentFile = destFile.getParentFile();
if (!parentFile.exists() || !parentFile.isDirectory()) {
/*
* 进入此if,即代表parentFile存在,且为file, 而我们又需要创建一个同名的文件夹。
* 如果系统不支持创建与文件同名(大小写不敏感)的文件夹的话,那么创建结果为false
*/
boolean mkdirs = parentFile.mkdirs();
if (!mkdirs) {
// step0. 将与与文件夹名冲突的文件重命名为:原文件名_时间戳
Arrays.stream(Objects.requireNonNull(parentFile.getParentFile().listFiles()))
.filter(file -> file.getName().equalsIgnoreCase(parentFile.getName())).findFirst()
.ifPresent(conflictFile -> {
String renameFilePath =
conflictFile.getAbsolutePath() + "_" + System.currentTimeMillis();
boolean renameResult = conflictFile.renameTo(new File(renameFilePath));
log.warn("rename file [" + conflictFile.getAbsolutePath() + "] to ["
+ renameFilePath + "] " + (renameResult ? "success" : "fail") + ".");
});
// step1. 再次创建文件夹
mkdirs = parentFile.mkdirs();
if (!mkdirs) {
log.warn("create dir [" + parentFile.getAbsolutePath() + "] fail.");
}
}
}
//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 RuntimeException(" toFile [" + destFile.getAbsolutePath() + "] occur exception.", e);
} finally {
close(os);
}
}
/**
* 将inputStream转换为byte[]
*
* 注:此方法会释放inputStream
*
*
* @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);
}
}
/**
* 删除文件/文件夹
*
* @param dirOrFile
* 要删的除文件/文件夹
*/
public static void delete(File dirOrFile) {
if (!dirOrFile.exists()) {
return;
}
if (dirOrFile.isFile()) {
boolean success = dirOrFile.delete();
if (!success) {
log.debug("delete file [" + dirOrFile.getAbsolutePath() + "] fail.");
}
} else {
File[] files = dirOrFile.listFiles();
if (files != null) {
for (File f : files) {
delete(f);
}
}
}
//noinspection ResultOfMethodCallIgnored
dirOrFile.delete();
}
/**
* 关闭流
*
* @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
}
}
}
}
/**
* jar/war操作工具类
*
* @author JustryDeng
* @since 1.0.0
*/
private static final class JarUtil {
/**
* 解压jar(or war)至指定的目录
*
* @see JarUtil#unJarWar(String, String, boolean, Collection)
*/
public static > List 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 entryNamePrefixes
* 只有当entryName为指定的前缀时,才对该entry进行解压(若为null或者长度为0, 则解压所有文件) 如: ["BOOT-INF/classes/", "BOOT-INF/classes/com/example/ssm/author/JustryDeng.class"]
*
* 注:当entry对应jar或者war中的目录时,那么其值形如 BOOT-INF/classes/
*
* 注:当entry对应jar或者war中的文件时,那么其值形如 BOOT-INF/classes/com/example/ssm/author/JustryDeng.class
* @return 解压出来的文件(包含目录)的完整路径
*/
public static > List unJarWar(String jarWarPath, String targetDir,
boolean delOldTargetDirIfAlreadyExist,
T entryNamePrefixes) {
List 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();
String entryName = entry.getName();
// 若entryNamePrefixes不为空,则不解压前缀不匹配的文件或文件夹
if (entryNamePrefixes != null && entryNamePrefixes.size() > 0
&& entryNamePrefixes.stream().noneMatch(entryName::startsWith)) {
continue;
}
if (entry.isDirectory()) {
targetFile = new File(target, entryName);
guarantyDirExist(targetFile);
} else {
// 有时遍历时,文件先于文件夹出来,所以也得保证目录存在
int lastSeparatorIndex = entryName.lastIndexOf("/");
if (lastSeparatorIndex > 0) {
guarantyDirExist(new File(target, entryName.substring(0, lastSeparatorIndex)));
}
// 解压文件
targetFile = new File(target, entryName);
byte[] bytes = IOUtil.toBytes(zipFile.getInputStream(entry));
IOUtil.toFile(bytes, targetFile, true);
list.add(targetFile.getAbsolutePath());
}
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
IOUtil.close(zipFile);
}
return list;
}
/**
* 保证目录存在
*
* @param dir
* 目录
*/
public static void guarantyDirExist(File dir) {
if (!dir.exists()) {
//noinspection ResultOfMethodCallIgnored
dir.mkdirs();
}
}
}
}