views:

141

answers:

7

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 byte code 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?

+1  A: 

I think the following might help you out:

  1. Class Dependency Analyzer
  2. Dependency Finder
Faisal Feroz
a) Thanks for answering b) I don't think either of these will help, as I want to run some java code based on the results automatically.
seanizer
Dependency Finder is open source. You can check out and integrate the the code in your project or you can do textual analysis on the report generated.
Faisal Feroz
Thanks for the answer (+1), but there are several technologies listed here that do exactly what I'm looking for so I won't inspect the close calls.
seanizer
+1  A: 

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.

aioobe
This is exactly the way I would hack up a solution myself (+1 for that :-) ), but I am looking for a solution that actually understands either source or byte code.
seanizer
+4  A: 

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.


EDIT by seanizer (OP)

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
Adam Paynter
sounds like exactly what I'm looking for, I'll have a look at it, thanks (+1)
seanizer
That looks groovy! I especially like the output. I just realized that the "class names" you are receiving appear to be the *internal* class names. ASM supplies the [`Type`](http://asm.ow2.org/asm33/javadoc/user/org/objectweb/asm/Type.html) class to help you with this. For example, you can use `Type type = Type.getType(internalClassName)`. From here, you can then call `type.getClassName()`, which is the class name you're used to.
Adam Paynter
OK thanks, that will make things simpler :-)
seanizer
+1. nice to know.
Bozho
@Bohzo: This is a hilarious work-around to Stack Overflow's lack of a messaging system! :) I want you to have that answer.
Adam Paynter
+2  A: 
  1. Compiler Tree API
  2. byte code analysis - the fully qualified names should be in the constant pool
emory
I'm currently playing with byte code analysis using bcel, but it's a pain. I'd prefer the Compiler Tree API, but what's the starting point? (I have no Compilation Task and no ProcessingEnvironment)
seanizer
You can run an annotation processor - http://download.oracle.com/javase/6/docs/api/javax/annotation/processing/AbstractProcessor.html on ur source code to get a ProcessingEnvironment
emory
I know how to start an annotation processor, but I don't want to do this from within the compile process, nor start a second compile process.
seanizer
I don't know how to use the Compiler Tree API without using a compile process. This is probably not suitable for you.
emory
OK thanks anyway.
seanizer
+1  A: 

I use DependencyFinder exactly for that purpose. It can analyse bytecode and extract all dependencies, then dump a report in txt or xml format (see the DependencyExtractor tool). You should be able to programatically analyse the report from your application's code.

I have integrated this in my build process in order to check that certain APIs are NOT used by an application.

Grodriguez
I'll upvote this because it's a good answer, but I don't think DependencyFinder and I can ever be friends :-)
seanizer
A: 

You may want to use STAN for that.

The "Couplings View" visualizes the dependencies to your API in a nice graph.

chris
Nice, but not an answer to my question, I need a programmatic way to get at the used classes, not a pretty report.
seanizer
A: 

If you use Eclipse. Try using the profiling tools. It doesn't only tells which classes are being used, but tells much more about it. The results will be something like:

alt text

There is a very good quickstart at:

http://www.eclipse.org/tptp/home/documents/tutorials/profilingtool/profilingexample_32.html

Leo Holanda
Try reading the question and the other answers carefully next time. I was looking for something I could use programmatically. What you are suggesting is a nice tool, but not the answer to my question.
seanizer