11

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?

gunr2171
  • 16,104
  • 25
  • 61
  • 88
Sean Patrick Floyd
  • 292,901
  • 67
  • 465
  • 588

8 Answers8

13

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
Sean Patrick Floyd
  • 292,901
  • 67
  • 465
  • 588
Adam Paynter
  • 46,244
  • 33
  • 149
  • 164
  • sounds like exactly what I'm looking for, I'll have a look at it, thanks (+1) – Sean Patrick Floyd Sep 17 '10 at 12:08
  • 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 Sep 17 '10 at 14:00
  • @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 Oct 08 '10 at 13:11
  • I like this, but it misses classes used inside methods; is there any way to pick up those references as well? – raffian Nov 08 '13 at 03:29
  • Very cool. Looks like EmptyVisitor has been removed. What to use instead https://github.com/bmc/javautil/pull/15/files?diff=split ? – user48956 Jun 22 '16 at 22:58
4

I think the following might help you out:

  1. Class Dependency Analyzer
  2. Dependency Finder
Faisal Feroz
  • 12,458
  • 4
  • 40
  • 51
  • 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. – Sean Patrick Floyd Sep 17 '10 at 11:29
  • 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 Sep 17 '10 at 11:56
  • 1
    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. – Sean Patrick Floyd Sep 17 '10 at 12:13
2
  1. Compiler Tree API
  2. byte code analysis - the fully qualified names should be in the constant pool
emory
  • 10,725
  • 2
  • 30
  • 58
  • 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) – Sean Patrick Floyd Sep 17 '10 at 12:07
  • 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 Sep 17 '10 at 12:16
  • 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. – Sean Patrick Floyd Sep 17 '10 at 12:34
  • I don't know how to use the Compiler Tree API without using a compile process. This is probably not suitable for you. – emory Sep 17 '10 at 13:03
1

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
  • 413,195
  • 112
  • 811
  • 826
  • 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. – Sean Patrick Floyd Sep 17 '10 at 12:14
1

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
  • 21,501
  • 10
  • 63
  • 107
0

Thanks Adam Paynter, It helped me. But what I was looking for is to fetch the dependent classes (recursively)- which means take a feature from a projects. So, need to get all the classes associated with a particular classes and again the used classes of those classes and so on. and also get the jars. So, I Created my own Java Dependency Resolver project Which will find the dependent Classes/jars for a particular class in a Project. I am sharing it here that may come to any use of some body.

Asraful Haque
  • 753
  • 8
  • 11
0

You may want to use STAN for that.

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

Sean Patrick Floyd
  • 292,901
  • 67
  • 465
  • 588
chris
  • 3,573
  • 22
  • 20
0

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

L. Holanda
  • 4,432
  • 1
  • 36
  • 44
  • 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. – Sean Patrick Floyd Sep 17 '10 at 19:03