使用反射获取指定包下所有类及其方法时的诡异问题及解决方案
引言
最近在项目中遇到了一个棘手的问题:通过反射获取指定包下面的所有类和类下面的所有方法,在本地使用IDEA运行项目时一切正常,但将项目打成JAR包后部署到服务器上却无法获取到。经过一番调查,发现问题出在类加载方式的不同上。
原因分析
问题的根源在于类加载器的不同。IDEA运行时使用的是IDEA自带的类加载器,而JAR包在服务器上运行时使用的是Java标准的类加载器。这种差异会导致反射获取类和方法时的行为不同。
解决方案
为了在JAR包部署环境中也能正确获取指定包下的所有类及其方法:
/**
* 获取包下所有类
*
* @param packageName 包路径
* @return 类集合
*/
public static Set<Class<?>> getClasses(String packageName) throws IOException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
assert classLoader != null;
String path = packageName.replace('.', '/');
Enumeration<URL> resources = classLoader.getResources(path);
Set<Class<?>> classes = new LinkedHashSet<>();
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
if ("file".equals(resource.getProtocol())) {
processDirectory(classes, resource, packageName);
} else if ("jar".equals(resource.getProtocol())) {
processJarFile(classes, resource, packageName);
}
}
return classes;
}
/**
* 递归遍历目录下的所有文件
*
* @param classes 类集合
* @param directory 目录
* @param packageName 包名
*/
private static void processDirectory(Set<Class<?>> classes, URL directory, String packageName) throws UnsupportedEncodingException, MalformedURLException {
String path = URLDecoder.decode(directory.getFile(), "UTF-8");
File[] files = new File(path).listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
processDirectory(classes, file.toURI().toURL(), packageName + "." + file.getName());
} else if (file.getName().endsWith(".class")) {
String className = packageName + '.' + file.getName().substring(0, file.getName().length() - 6);
try {
classes.add(Class.forName(className, false, getClassLoader()));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
}
}
/**
* 解析jar包中的类集合
*
* @param classes 类集合
* @param jarFileUrl jar包
* @param packageName 包名
*/
private static void processJarFile(Set<Class<?>> classes, URL jarFileUrl, String packageName) throws IOException {
JarFile jarFile = null;
try {
JarURLConnection jarURLConnection = (JarURLConnection) jarFileUrl.openConnection();
if (jarURLConnection != null) {
jarFile = jarURLConnection.getJarFile();
if (jarFile != null) {
Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
String jarEntryName = jarEntry.getName();
if (jarEntryName.startsWith(packageName.replace('.', '/') + '/') && jarEntryName.endsWith(".class")) {
String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replaceAll("/", ".");
try {
classes.add(Class.forName(className));
} catch (ClassNotFoundException e) {
log.error(e.getMessage());
}
}
}
}
}
} catch (IOException e) {
System.err.println("Error processing the Jar file: " + e.getMessage());
} finally {
// 在 finally 块中确保 JarFile 被正确关闭
if (jarFile != null) {
try {
jarFile.close();
} catch (IOException e) {
System.err.println("Error closing the Jar file: " + e.getMessage());
}
}
}
}
/**
* 获取类的方法列表
*
* @param clazz 类
* @return 方法列表
*/
public static List<Map<String, Object>> getMethodsByClass(Class<?> clazz) {
List<Map<String, Object>> methodList = new ArrayList<>();
Arrays.stream(clazz.getDeclaredMethods()).forEach(method -> {
Map<String, Object> methodNode = new HashMap<>();
methodNode.put("methodName", method.getName());
methodNode.put("methodPath", method.toString());
methodList.add(methodNode);
});
return methodList;
}
代码说明
-
获取指定包下的所有类
使用Thread.currentThread().getContextClassLoader().getResources(packagePath)
获取指定包路径下的所有资源。 -
处理JAR文件
如果资源是JAR文件,则通过JarURLConnection
获取JAR文件中的所有条目,并筛选出类文件。 -
处理目录
如果资源是目录,则递归查找目录中的所有类文件。 -
使用反射获取方法
对于每个找到的类,使用clazz.getDeclaredMethods()
获取其所有方法,并输出方法名。
结论
通过上述方法,可以在本地IDEA开发环境和服务器部署环境中,正确地通过反射获取指定包下的所有类及其方法。