Find java class dependencies at runtime

2019-03-02 14:50发布

What's the most effective way to get a list of dependencies for a Java class at runtime?

Using this (based on ASM ByteCode Manipulator 3.3.1), I can do the following:

final Collection<Class<?>> classes = 
  getClassesUsedBy(MyService.class.getName(), "com");

This returns references to BasicService and IService, but misses ContainerValue, and that's the issue. I played around with the ASM code but could not figure out how to pick up ContainerValue.

package com.foo.bar;

    public class MyService extends BasicService implements IService {
         public String invoke(){
            return new ContainerValue("bar").toString();
    }

As a side note, if I make ContainerValue the return type on invoke, it works.

Is there any alternative to using ASM to get a list of dependencies for a class? Why the heck is that so difficult?

2条回答
倾城 Initia
2楼-- · 2019-03-02 15:33

Holger's answer seems to work well, but I also found the code a bit cryptic. I did a little research and re-worked Holger's code for clarity and readability. I added a main() as an example, which filters out java.lang dependencies (which seem like clutter to me) and prints the full class names.

public class DependencyFinder {

public static void main(String[] args) {
    try {
        // Get dependencies for my class:
        Set<Class<?>> dependencies = getDependencies(Class
                .forName("com.example.MyClass"));  // REPLACE WITH YOUR CLASS NAME

        // Print the full class name for each interesting dependency:
        dependencies
                .stream()
                .filter(clazz -> !clazz.getCanonicalName().startsWith(
                        "java.lang")) // do not show java.lang dependencies,
                                        // which add clutter
                .forEach(c -> System.out.println(c.getCanonicalName()));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

/**
 * Get the set of direct dependencies for the given class
 * 
 * @param classToCheck
 * @return The direct dependencies for classToCheck, as a set of classes
 * @throws IOException
 * @throws ClassNotFoundException
 */
public static Set<Class<?>> getDependencies(final Class<?> classToCheck)
        throws IOException, ClassNotFoundException {
    Class<?> adjustedClassToCheck = adjustSourceClassIfArray(classToCheck);
    if (adjustedClassToCheck.isPrimitive()) {
        return Collections.emptySet();
    }
    return mapClassNamesToClasses(
            adjustedClassToCheck,
            getDependenciesFromClassBytes(readClassBytes(adjustedClassToCheck)));
}

private static Class<?> adjustSourceClassIfArray(final Class<?> sourceClass) {
    Class<?> adjustedSourceClass = sourceClass;
    while (adjustedSourceClass.isArray()) {
        adjustedSourceClass = sourceClass.getComponentType();
    }
    return adjustedSourceClass;
}

private static Set<Class<?>> mapClassNamesToClasses(Class<?> from,
        Set<String> names) throws ClassNotFoundException {
    ClassLoader cl = from.getClassLoader();
    Set<Class<?>> classes = new HashSet<>(names.size());

    for (String name : names) {
        classes.add(Class.forName(name, false, cl));
    }
    classes.remove(from);// remove self-reference
    return classes;
}

private static ByteBuffer readClassBytes(Class<?> from) throws IOException {
    Buffer readBuf = new Buffer();
    try (InputStream is = from.getResourceAsStream(from.getSimpleName()
            + ".class")) {
        int byteCountFromLastRead = 0;
        do {
            readBuf.read += byteCountFromLastRead;
            adustBufferSize(readBuf, is);
            byteCountFromLastRead = is.read(readBuf.buf, readBuf.read,
                    readBuf.buf.length - readBuf.read);
        } while (byteCountFromLastRead > 0);
    }
    return readBuf.toByteBuffer();
}

private static void adustBufferSize(Buffer readBuf, InputStream is)
        throws IOException {
    int bufferSize = Math.max(is.available() + 100, 100);
    if (readBuf.buf == null) {
        readBuf.buf = new byte[bufferSize];
    } else if (readBuf.buf.length - readBuf.read < bufferSize) {
        System.arraycopy(readBuf.buf, 0,
                readBuf.buf = new byte[readBuf.read + bufferSize], 0,
                readBuf.read);
    }
}

private static Set<String> getDependenciesFromClassBytes(
        ByteBuffer readBuffer) {
    verifyMagicFileTypeHeader(readBuffer);
    final int constantPoolItemCount = getConstantPoolItemCount(readBuffer);
    ConstantPoolItemFlags flags = new ConstantPoolItemFlags(constantPoolItemCount);
    flagConstantPoolItemsAsDependencies(readBuffer, constantPoolItemCount, flags);
    return extractClassNamesFromConstantsBasedOnFlags(readBuffer,
            constantPoolItemCount, flags);
}

private static void flagConstantPoolItemsAsDependencies(ByteBuffer readBuffer,
        final int constantPoolItemCount, ConstantPoolItemFlags flags) {
    for (int c = 1; c < constantPoolItemCount; c++) {
        c = readOneConstantPoolItemAndSetFlagIfClassOrNamedType(readBuffer,
                flags, c);
    }
    skipPastAccessFlagsThisClassAndSuperClass(readBuffer);
    skipInterfaces(readBuffer);
    flagFieldsAndMethodsAsNamedTypes(readBuffer, flags.isNamedType);
}

private static int getConstantPoolItemCount(ByteBuffer readBuffer) {
    setCursorToConstantPoolCountPosition(readBuffer);
    final int constantPoolCount = readBuffer.getChar();
    return constantPoolCount;
}

/**
 * @param readBuffer
 */
private static void skipInterfaces(ByteBuffer readBuffer) {
    readBuffer.position(readBuffer.getChar() * 2 + readBuffer.position());
}

/**
 * @param readBuffer
 */
private static void skipPastAccessFlagsThisClassAndSuperClass(
        ByteBuffer readBuffer) {
    skipBytes(readBuffer, 6);
}

/**
 * @param readBuffer
 * @param numberOfConstants
 * @param isClass
 * @param isNamedType
 * @return
 * @throws AssertionError
 */
private static HashSet<String> extractClassNamesFromConstantsBasedOnFlags(
        ByteBuffer readBuffer, final int numberOfConstants, ConstantPoolItemFlags flags) throws AssertionError {
    HashSet<String> names = new HashSet<>();
    returnBufferToStartOfConstantPool(readBuffer);
    for (int constantPoolIndex = 1; constantPoolIndex < numberOfConstants; constantPoolIndex++) {
        switch (readBuffer.get()) {
        case CONSTANT_Utf8:
            readClassNamesInUTF8Value(readBuffer, flags,
                    names, constantPoolIndex);
            break;
        case CONSTANT_Integer:
        case CONSTANT_Float:
        case CONSTANT_FieldRef:
        case CONSTANT_MethodRef:
        case CONSTANT_InterfaceMethodRef:
        case CONSTANT_NameAndType:
        case CONSTANT_InvokeDynamic:
            skipBytes(readBuffer, 4);
            break;
        case CONSTANT_Long:
        case CONSTANT_Double:
            skipBytes(readBuffer, 8);
            constantPoolIndex++; // long or double counts as 2 items
            break;
        case CONSTANT_String:
        case CONSTANT_Class:
        case CONSTANT_MethodType:
            skipBytes(readBuffer, 2);
            break;
        case CONSTANT_MethodHandle:
            skipBytes(readBuffer, 3);
            break;
        default:
            throw new AssertionError();
        }
    }
    return names;
}

/**
 * @param readBuffer
 * @param isClass
 * @param isNamedType
 * @param dependencyClassNames
 * @param constantNumber
 */
private static void readClassNamesInUTF8Value(ByteBuffer readBuffer,
        ConstantPoolItemFlags flags,
        HashSet<String> dependencyClassNames, int constantNumber) {
    int strSize = readBuffer.getChar(), strStart = readBuffer.position();
    boolean multipleNames = flags.isNamedType.get(constantNumber);
    if (flags.isClass.get(constantNumber)) {
        if (readBuffer.get(readBuffer.position()) == ARRAY_START_CHAR) {
            multipleNames = true;
        } else {
            addClassNameToDependencySet(dependencyClassNames, readBuffer,
                    strStart, strSize);
        }
    }
    if (multipleNames) {
        addClassNamesToDependencySet(dependencyClassNames, readBuffer,
                strStart, strSize);
    }
    readBuffer.position(strStart + strSize);
}

/**
 * @param readBuffer
 * @param isNamedType
 */
private static void flagFieldsAndMethodsAsNamedTypes(ByteBuffer readBuffer,
        BitSet isNamedType) {
    for (int type = 0; type < 2; type++) { // fields and methods
        int numMember = readBuffer.getChar();
        for (int member = 0; member < numMember; member++) {
            skipBytes(readBuffer, 4);
            isNamedType.set(readBuffer.getChar());
            int numAttr = readBuffer.getChar();
            for (int attr = 0; attr < numAttr; attr++) {
                skipBytes(readBuffer, 2);
                readBuffer.position(readBuffer.getInt()
                        + readBuffer.position());
            }
        }
    }
}

/**
 * @param readBuffer
 */
private static void returnBufferToStartOfConstantPool(ByteBuffer readBuffer) {
    readBuffer.position(10);
}

/**
 * @param readBuffer
 * @param isClass
 * @param isNamedType
 * @param currentConstantIndex
 * @return
 */
private static int readOneConstantPoolItemAndSetFlagIfClassOrNamedType(
        ByteBuffer readBuffer, ConstantPoolItemFlags flags,
        int currentConstantIndex) {
    switch (readBuffer.get()) {
    case CONSTANT_Utf8:
        skipPastVariableLengthString(readBuffer);
        break;
    case CONSTANT_Integer:
    case CONSTANT_Float:
    case CONSTANT_FieldRef:
    case CONSTANT_MethodRef:
    case CONSTANT_InterfaceMethodRef:
    case CONSTANT_InvokeDynamic:
        skipBytes(readBuffer, 4);
        break;
    case CONSTANT_Long:
    case CONSTANT_Double:
        skipBytes(readBuffer, 8);
        currentConstantIndex++;
        break;
    case CONSTANT_String:
        skipBytes(readBuffer, 2);
        break;
    case CONSTANT_NameAndType:
        skipBytes(readBuffer, 2);// skip name, fall through to flag as a
                                    // named type:
    case CONSTANT_MethodType:
        flags.isNamedType.set(readBuffer.getChar()); // flag as named type
        break;
    case CONSTANT_Class:
        flags.isClass.set(readBuffer.getChar()); // flag as class
        break;
    case CONSTANT_MethodHandle:
        skipBytes(readBuffer, 3);
        break;
    default:
        throw new IllegalArgumentException("constant pool item type "
                + (readBuffer.get(readBuffer.position() - 1) & 0xff));
    }
    return currentConstantIndex;
}

private static void skipBytes(ByteBuffer readBuffer, int bytesToSkip) {
    readBuffer.position(readBuffer.position() + bytesToSkip);
}

private static void skipPastVariableLengthString(ByteBuffer readBuffer) {
    readBuffer.position(readBuffer.getChar() + readBuffer.position());
}

private static void setCursorToConstantPoolCountPosition(
        ByteBuffer readBuffer) {
    readBuffer.position(8);
}

private static void verifyMagicFileTypeHeader(ByteBuffer readBuffer) {
    if (readBuffer.getInt() != 0xcafebabe) {
        throw new IllegalArgumentException("Not a class file");
    }
}

private static void addClassNameToDependencySet(HashSet<String> names,
        ByteBuffer readBuffer, int start, int length) {
    final int end = start + length;
    StringBuilder dst = new StringBuilder(length);
    ascii: {
        for (; start < end; start++) {
            byte b = readBuffer.get(start);
            if (b < 0) {
                break ascii;
            }
            dst.append((char) (b == '/' ? '.' : b));
        }
        names.add(dst.toString());
        return;
    }
    final int oldLimit = readBuffer.limit(), oldPos = dst.length();
    readBuffer.limit(end).position(start);
    dst.append(StandardCharsets.UTF_8.decode(readBuffer));
    readBuffer.limit(oldLimit);
    for (int pos = oldPos, len = dst.length(); pos < len; pos++) {
        if (dst.charAt(pos) == '/') {
            dst.setCharAt(pos, '.');
        }
    }
    names.add(dst.toString());
    return;
}

private static void addClassNamesToDependencySet(HashSet<String> names,
        ByteBuffer readBuffer, int start, int length) {
    final int end = start + length;
    for (; start < end; start++) {
        if (readBuffer.get(start) == 'L') {
            int endMarkerPosition = start + 1;
            while (readBuffer.get(endMarkerPosition) != ';') {
                endMarkerPosition++;
            }
            addClassNameToDependencySet(names, readBuffer, start + 1,
                    calculateLength(start, endMarkerPosition));
            start = endMarkerPosition;
        }
    }
}

private static int calculateLength(int start, int endMarkerPosition) {
    return endMarkerPosition - start - 1;
}

private static final char ARRAY_START_CHAR = '[';

// Constant pool data type constants:
private static final byte CONSTANT_Utf8 = 1, CONSTANT_Integer = 3,
        CONSTANT_Float = 4, CONSTANT_Long = 5, CONSTANT_Double = 6,
        CONSTANT_Class = 7, CONSTANT_String = 8, CONSTANT_FieldRef = 9,
        CONSTANT_MethodRef = 10, CONSTANT_InterfaceMethodRef = 11,
        CONSTANT_NameAndType = 12, CONSTANT_MethodHandle = 15,
        CONSTANT_MethodType = 16, CONSTANT_InvokeDynamic = 18;

// encapsulate byte buffer with its read count:
private static class Buffer {
    byte[] buf = null;
    int read = 0;

    // convert to ByteBuffer
    ByteBuffer toByteBuffer() {
        return ByteBuffer.wrap(this.buf, 0, this.read);
    }
}

// flags for identifying dependency names in the constant pool
private static class ConstantPoolItemFlags {
    final BitSet isClass;
    final BitSet isNamedType;

    ConstantPoolItemFlags(int constantPoolItemCount) {
        isClass = new BitSet(constantPoolItemCount);
        isNamedType = new BitSet(constantPoolItemCount);
    }
}

}

查看更多
▲ chillily
3楼-- · 2019-03-02 15:35

“Is there any alternative to using ASM to get a list of dependencies for a class?” Well there are several alternatives. One is to implement the operation without additional libraries.

“Why the heck is that so difficult?” It’s not that difficult. But you should not judge by looking at a mighty library intended for lots of different use cases when you need it for a rather tiny task.

Here is a piece of code performing the entire dependency scan in a straightforward manner. It’s quite efficient but will become a nightmare once you want it to do other things than that. So once you need other byte code operations I recommend turning back to use a library for it.

public static Set<Class<?>> getDependencies(Class<?> from)
  throws IOException, ClassNotFoundException {

  while(from.isArray()) from=from.getComponentType();
  if(from.isPrimitive()) return Collections.emptySet();
  byte[] buf=null;
  int read=0;
  try(InputStream is=from.getResourceAsStream(from.getSimpleName()+".class")) {
    for(int r; ;read+=r) {
      int num=Math.max(is.available()+100, 100);
      if(buf==null) buf=new byte[num];
      else if(buf.length-read<num)
        System.arraycopy(buf, 0, buf=new byte[read+num], 0, read);
      r=is.read(buf, read, buf.length-read);
      if(r<=0) break;
    }
  }
  Set<String> names=getDependencies(ByteBuffer.wrap(buf, 0, read));
  Set<Class<?>> classes=new HashSet<>(names.size());
  ClassLoader cl=from.getClassLoader();
  for(String name:names) classes.add(Class.forName(name, false, cl));
  classes.remove(from);// remove self-reference
  return classes;
}

public static Set<String> getDependencies(ByteBuffer bb) {

  if(bb.getInt()!=0xcafebabe)
    throw new IllegalArgumentException("Not a class file");
  bb.position(8);
  final int numC=bb.getChar();
  BitSet clazz=new BitSet(numC), sign=new BitSet(numC);
  for(int c=1; c<numC; c++) {
    switch(bb.get()) {
      case CONSTANT_Utf8: bb.position(bb.getChar()+bb.position()); break;
      case CONSTANT_Integer: case CONSTANT_Float:
      case CONSTANT_FieldRef: case CONSTANT_MethodRef:
      case CONSTANT_InterfaceMethodRef: case CONSTANT_InvokeDynamic:
        bb.position(bb.position()+4); break;
      case CONSTANT_Long: case CONSTANT_Double:
        bb.position(bb.position()+8); c++; break;
      case CONSTANT_String: bb.position(bb.position()+2); break;
      case CONSTANT_NameAndType:
        bb.position(bb.position()+2);// skip name, fall through:
      case CONSTANT_MethodType: sign.set(bb.getChar()); break;
      case CONSTANT_Class: clazz.set(bb.getChar()); break;
      case CONSTANT_MethodHandle: bb.position(bb.position()+3); break;
      default: throw new IllegalArgumentException(
        "constant pool item type "+(bb.get(bb.position()-1)&0xff));
    }
  }
  bb.position(bb.position()+6);
  bb.position(bb.getChar()*2+bb.position());
  for(int type=0; type<2; type++) { // fields and methods
    int numMember=bb.getChar();
    for(int member=0; member<numMember; member++) {
      bb.position(bb.position()+4);
      sign.set(bb.getChar());
      int numAttr=bb.getChar();
      for(int attr=0; attr<numAttr; attr++) {
        bb.position(bb.position()+2);
        bb.position(bb.getInt()+bb.position());
      }
    }
  }
  bb.position(10);
  HashSet<String> names=new HashSet<>();
  for(int c=1; c<numC; c++) {
    switch(bb.get()) {
      case CONSTANT_Utf8:
        int strSize=bb.getChar(), strStart=bb.position();
        boolean s = sign.get(c);
        if(clazz.get(c))
          if(bb.get(bb.position())=='[') s=true;
          else addName(names, bb, strStart, strSize);
        if(s) addNames(names, bb, strStart, strSize);
        bb.position(strStart+strSize);
        break;
      case CONSTANT_Integer: case CONSTANT_Float:
      case CONSTANT_FieldRef: case CONSTANT_MethodRef:
      case CONSTANT_InterfaceMethodRef: case CONSTANT_NameAndType:
      case CONSTANT_InvokeDynamic:
        bb.position(bb.position()+4); break;
      case CONSTANT_Long: case CONSTANT_Double:
        bb.position(bb.position()+8); c++; break;
      case CONSTANT_String: case CONSTANT_Class:case CONSTANT_MethodType:
        bb.position(bb.position()+2); break;
      case CONSTANT_MethodHandle: bb.position(bb.position()+3); break;
      default: throw new AssertionError();
    }
  }
  return names;
}

private static void addName(HashSet<String> names,
  ByteBuffer src, int s, int strSize) {
  final int e=s+strSize;
  StringBuilder dst=new StringBuilder(strSize);
  ascii: {
    for(;s<e; s++) {
      byte b=src.get(s);
      if(b<0) break ascii;
      dst.append((char)(b=='/'? '.': b));
    }
    names.add(dst.toString());
    return;
  }
  final int oldLimit=src.limit(), oldPos=dst.length();
  src.limit(e).position(s);
  dst.append(StandardCharsets.UTF_8.decode(src));
  src.limit(oldLimit);
  for(int pos=oldPos, len=dst.length(); pos<len; pos++)
    if(dst.charAt(pos)=='/') dst.setCharAt(pos, '.');
  names.add(dst.toString());
  return;
}

private static void addNames(HashSet<String> names,
  ByteBuffer bb, int s, int l) {
  final int e=s+l;
  for(;s<e; s++) {
    if(bb.get(s)=='L') {
      int p=s+1; while(bb.get(p)!=';') p++;
      addName(names, bb, s+1, p-s-1);
      s=p;
    }
  }
}
private static final byte CONSTANT_Utf8 = 1, CONSTANT_Integer = 3,
  CONSTANT_Float = 4, CONSTANT_Long = 5, CONSTANT_Double = 6,
  CONSTANT_Class = 7, CONSTANT_String = 8, CONSTANT_FieldRef = 9,
  CONSTANT_MethodRef = 10, CONSTANT_InterfaceMethodRef = 11,
  CONSTANT_NameAndType = 12, CONSTANT_MethodHandle = 15,
  CONSTANT_MethodType = 16, CONSTANT_InvokeDynamic = 18;
查看更多
登录 后发表回答