Java 中缺乏内建的类路径扫描机制。但是这个机制对于做框架的人来说很常用。下面的工具类从类路径下面的文件或者 JAR 路径中扫描包,可以实现我们需要的功能。实现是基于 Spring 3.1.11.RELEASE 版的 PathMatchingResourcePatternResolver 修改的。去掉了原版中 ANT * 号匹配的功能。依赖 Google Guava, Apache Commons Collections 4, Apache Commons Lang。
这个实现的大概思路是
1. 先根据包名构建一个路径("." 换成 "/")
2. 用类加载器去加载这个路径对应的资源,得到一个 URL
3. 如果 URL 对应的是 JAR,扫描 JAR(非递归扫描)
4. 如果 URL 是文件系统,扫描文件系统(递归扫描)
3. 4., 一个递归一个非递归,主要原因是 JAR 对应的 API 返回的文件列表是展开的,所以我们不需要递归展开了。
比如有意思的是 2. 这一步。如果路径是空字符串 "",那么
1. 在 maven 下或者 IDE 下直接运行。得到的 URL 在文件系统下是 classes、test-classes 这些
2. 用 java -jar xxx.jar 运行。得到的 URL 是空的。但如果不传空字符串,传 com.xxx.app 就可以加载到资源
所以这个工具类的使用需要注意一点。[b]如果你想让这个工具类在打成 JAR 包之后还能正常用,那么参数 basePackages 这里至少添上一个 com.xxx.app 之类的东西。[/b]
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.commons.collections4.EnumerationUtils;
import org.apache.commons.lang.StringUtils;
import java.io.File;
import java.io.IOException;
import java.net.*;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* 反射工具。实现是基于 Spring 3.1.11.RELEASE 版的 PathMatchingResourcePatternResolver 修改的。
*
* @author fanshen
* @since 2015/7/7
*/
public class ReflectionUtils {
private ReflectionUtils() {
}
/**
* URL protocol for an entry from a jar file: "jar"
*/
private static final String URL_PROTOCOL_JAR = "jar";
/**
* URL protocol for an entry from a zip file: "zip"
*/
private static final String URL_PROTOCOL_ZIP = "zip";
/**
* URL protocol for an entry from a WebSphere jar file: "wsjar"
*/
private static final String URL_PROTOCOL_WSJAR = "wsjar";
/**
* URL protocol for an entry from a JBoss jar file: "vfszip"
*/
private static final String URL_PROTOCOL_VFSZIP = "vfszip";
/**
* URL protocol for an entry from an OC4J jar file: "code-source"
*/
private static final String URL_PROTOCOL_CODE_SOURCE = "code-source";
/**
* Separator between JAR URL and file path within the JAR
*/
private static final String JAR_URL_SEPARATOR = "!/";
/**
* URL prefix for loading from the file system: "file:"
*/
private static final String FILE_URL_PREFIX = "file:";
/**
* 文件夹隔离符。
*/
private static final String FOLDER_SEPARATOR = "/";
/**
* 递归地在类路径中以指定的类加载器获取 dirPath 指定的目录下面所有的资源。cl 可为空,为空时取系统类加载器。
* 返回值一定不为 null。
*/
public static ClassPathResource[] getClassPathResources(String dirPath, ClassLoader cl) throws IOException {
if (cl == null) {
cl = Thread.currentThread().getContextClassLoader();
}
URL[] roots = getRoots(dirPath, cl);
Set result = new LinkedHashSet(16);
for (URL root : roots) {
if (isJarResource(root)) {
result.addAll(doFindPathMatchingJarResources(root));
} else {
result.addAll(doFindPathMatchingFileResources(root, dirPath));
}
}
return result.toArray(new ClassPathResource[result.size()]);
}
/**
* 获取指定 basePackages 下面所有能用 classLoaders 加载的类。
*
* @param basePackages 这些包下面的类会被扫描
* @param classLoaders 用来加载 basePackages 下面类的类加载器
* @return 能够扫描到的类
*/
public static Class>[] getAllClassPathClasses(String[] basePackages, ClassLoader... classLoaders)
throws IOException {
if (classLoaders.length <= 0) {
classLoaders = new ClassLoader[] {Thread.currentThread().getContextClassLoader()};
}
List> classes = Lists.newArrayListWithCapacity(100);
for (ClassLoader classLoader : classLoaders) {
for (String basePackage : basePackages) {
classes.addAll(Lists.newArrayList(ReflectionUtils.getClasses(basePackage, classLoader)));
}
}
return classes.toArray(new Class[classes.size()]);
}
/**
* 使用 cl 指定的类加载器递归加载 packageName 指定的包名下面的所有的类。不会返回 null。
* cl 为空时使用系统类加载器。返回值一定不为 null。返回值中不包含类路径中的内部类。
*/
public static Class>[] getClasses(String packageName, ClassLoader cl) throws IOException {
if (cl == null) {
cl = Thread.currentThread().getContextClassLoader();
}
ClassPathResource[] resources = getClassPathResources(StringUtils.replace(packageName, ".", "/"), cl);
List> result = Lists.newArrayList();
for (ClassPathResource resource : resources) {
String urlPath = resource.getUrl().getPath();
if (!urlPath.endsWith(".class") || urlPath.contains("$")) {
continue;
}
Class> cls = resolveClass(cl, resource);
if (cls != null) {
result.add(cls);
}
}
return result.toArray(new Class[result.size()]);
}
private static Class> resolveClass(ClassLoader cl, ClassPathResource resource) {
String className = resolveClassName(resource);
try {
return cl.loadClass(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
private static String resolveClassName(ClassPathResource resource) {
String path = resource.getClassPathPath();
String className = path.substring(0, path.length() - ".class".length());
className = StringUtils.replace(className, "/", ".");
return className;
}
private static URL[] getRoots(String dirPath, ClassLoader cl) throws IOException {
Enumeration resources = cl.getResources(dirPath);
List resourceUrls = EnumerationUtils.toList(resources);
return resourceUrls.toArray(new URL[resourceUrls.size()]);
}
private static Collection doFindPathMatchingFileResources(URL rootUrl, String dirPath)
throws IOException {
String filePath = rootUrl.getFile();
File file = new File(filePath);
File rootDir = file.getAbsoluteFile();
return doFindMatchingFileSystemResources(rootDir, dirPath);
}
private static Collection doFindMatchingFileSystemResources(File rootDir, String dirPath)
throws IOException {
Set allFiles = Sets.newLinkedHashSet();
retrieveAllFiles(rootDir, allFiles);
String classPathRoot = parseClassPathRoot(rootDir, dirPath);
Set result = new LinkedHashSet(allFiles.size());
for (File file : allFiles) {
String absolutePath = file.getAbsolutePath();
URL url = new URL("file:///" + absolutePath);
String classPathPath = absolutePath.substring(classPathRoot.length());
classPathPath = StringUtils.replace(classPathPath, "\\", "/");
result.add(new ClassPathResource(url, classPathPath));
}
return result;
}
private static String parseClassPathRoot(File rootDir, String dirPath) {
String absolutePath = rootDir.getAbsolutePath();
absolutePath = StringUtils.replace(absolutePath, "\\", "/");
int lastIndex = absolutePath.lastIndexOf(dirPath);
String result = absolutePath.substring(0, lastIndex);
if (!result.endsWith("/")) {
result = result + "/";
}
return result;
}
private static void retrieveAllFiles(File dir, Set allFiles) {
File[] subFiles = dir.listFiles();
assert subFiles != null;
allFiles.addAll(Arrays.asList(subFiles));
for (File subFile : subFiles) {
if (subFile.isDirectory()) {
retrieveAllFiles(subFile, allFiles);
}
}
}
private static Collection doFindPathMatchingJarResources(URL rootUrl) throws IOException {
URLConnection con = rootUrl.openConnection();
JarFile jarFile;
String rootEntryPath;
boolean newJarFile = false;
if (con instanceof JarURLConnection) {
// Should usually be the case for traditional JAR files.
JarURLConnection jarCon = (JarURLConnection) con;
jarCon.setUseCaches(true);
jarFile = jarCon.getJarFile();
JarEntry jarEntry = jarCon.getJarEntry();
rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
} else {
// No JarURLConnection -> need to resort to URL file parsing.
// We'll assume URLs of the format "jar:path!/entry", with the protocol
// being arbitrary as long as following the entry format.
// We'll also handle paths with and without leading "file:" prefix.
String urlFile = rootUrl.getFile();
int separatorIndex = urlFile.indexOf(JAR_URL_SEPARATOR);
if (separatorIndex != -1) {
String jarFileUrl = urlFile.substring(0, separatorIndex);
rootEntryPath = urlFile.substring(separatorIndex + JAR_URL_SEPARATOR.length());
jarFile = getJarFile(jarFileUrl);
} else {
jarFile = new JarFile(urlFile);
rootEntryPath = "";
}
newJarFile = true;
}
try {
if (!"".equals(rootEntryPath) && !rootEntryPath.endsWith("/")) {
// Root entry path must end with slash to allow for proper matching.
// The Sun JRE does not return a slash here, but BEA JRockit does.
rootEntryPath = rootEntryPath + "/";
}
Set result = new LinkedHashSet(8);
for (Enumeration entries = jarFile.entries(); entries.hasMoreElements();) {
JarEntry entry = entries.nextElement();
String entryPath = entry.getName();
if (entryPath.startsWith(rootEntryPath)) {
String relativePath = entryPath.substring(rootEntryPath.length());
String rootPath = rootUrl.getPath();
rootPath = rootPath.endsWith("/") ? rootPath : rootPath + "/";
String newPath = applyRelativePath(rootPath, relativePath);
String classPathPath = applyRelativePath(rootEntryPath, relativePath);
result.add(new ClassPathResource(new URL(newPath), classPathPath));
}
}
return result;
} finally {
// Close jar file, but only if freshly obtained -
// not from JarURLConnection, which might cache the file reference.
if (newJarFile) {
jarFile.close();
}
}
}
/**
* Apply the given relative path to the given path,
* assuming standard Java folder separation (i.e. "/" separators).
*
* @param path the path to start from (usually a full file path)
* @param relativePath the relative path to apply
* (relative to the full file path above)
* @return the full file path that results from applying the relative path
*/
private static String applyRelativePath(String path, String relativePath) {
int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR);
if (separatorIndex != -1) {
String newPath = path.substring(0, separatorIndex);
if (!relativePath.startsWith(FOLDER_SEPARATOR)) {
newPath += FOLDER_SEPARATOR;
}
return newPath + relativePath;
} else {
return relativePath;
}
}
/**
* Resolve the given jar file URL into a JarFile object.
*/
private static JarFile getJarFile(String jarFileUrl) throws IOException {
if (jarFileUrl.startsWith(FILE_URL_PREFIX)) {
try {
return new JarFile(toURI(jarFileUrl).getSchemeSpecificPart());
} catch (URISyntaxException ex) {
// Fallback for URLs that are not valid URIs (should hardly ever happen).
return new JarFile(jarFileUrl.substring(FILE_URL_PREFIX.length()));
}
} else {
return new JarFile(jarFileUrl);
}
}
/**
* Create a URI instance for the given location String,
* replacing spaces with "%20" URI encoding first.
*
* @param location the location String to convert into a URI instance
* @return the URI instance
* @throws URISyntaxException if the location wasn't a valid URI
*/
private static URI toURI(String location) throws URISyntaxException {
return new URI(StringUtils.replace(location, " ", "%20"));
}
private static boolean isJarResource(URL url) {
String protocol = url.getProtocol();
return (URL_PROTOCOL_JAR.equals(protocol) || URL_PROTOCOL_ZIP.equals(protocol) ||
URL_PROTOCOL_VFSZIP.equals(protocol) || URL_PROTOCOL_WSJAR.equals(protocol) ||
(URL_PROTOCOL_CODE_SOURCE.equals(protocol) && url.getPath().contains(JAR_URL_SEPARATOR)));
}
/**
* 类路径资源。
*/
public static class ClassPathResource {
/**
* 此资源对应的 URL 对象。
*/
private URL url;
/**
* 类路径下的路径。特点是这个路径字符串去掉了类路径的“根”部分。
*/
private String classPathPath;
/**
* ctor.
*/
public ClassPathResource(URL url, String classPathPath) {
this.url = url;
this.classPathPath = classPathPath;
}
public URL getUrl() {
return url;
}
public String getClassPathPath() {
return classPathPath;
}
}
}