Abhängigkeiten zwischen Java-Klassen mit BCEL ermitteln

Das folgende Code-Beispiel zeigt, wie man mit der BCEL-API aus dem JavaSE alle Abhängikeiten einer Klasse ermitteln kann. Die BCEL-API ist eine interne API, sie sollte also nie für produktiven Code verwendet werden. Für einfache Tests spricht aber nichts dagegen. Dadurch spart man den Aufwand eine Fremdbibliothek einzubinden.

Es ist nicht möglich, alle Abhängigkeiten einer Klasse über die Reflection-API zu ermitteln, weil diese keinen Zugriff auf die Internas einer Methode bietet. Man bekommt beispielsweise nicht per Reflection heraus, wenn eine fremde Klasse nur für eine lokale Variable verwendet wird. Deswegen muss man das Class-File analysieren. Dieses hat eine Abschnitt 'ConstantPool', der u. a. für jede verwendete Klasse einen entsprechenden Class-Eintrag enthält.

package de.spricom.archa.test;

import static org.fest.assertions.Assertions.assertThat;

import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.Test;

import com.sun.org.apache.bcel.internal.classfile.ClassParser;
import com.sun.org.apache.bcel.internal.classfile.Constant;
import com.sun.org.apache.bcel.internal.classfile.ConstantClass;
import com.sun.org.apache.bcel.internal.classfile.ConstantUtf8;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;

public class BcelClassParserTest {
  private final Pattern classArrayPattern = Pattern.compile("\\[+L(.*);");

  @Test
  public void test() throws IOException {
    JavaClass javaClass = read(HashSet.class);
    dump(javaClass);
    Set<String> dependentClasses = getDependentClasses(javaClass);
    assertThat(dependentClasses).containsOnly(
        InternalError.class.getName(),
        Math.class.getName(),
        InvalidObjectException.class.getName(),
        StringBuilder.class.getName(),
        Iterator.class.getName(),
        AbstractSet.class.getName(),
        Cloneable.class.getName(),
        HashMap.class.getName(),
        ObjectOutputStream.class.getName(),
        Float.class.getName(),
        CloneNotSupportedException.class.getName(),
        Collection.class.getName(),
        Serializable.class.getName(),
        Object.class.getName(),
        Set.class.getName(),
        ClassNotFoundException.class.getName(),
        LinkedHashSet.class.getName(),
        LinkedHashMap.class.getName(),
        IOException.class.getName(),
        ObjectInputStream.class.getName());
  }

  private void dump(JavaClass cl) {
    Constant[] constantPool = cl.getConstantPool().getConstantPool();
    for (int i = 0; i < constantPool.length; i++) {
      System.out.printf("%3d. %s%n", i, String.valueOf(constantPool[i]));
    }
  }

  private JavaClass read(Class<?> clazz) throws IOException {
    if (clazz.getDeclaringClass() != null) {
      clazz = clazz.getDeclaringClass();
    }
    InputStream in = clazz.getResourceAsStream(clazz.getSimpleName() + ".class");
    ClassParser classParser = new ClassParser(in, clazz.getSimpleName() + ".class");
    return classParser.parse();
  }

  public Set<String> getDependentClasses(JavaClass javaClass) {
    Constant[] constantPool = javaClass.getConstantPool().getConstantPool();
    Set<String> classes = new HashSet<>();
    for (int i = 1; i < constantPool.length; i++) {
      if (constantPool[i] != null 
          // the constant pool entry after long or double is null 
          && i != javaClass.getClassNameIndex()
          // skip javaClass, because it does not depend on itself
          && constantPool[i] instanceof ConstantClass) {
          // only class entries in constant pool are relevant
        ConstantClass cl = (ConstantClass) constantPool[i];
        ConstantUtf8 name = (ConstantUtf8) constantPool[cl.getNameIndex()];
        
        // handle arrays
        Matcher matcher = classArrayPattern.matcher(name.getBytes());
        String classname;
        if (matcher.matches()) {
          classname = matcher.group(1);
        } else {
          classname = name.getBytes();
        }
        
        // ignore inner classes
        int pos = classname.indexOf('$');
        if (pos != -1) {
          classname = classname.substring(0, pos);
        }
        
        // use Java package notition
        classname = classname.replace('/', '.');
        classes.add(classname);
      }
    }
    return classes;
  }
}