Getting all Classes from a Package
Asked Answered
F

9

16

Lets say I have a java package commands which contains classes that all inherit from ICommand can I get all of those classes somehow? I'm locking for something among the lines of:

Package p = Package.getPackage("commands");
Class<ICommand>[] c = p.getAllPackagedClasses(); //not real 

Is something like that possible?

Foredoom answered 27/11, 2009 at 20:57 Comment(3)
please edit this to be legible. What is a 'javapackage'. What is a 'commands wish'.Bobsleigh
The topic subject is perfect: google.com/search?q=Getting+all+Classes+from+a+Package =)Orderly
@balusc, but the /correct/ answer is hard.Queenqueena
O
21

Here's a basic example, assuming that classes are not JAR-packaged:

// Prepare.
String packageName = "com.example.commands";
List<Class<ICommand>> commands = new ArrayList<Class<ICommand>>();
URL root = Thread.currentThread().getContextClassLoader().getResource(packageName.replace(".", "/"));

// Filter .class files.
File[] files = new File(root.getFile()).listFiles(new FilenameFilter() {
    public boolean accept(File dir, String name) {
        return name.endsWith(".class");
    }
});

// Find classes implementing ICommand.
for (File file : files) {
    String className = file.getName().replaceAll(".class$", "");
    Class<?> cls = Class.forName(packageName + "." + className);
    if (ICommand.class.isAssignableFrom(cls)) {
        commands.add((Class<ICommand>) cls);
    }
}
Orderly answered 27/11, 2009 at 21:19 Comment(2)
+1, though a small improvement: root.getFile() should be URLDecoder.decode(root.getFile(), "UTF-8") in case of any spaces in classloader path. getResource() will convert this to %20, along with other characters I suppose.Cung
I put "java.base" in the .getResource but it returns null... Do you know why?Obstruct
S
7

Below is an implementation using the JSR-199 API, i.e. classes from javax.tools.*:

List<Class> commands = new ArrayList<>();

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(
        null, null, null);

StandardLocation location = StandardLocation.CLASS_PATH;
String packageName = "commands";
Set<JavaFileObject.Kind> kinds = new HashSet<>();
kinds.add(JavaFileObject.Kind.CLASS);
boolean recurse = false;

Iterable<JavaFileObject> list = fileManager.list(location, packageName,
        kinds, recurse);

for (JavaFileObject classFile : list) {
    String name = classFile.getName().replaceAll(".*/|[.]class.*","");
    commands.add(Class.forName(packageName + "." + name));
}

Works for all packages and classes on the class path, packaged in jar files or without. For classes not explicitly added to the class path, i.e. those loaded by the bootstrap class loader, try setting location to PLATFORM_CLASS_PATH instead.

