简体   繁体   中英

Java compile-time class resolution

How can I get a class reference/TypeElement of a specific identifier at compile time in Java?

Say I have the following source file, and I want to get a reference to the Pixel class so I can get a list of its member fields.

package com.foo.bar;
class Test {
    class Pixel {
        int x,y,r,g,b;
    }
    Pixel saturate(Pixel p, int value) {...}
}

The Pixel class definition could be nested inside the Test class, or included from a different package where the source is not available.

I am using the javax.tools API to compile the source files, and I define visitor methods so I can view the arguments to each function. The arguments of a function can be iterated using VariableTree var : node.getParameters() , but the type information from var.getType() only triggers visitIdentifier for class names. This identifier is only the simple name Pixel , not the fully-qualified com.foo.bar.Pixel .

I need a way to reverse this identifier into either Pixel.class or into the TypeElement for the definition of the Pixel class, or into the fully-qualified com.foo.bar.Pixel string so I can then use a ClassLoader on it.

A crude way would be to record all class definitions and then try to do compile-time type lookup, but this wouldn't work for externally-defined classes.

As far as I remember var.getType().toString() returns you the fully qualified class name. Unfortunately I cannot check it right now, but try it yourself.

Yes, I know, it is a very bad style to use toString() for something except logging but it seems they have not given us other choice.

I ended up creating my own class lookup tool. For those that are interested, I'll include it here.

Call this with the path name of every source file to be included in the search:

  public void populateClassDefinitions(String path) {
    Iterable<? extends JavaFileObject> files = fileManager.getJavaFileObjects(path);
    CompilationTask task =
        compiler.getTask(null, fileManager, diagnosticsCollector, null, null, files);
    final JavacTask javacTask = (JavacTask) task;
    parseResult = null;
    try {
      parseResult = javacTask.parse();
    } catch (IOException e) {
      e.printStackTrace();
      return;
    }
    for (CompilationUnitTree tree : parseResult) {
      tree.accept(new TreeScanner<Void, Void>() {
        @Override
        public Void visitCompilationUnit(CompilationUnitTree node, Void p) {
          currentPackage = "";
          ExpressionTree packageName = node.getPackageName();
          if (packageName != null) {
            String packageNameString = String.valueOf(packageName);
            if (packageNameString.length() > 0) {
              currentPackage = packageNameString;
            }
          }
          TreeScanner<Void, String> visitor = new TreeScanner<Void, String>() {
            @Override
            public Void visitClass(ClassTree node, String packagePrefix) {
              if (classDefinitions.get(currentPackage) == null) {
                classDefinitions.put(currentPackage, new HashMap<String, ClassTree>());
              }
              classDefinitions.get(currentPackage).put(packagePrefix + node.getSimpleName(), node);
              return super.visitClass(node, packagePrefix + node.getSimpleName() + ".");
            }
          };
          for (Tree decls : node.getTypeDecls()) {
            decls.accept(visitor, "");
          }
          return super.visitCompilationUnit(node, p);
        }
      }, null);
    }
  }

Call this to search for classes.

  /**
   * Lookup the definition of a class.
   * 
   * Lookup order: 1. Search in the current file: within the current class scope upwards to the
   * root. 2. Search laterally across files with the same package value for implicitly included
   * classes. 3. Check all import statements.
   * 
   * @param pack
   *          Current package ex "edu.illinois.crhc"
   * @param scope
   *          Current scope ex "Test.InnerClass"
   * @param identifier
   *          The partial class name to search for
   * @return ClassTree the definition of this class if found
   */
  ClassLookup lookupClass(CompilationUnitTree packTree, String scope, String identifier) {
    dumpClassTable();
    String pack = packTree.getPackageName().toString();
    System.out.println("Looking for class " + pack + " - " + scope + " - " + identifier);
    // Search nested scope and within same package
    HashMap<String, ClassTree> packClasses = classDefinitions.get(pack);
    if (packClasses != null) {
      String[] scopeWalk = scope.split("\\.");
      for (int i = scopeWalk.length; i >= 0; i--) {
        StringBuilder scopeTest = new StringBuilder();
        for (int j = 0; j < i; j++) {
          scopeTest.append(scopeWalk[j] + ".");
        }
        scopeTest.append(identifier);
        System.out.println("Testing scope " + pack + " - " + scopeTest.toString());
        if (packClasses.containsKey(scopeTest.toString())) {
          return new ClassLookup(packClasses.get(scopeTest.toString()), pack.replace(".", "/")
              + "/" + scopeTest.toString().replace(".", "$"));
        }
      }
    }
    /*
     * Check if fully-qualified identifier (foo.bar.Widget) is used. This needs to search all
     * combinations of package and class nesting.
     */
    StringBuilder packTest = new StringBuilder();
    String[] qualifiedName = identifier.split("\\.");
    for (int i = 0; i < qualifiedName.length - 1; i++) {
      packTest.append(qualifiedName[i]);
      if (i != qualifiedName.length - 2) {
        packTest.append(".");
      }
    }
    String clazz = qualifiedName[qualifiedName.length - 1];
    System.out.println("Testing absolute identifier: " + packTest.toString() + " " + clazz);
    if (classDefinitions.containsKey(packTest.toString())) {
      HashMap<String, ClassTree> foundPack = classDefinitions.get(packTest.toString());
      if (foundPack.containsKey(clazz)) {
        return new ClassLookup(foundPack.get(clazz), packTest.toString().replace(".", "/") + "/"
            + clazz.replace(".", "$"));
      }
    }

    /*
     * Search import statements. Last identifier segment must be class name. Search all of the
     * packages for the identifier by splitting off the class name. a.b.c.Tree Tree.Branch
     * Tree.Branch.Leaf
     */
    for (ImportTree imp : currentPackTree.getImports()) {
      pack = imp.getQualifiedIdentifier().toString();
      System.out.println(pack);
      String[] importName = pack.split("\\.");
      // Split off class name.
      // TODO: (edge case) no package
      StringBuilder importTest = new StringBuilder();
      for (int i = 0; i < importName.length - 1; i++) {
        importTest.append(importName[i]);
        if (i != importName.length - 2) {
          importTest.append(".");
        }
      }
      // See if the last import segment is * or matches the first segment of the identifier.

      System.out.println("Testing globally " + importTest.toString() + " - " + identifier);
      if (classDefinitions.containsKey(importTest.toString())) {
        HashMap<String, ClassTree> foundPack = classDefinitions.get(importTest.toString());
        String[] identifierParts = identifier.split(".");
        String importClass = importName[importName.length-1];
        if (importClass.equals("*") || identifierParts[0].equals(importClass)) {
          if (foundPack.containsKey(identifier)) {
            return new ClassLookup(foundPack.get(identifier), importTest.toString().replace(".", "/")
                + "/" + identifier.replace(".", "$"));
          }
        }
      }
    }

    return null;
  }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM