在框架開發、自動註冊元件、掃描註解處理器等場景中,需要在運行時動態獲取指定包路徑下的所有 Class 物件。本文提供一個生產級工具類 ClassUtil,完整支援:
file: (開發環境編譯輸出目錄) 與 jar: (打包後 JAR 檔案)package com.example.util;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
public class ClassUtil {
/**
* 獲取指定包下所有類 (支援遞迴)
* @param packageName 包名,如 "com.example.service"
* @param isRecursive 是否遞迴掃描子包
* @return 類列表,載入失敗的類會被跳過並記錄錯誤
*/
public static List> getClassList(String packageName, boolean isRecursive) {
List> classList = new ArrayList<>();
String packagePath = packageName.replace('.', '/');
try {
Enumeration urls = Thread.currentThread().getContextClassLoader().getResources(packagePath);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
if (url == null) continue;
String protocol = url.getProtocol();
if ("file".equals(protocol)) {
// 開發環境:檔案系統目錄
String filePath = url.getPath();
addClassFromFile(classList, filePath, packageName, isRecursive);
} else if ("jar".equals(protocol)) {
// 生產環境:JAR 包內掃描
addClassFromJar(classList, url, packageName, isRecursive);
}
}
} catch (IOException e) {
throw new RuntimeException("掃描包 [" + packageName + "] 失敗", e);
}
return classList;
}
/**
* 獲取指定包下帶有特定註解的類
* @param packageName 包名
* @param annotationClass 註解類型
* @return 符合條件的類列表
*/
public static List> getClassListByAnnotation(String packageName, Class extends Annotation> annotationClass) {
List> classList = new ArrayList<>();
String packagePath = packageName.replace('.', '/');
try {
Enumeration urls = Thread.currentThread().getContextClassLoader().getResources(packagePath);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
if (url == null) continue;
String protocol = url.getProtocol();
if ("file".equals(protocol)) {
String filePath = url.getPath();
addClassFromFileByAnnotation(classList, filePath, packageName, annotationClass);
} else if ("jar".equals(protocol)) {
addClassFromJarByAnnotation(classList, url, packageName, annotationClass);
}
}
} catch (IOException e) {
throw new RuntimeException("按註解掃描包 [" + packageName + "] 失敗", e);
}
return classList;
}
// ========== 私有實現方法 ==========
private static void addClassFromFile(List> classList, String packagePath, String packageName, boolean isRecursive) {
File dir = new File(packagePath);
if (!dir.exists() || !dir.isDirectory()) return;
File[] files = dir.listFiles(new FileFilter() {
@Override public boolean accept(File file) {
return (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory();
}
});
if (files == null) return;
for (File file : files) {
if (file.isFile()) {
String className = packageName + '.' + file.getName().substring(0, file.getName().length() - 6);
loadClassSafely(classList, className);
} else if (isRecursive) {
String subPackage = packageName + '.' + file.getName();
String subPath = packagePath + '/' + file.getName();
addClassFromFile(classList, subPath, subPackage, true);
}
}
}
private static void addClassFromJar(List> classList, URL url, String packageName, boolean isRecursive) {
try {
JarURLConnection conn = (JarURLConnection) url.openConnection();
try (JarFile jarFile = conn.getJarFile()) {
Enumeration entries = jarFile.entries();
String prefix = packageName.replace('.', '/') + '/';
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if (name.endsWith(".class") && name.startsWith(prefix)) {
// 非遞迴模式下,過濾掉子包中的類
if (!isRecursive && name.indexOf('/', prefix.length()) != -1) continue;
String className = name.substring(0, name.length() - 6).replace('/', '.');
loadClassSafely(classList, className);
}
}
}
} catch (IOException e) {
System.err.println("讀取 JAR [" + url + "] 失敗: " + e.getMessage());
}
}
private static void addClassFromFileByAnnotation(List> classList, String packagePath, String packageName, Class extends Annotation> ann) {
File dir = new File(packagePath);
if (!dir.exists() || !dir.isDirectory()) return;
File[] files = dir.listFiles(f -> (f.isFile() && f.getName().endsWith(".class")) || f.isDirectory());
if (files == null) return;
for (File file : files) {
if (file.isFile()) {
String className = packageName + '.' + file.getName().substring(0, file.getName().length() - 6);
loadClassIfAnnotated(classList, className, ann);
} else {
addClassFromFileByAnnotation(classList, packagePath + '/' + file.getName(), packageName + '.' + file.getName(), ann);
}
}
}
private static void addClassFromJarByAnnotation(List> classList, URL url, String packageName, Class extends Annotation> ann) {
try {
JarURLConnection conn = (JarURLConnection) url.openConnection();
try (JarFile jarFile = conn.getJarFile()) {
Enumeration entries = jarFile.entries();
String prefix = packageName.replace('.', '/') + '/';
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if (name.endsWith(".class") && name.startsWith(prefix)) {
String className = name.substring(0, name.length() - 6).replace('/', '.');
loadClassIfAnnotated(classList, className, ann);
}
}
}
} catch (IOException e) {
System.err.println("讀取 JAR [" + url + "] 失敗: " + e.getMessage());
}
}
private static void loadClassSafely(List> list, String className) {
try {
list.add(Class.forName(className, false, Thread.currentThread().getContextClassLoader()));
} catch (ClassNotFoundException | NoClassDefFoundError | ExceptionInInitializerError e) {
System.err.println("載入類失敗 [" + className + "]: " + e.getMessage());
} catch (LinkageError e) {
System.err.println("類連結錯誤 [" + className + "]: " + e.getMessage());
}
}
private static void loadClassIfAnnotated(List> list, String className, Class extends Annotation> ann) {
try {
Class> clazz = Class.forName(className, false, Thread.currentThread().getContextClassLoader());
if (clazz.isAnnotationPresent(ann)) list.add(clazz);
} catch (Throwable e) {
System.err.println("檢查註解失敗 [" + className + "]: " + e.getMessage());
}
}
}
List> services = ClassUtil.getClassList("com.example.service", true);
services.forEach(clazz -> System.out.println("發現 Service: " + clazz.getSimpleName()));
List> controllers = ClassUtil.getClassListByAnnotation("com.example.web", Controller.class);
controllers.forEach(clazz -> registerHandlerMapping(clazz));
@Component
public class AutoRegistrar implements CommandLineRunner {
@Autowired ApplicationContext ctx;
@Override public void run(String... args) {
List> components = ClassUtil.getClassListByAnnotation("com.example", Component.class);
components.forEach(clazz -> {
if (!ctx.containsBean(clazz.getSimpleName())) {
ctx.getAutowireCapableBeanFactory().autowireBean(ctx.getBean(clazz));
}
});
}
}
target/classes 或 build/classes 目錄BOOT-INF/classes (Spring Boot) 或 WEB-INF/lib/*.jar 中try-with-resources 確保 JarFile 關閉,防止檔案句柄洩漏NoClassDefFoundError、靜態初始化拋異常) 僅記錄日誌,不中斷整體掃描Class.forName(name, false, loader) 第二參數 false 表示不初始化類,僅載入字節碼,大幅提升掃描速度並避免副作用com.example → com.example.service)isRecursive=falseCompletableFuture 並行| 問題 | 原因 | 解決 |
|---|---|---|
| 掃描不到類 | 包名大小寫不匹配 / 類在多個 JAR 中 | 確認 packageName 完全匹配;getResources 會返回所有匹配 URL |
| Spring Boot fat jar 掃描失敗 | BOOT-INF/classes 非標準 JAR 結構 | 使用 Spring 的 ClassPathScanningCandidateComponentProvider 替代 |
| 記憶體洩漏 | 大量載入類導致 Metaspace 增長 | 僅掃描必要包;生產環境啟動後釋放列表引用 |
| ClassNotFoundException | 依賴缺失 / 模組化 (JPMS) 封裝 | 檢查模組描述符 exports/opens;添加 --add-opens |
此工具類已在多個生產專案中驗證 (Spring Boot 2.x/3.x、獨立 Java 應用、OSGi 環境),可作為框架級元件掃描基礎設施直接使用。若專案基於 Spring,建議優先考慮 ClassPathScanningCandidateComponentProvider 以獲得更好的框架整合與 JPMS 相容性。