Sapphira answered 28/11, 2009 at 0:30 Comment(7)
Interesting, but ToolProvider.getSystemJavaCompiler() returns null in both Eclipse and CLI? Is something more needed?Orderly
After digging, this apparently requires a JDK instead of JRE (which I however have in CLI, I'll dig later why this doesn't work). That would mean that you need to install a JDK in the final/prod environment to get it to work. That may be a showstopper.Orderly
Indeed, you need a JDK to get a non null compiler object (see bit.ly/89HtA0) so this API doesn't really target the desktop. It should be OK on the server-side though (most app servers use a JDK after all, e.g. for JSP compilation). But of course, there might be exceptions. Actually, I just wanted to show that there was some support in the JDK even if this isn't a perfect use case for the Compiler API (it's a poor usage here). This API can do more, much more, for example compiling on the fly Java source code generated dynamically in-memory (which is already more interesting).Sapphira
It's indeed an interesting API. But unfortunately really only for the developer.Orderly
When i run this javaFileObject.getClass() returns the class of the javaFileObject ie com.sun.tools.javac.util.DefaultFileManager$RegularFileObject. This class does not override Object.getClass()Grodin
It is incorrect as @Grodin mentions. javaFileObject.getClass() doesn't return a corresponding class. It calls Object.getClass().Optional
Use javaFileObject.getName() to get something like /somewhere/project.jar/(com/example/Example.class) while classes are packaged in a jar, or /somewhere/com/example/Example.class when not packaged in a jar, such as running in IDE. Then use string manipulations to get the class names. Of course, you may need to take care inner classes which ended with $1 or $n..Adalia
Y
3

Here is an utility method, using Spring.

Details about the pattern can be found here

    public static List<Class> listMatchingClasses(String matchPattern) throws IOException {
    List<Class> classes = new LinkedList<Class>();
    PathMatchingResourcePatternResolver scanner = new PathMatchingResourcePatternResolver();
    Resource[] resources = scanner.getResources(matchPattern);

    for (Resource resource : resources) {
        Class<?> clazz = getClassFromResource(resource);
        classes.add(clazz);
    }

    return classes;
}



public static Class getClassFromResource(Resource resource) {
    try {
        String resourceUri = resource.getURI().toString();
        resourceUri = resourceUri.replace(esourceUri.indexOf(".class"), "").replace("/", ".");
        // try printing the resourceUri before calling forName, to see if it is OK.
        return Class.forName(resourceUri);
    } catch (Exception ex) {
        ex.printStackTrace();
    }
    return null;
}
Yhvh answered 27/11, 2009 at 21:12 Comment(0)
T
3

If you do not want to use external depencies and you want to work on your IDE / on a JAR file, you can try this:

public static List<Class<?>> getClassesForPackage(final String pkgName) throws IOException, URISyntaxException {
    final String pkgPath = pkgName.replace('.', '/');
    final URI pkg = Objects.requireNonNull(ClassLoader.getSystemClassLoader().getResource(pkgPath)).toURI();
    final ArrayList<Class<?>> allClasses = new ArrayList<Class<?>>();

    Path root;
    if (pkg.toString().startsWith("jar:")) {
        try {
            root = FileSystems.getFileSystem(pkg).getPath(pkgPath);
        } catch (final FileSystemNotFoundException e) {
            root = FileSystems.newFileSystem(pkg, Collections.emptyMap()).getPath(pkgPath);
        }
    } else {
        root = Paths.get(pkg);
    }

    final String extension = ".class";
    try (final Stream<Path> allPaths = Files.walk(root)) {
        allPaths.filter(Files::isRegularFile).forEach(file -> {
            try {
                final String path = file.toString().replace('/', '.');
                final String name = path.substring(path.indexOf(pkgName), path.length() - extension.length());
                allClasses.add(Class.forName(name));
            } catch (final ClassNotFoundException | StringIndexOutOfBoundsException ignored) {
            }
        });
    }
    return allClasses;
}

From: Can you find all classes in a package using reflection?

Trichloroethylene answered 8/11, 2019 at 20:19 Comment(0)
B
2

Start with public Classloader.getResources(String name). Ask the classloader for a class corresponding to each name in the package you are interested. Repeat for all classloaders of relevance.

Bobsleigh answered 27/11, 2009 at 21:4 Comment(0)
F
1

Yes but its not the easiest thing to do. There are lots of issues with this. Not all of the classes are easy to find. Some classes could be in a: Jar, as a class file, over the network etc.

Take a look at this thread.

To make sure they were the ICommand type then you would have to use reflection to check for the inheriting class.

Farkas answered 27/11, 2009 at 21:10 Comment(0)
M
1

This would be a very useful tool we need, and JDK should provide some support.

But it's probably better done during build. You know where all your class files are and you can inspect them statically and build a graph. At runtime you can query this graph to get all subtypes. This requires more work, but I believe it really belongs to the build process.

Millennial answered 27/11, 2009 at 23:31 Comment(1)
Something like the JSR-199 API? See #1811114Sapphira
M
0

Using Johannes Link's ClasspathSuite, I was able to do it like this:

import org.junit.extensions.cpsuite.ClassTester;
import org.junit.extensions.cpsuite.ClasspathClassesFinder;

public static List<Class<?>> getClasses(final Package pkg, final boolean includeChildPackages) {
    return new ClasspathClassesFinder(new ClassTester() {
        @Override public boolean searchInJars() { return true; }
        @Override public boolean acceptInnerClass() { return false; }
        @Override public boolean acceptClassName(String name) {
            return name.startsWith(pkg.getName()) && (includeChildPackages || name.indexOf(".", pkg.getName().length()) != -1);
        }
        @Override public boolean acceptClass(Class<?> c) { return true; }
    }, System.getProperty("java.class.path")).find();
}

The ClasspathClassesFinder looks for class files and jars in the system classpath.

In your specific case, you could modify acceptClass like this:

@Override public boolean acceptClass(Class<?> c) {
    return ICommand.class.isAssignableFrom(c);
}

One thing to note: be careful what you return in acceptClassName, as the next thing ClasspathClassesFinder does is to load the class and call acceptClass. If acceptClassName always return true, you'll end up loading every class in the classpath and that may cause an OutOfMemoryError.

Madness answered 18/12, 2009 at 20:47 Comment(0)
K
0

You could use OpenPojo and do this:

final List<PojoClass> pojoClasses = PojoClassFactory.getPojoClassesRecursively("my.package.path", null);

Then you can go over the list and perform any functionality you desire.

Kemme answered 28/8, 2012 at 4:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.