package 配下のクラス一覧を取得する方法いろいろ

簡単にできそうで意外と面倒な package 配下のクラス一覧を取得する方法について。


Guava の ClassPath 利用

ClassPath を使えば簡単に取得できる。

ClassLoader loader = Thread.currentThread().getContextClassLoader();
Set<Class<?>> allClasses = ClassPath.from(loader)
        .getTopLevelClasses("my.package").stream()
        .map(info -> info.load())
        .collect(Collectors.toSet());

再帰的に取りたい場合は getTopLevelClassesRecursive() を使う。


Reflections 利用

Reflections ライブラリだと以下のようになる。

Reflections reflections = new Reflections("my.package");
Set<Class<?>> allClasses = reflections.getSubTypesOf(Object.class);


Spring

ClassPathScanningCandidateComponentProvider を利用する。

ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile(".*")));

Set<Class<?>> allClasses = provider.findCandidateComponents("my.package").stream()
        .map(bean -> uncheckCall(() -> Class.forName(bean.getBeanClassName())));

uncheckCall は例外用のユーティリティ。

public static <T> T uncheckCall(Callable<T> callable) {
    try {
        return callable.call();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}


ライブラリ利用なし

フラットなファイルシステムから取る場合と jar から取る場合で分岐が必要で泥臭くなる。

public static Set<Class<?>> listClasses(String packageName) {
        
    final String resourceName = packageName.replace('.', '/');
    final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    final URL root = classLoader.getResource(resourceName);
        
    if ("file".equals(root.getProtocol())) {
        File[] files = new File(root.getFile()).listFiles((dir, name) -> name.endsWith(".class"));
        return Arrays.asList(files).stream()
                .map(file -> file.getName())
                .map(name -> name.replaceAll(".class$", ""))
                .map(name -> packageName + "." + name)
                .map(fullName -> uncheckCall(() -> Class.forName(fullName)))
                .collect(Collectors.toSet());
    }
    if ("jar".equals(root.getProtocol())) {
        try (JarFile jarFile = ((JarURLConnection) root.openConnection()).getJarFile()) {
            return Collections.list(jarFile.entries()).stream()
                    .map(jarEntry -> jarEntry.getName())
                    .filter(name -> name.startsWith(resourceName))
                    .filter(name -> name.endsWith(".class"))
                    .map(name -> name.replace('/', '.').replaceAll(".class$", ""))
                    .map(fullName -> uncheckCall(() -> classLoader.loadClass(fullName)))
                    .collect(Collectors.toSet());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    return new HashSet<>();
}


ToolProvider で取得

tools.jar が必要なので JDK 入れないとダメだけど ToolProvider 使うこともできる。

public static Set<Class<?>> listClasses(String packageName) throws Exception {
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    JavaFileManager jfm = compiler.getStandardFileManager(new DiagnosticCollector<>(), null, null);
        
    Set<JavaFileObject.Kind> kind = new HashSet<JavaFileObject.Kind>(){{
        add(JavaFileObject.Kind.CLASS);
    }};

    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        
    Iterable<JavaFileObject> iterable = jfm.list(StandardLocation.CLASS_PATH, packageName, kind, false);
    return StreamSupport.stream(iterable.spliterator(), false)
            .map(javaFileObject -> javaFileObject.toUri().toString())
            .map(uri -> uri.replace("/", "."))
            .map(dottedUri -> dottedUri.substring(dottedUri.indexOf(packageName)).replaceAll(".class$", ""))
            .map(fqcn -> uncheckCall(() -> classLoader.loadClass(fqcn)))
            .collect(Collectors.toSet());
    }