In a Java Project of mine, I would like to find out programmatically which classes from a given API are used. Is there a good way to do that? Through source code parsing or bytecode parsing maybe? Because Reflection won't be of any use, I'm afraid.
To make things simpler: there are no wildcard imports (import com.mycompany.api.*;
) anywhere in my project, no fully qualified field or variable definitions (private com.mycompany.api.MyThingy thingy;
) nor any Class.forName(...)
constructs. Given these limitations, it boils down to parsing import statements, I guess. Is there a preferred approach to do this?
I think the following might help you out:
You can discover the classes using ASM's Remapper
class (believe it or not). This class is actually meant to replace all occurrences of a class name within bytecode. For your purposes, however, it doesn't need to replace anything.
This probably doesn't make a whole lot of sense, so here is an example...
First, you create a subclass of Remapper
whose only purpose in life is to intercept all calls to the mapType(String)
method, recording its argument for later use.
public class ClassNameRecordingRemapper extends Remapper {
private final Set<? super String> classNames;
public ClassNameRecordingRemapper(Set<? super String> classNames) {
this.classNames = classNames;
}
@Override
public String mapType(String type) {
classNames.add(type);
return type;
}
}
Now you can write a method like this:
public Set<String> findClassNames(byte[] bytecode) {
Set<String> classNames = new HashSet<String>();
ClassReader classReader = new ClassReader(bytecode);
ClassWriter classWriter = new ClassWriter(classReader, 0);
ClassNameRecordingRemapper remapper = new ClassNameRecordingRemapper(classNames);
classReader.accept(remapper, 0);
return classNames;
}
It's your responsibility to actually obtain all classes' bytecode.
I am accepting this answer, but as the above code is not quite correct, I will insert the way I used this:
public static class Collector extends Remapper{
private final Set<Class<?>> classNames;
private final String prefix;
public Collector(final Set<Class<?>> classNames, final String prefix){
this.classNames = classNames;
this.prefix = prefix;
}
/**
* {@inheritDoc}
*/
@Override
public String mapDesc(final String desc){
if(desc.startsWith("L")){
this.addType(desc.substring(1, desc.length() - 1));
}
return super.mapDesc(desc);
}
/**
* {@inheritDoc}
*/
@Override
public String[] mapTypes(final String[] types){
for(final String type : types){
this.addType(type);
}
return super.mapTypes(types);
}
private void addType(final String type){
final String className = type.replace('/', '.');
if(className.startsWith(this.prefix)){
try{
this.classNames.add(Class.forName(className));
} catch(final ClassNotFoundException e){
throw new IllegalStateException(e);
}
}
}
@Override
public String mapType(final String type){
this.addType(type);
return type;
}
}
public static Set<Class<?>> getClassesUsedBy(
final String name, // class name
final String prefix // common prefix for all classes
// that will be retrieved
) throws IOException{
final ClassReader reader = new ClassReader(name);
final Set<Class<?>> classes =
new TreeSet<Class<?>>(new Comparator<Class<?>>(){
@Override
public int compare(final Class<?> o1, final Class<?> o2){
return o1.getName().compareTo(o2.getName());
}
});
final Remapper remapper = new Collector(classes, prefix);
final ClassVisitor inner = new EmptyVisitor();
final RemappingClassAdapter visitor =
new RemappingClassAdapter(inner, remapper);
reader.accept(visitor, 0);
return classes;
}
Here's a main class to test it using:
public static void main(final String[] args) throws Exception{
final Collection<Class<?>> classes =
getClassesUsedBy(Collections.class.getName(), "java.util");
System.out.println("Used classes:");
for(final Class<?> cls : classes){
System.out.println(" - " + cls.getName());
}
}
And here's the Output:
Used classes:
- java.util.ArrayList
- java.util.Arrays
- java.util.Collection
- java.util.Collections
- java.util.Collections$1
- java.util.Collections$AsLIFOQueue
- java.util.Collections$CheckedCollection
- java.util.Collections$CheckedList
- java.util.Collections$CheckedMap
- java.util.Collections$CheckedRandomAccessList
- java.util.Collections$CheckedSet
- java.util.Collections$CheckedSortedMap
- java.util.Collections$CheckedSortedSet
- java.util.Collections$CopiesList
- java.util.Collections$EmptyList
- java.util.Collections$EmptyMap
- java.util.Collections$EmptySet
- java.util.Collections$ReverseComparator
- java.util.Collections$ReverseComparator2
- java.util.Collections$SelfComparable
- java.util.Collections$SetFromMap
- java.util.Collections$SingletonList
- java.util.Collections$SingletonMap
- java.util.Collections$SingletonSet
- java.util.Collections$SynchronizedCollection
- java.util.Collections$SynchronizedList
- java.util.Collections$SynchronizedMap
- java.util.Collections$SynchronizedRandomAccessList
- java.util.Collections$SynchronizedSet
- java.util.Collections$SynchronizedSortedMap
- java.util.Collections$SynchronizedSortedSet
- java.util.Collections$UnmodifiableCollection
- java.util.Collections$UnmodifiableList
- java.util.Collections$UnmodifiableMap
- java.util.Collections$UnmodifiableRandomAccessList
- java.util.Collections$UnmodifiableSet
- java.util.Collections$UnmodifiableSortedMap
- java.util.Collections$UnmodifiableSortedSet
- java.util.Comparator
- java.util.Deque
- java.util.Enumeration
- java.util.Iterator
- java.util.List
- java.util.ListIterator
- java.util.Map
- java.util.Queue
- java.util.Random
- java.util.RandomAccess
- java.util.Set
- java.util.SortedMap
- java.util.SortedSet
Something like this perhaps:
import java.io.*;
import java.util.Scanner;
import java.util.regex.Pattern;
public class FileTraverser {
public static void main(String[] args) {
visitAllDirsAndFiles(new File("source_directory"));
}
public static void visitAllDirsAndFiles(File root) {
if (root.isDirectory())
for (String child : root.list())
visitAllDirsAndFiles(new File(root, child));
process(root);
}
private static void process(File f) {
Pattern p = Pattern.compile("(?=\\p{javaWhitespace}*)import (.*);");
if (f.isFile() && f.getName().endsWith(".java")) {
try {
Scanner s = new Scanner(f);
String cls = "";
while (null != (cls = s.findWithinHorizon(p, 0)))
System.out.println(cls);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
}
You may want to take comments into account, but it shouldn't be too hard. You could also make sure you only look for imports before the class declaration.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With