通过扫描文件系统查找直接和间接子类

时间:2018-04-18 10:01:41

标签: java algorithm recursion java-8 nio

我在编写算法时遇到问题,以帮助我扫描文件系统并找到某个类的所有子类。

详细信息:

我有一个使用nio Files.walk()扫描外部应用程序的应用程序,同时检索我检查“extends SuperClass”,如果单词退出则读取文件,我在列表中添加类名如下:

List<String> subclasses = new ArrayList<>();
Files.walk(appPath)
     .filter(p->Files.isRegularFile(p) && p.toString()
     .endsWith(".java")).forEach(path -> {
        try {
         List<String> lines = Files.readAllLines(path);
         Pattern pattern = Pattern.compile("\\bextends SuperClass\\b");
         Matcher matcher = pattern
                           .matcher(lines.stream()
                                 .collect(Collectors.joining(" ")));
         boolean isChild = matcher.find();
         if(isChild) subclasses.add(path.getFileName().toString());
        }catch (IOException e){
                //handle IOE
        }

上面的问题是它只获得SuperClass的直接子类,但我需要检索所有直接和间接的子类。 我想到了递归,因为我不知道有多少SuperClass的子类,但是我无法实现任何合理的实现。

备注:

  • 扫描超过600,000个文件
  • 我不知道有多少SuperClass的直接/间接子类
  • 我正在扫描的应用程序是外部的,我无法修改其代码,因此我只能通过阅读文件访问它并查看extends存在的位置
  • 如果问题的非递归解决方案很好,但如果没有别的方法,我会非常乐意接受递归的解决方案,因为我更关心解决方案而不是性能。

修改

我使用以下正则表达式来比较名称和导入,以确保即使在相同名称的情况下,不同的包输出也是正确的:

Pattern pattern = Pattern.compile("("+superClasss.getPackage()+")[\\s\\S]*(\\bextends "+superClass.getName()+"\\b)[\\s\\S]");

我也尝试过:

Pattern pattern = Pattern.compile("\\bextends "+superClass.getName()+"\\b");

但是也有一些缺少的子类,我相信代码会跳过一些检查,并且不能完全发挥作用:

public static List<SuperClass> getAllSubClasses(Path path, SuperClass parentClass) throws IOException{
classesToDo.add(baseClass);
while(classesToDo.size() > 0) {
    SuperClass superClass = classesToDo.remove(0);
    List<SuperClass> subclasses = getDirectSubClasses(parentPath,parentClass);
    if(subclasses.size() > 0)
        classes.addAll(subclasses);
    classesToDo.addAll(subclasses);
}
return classes;

}

真的很感激任何帮助!

编辑2 我还注意到另一个问题,就是当我检测到subclass时,我得到的文件名currentPath.getFileName()可能是也可能不是子类名,因为子类可能是nested或非公共class在同一个文件中。

2 个答案:

答案 0 :(得分:13)

我强烈建议解析编译的类文件而不是源代码。由于这些类文件已经过优化以供机器处理,因此消除了源代码文件处理的许多复杂性和极端情况。

因此,使用ASM库构建完整的类层次结构树的解决方案如下所示:

public static Map<String, Set<String>> getClassHierarchy(Path root) throws IOException {
    return Files.walk(root)
         .filter(p->Files.isRegularFile(p) && isClass(p.getFileName().toString()))
         .map(p -> getClassAndSuper(p))
         .collect(Collectors.groupingBy(Map.Entry::getValue,
                Collectors.mapping(Map.Entry::getKey, Collectors.toSet())));
}
private static boolean isClass(String fName) {
    // skip package-info and module-info
    return fName.endsWith(".class") && !fName.endsWith("-info.class");
}
private static Map.Entry<String,String> getClassAndSuper(Path p) {
    final class CV extends ClassVisitor {
        Map.Entry<String,String> result;
        public CV() {
            super(Opcodes.ASM5);
        }
        @Override
        public void visit(int version, int access,
                String name, String signature, String superName, String[] interfaces) {
            result = new AbstractMap.SimpleImmutableEntry<>(
                Type.getObjectType(name).getClassName(),
                superName!=null? Type.getObjectType(superName).getClassName(): "");
        }
    }
    try {
        final CV visitor = new CV();
        new ClassReader(Files.readAllBytes(p)).accept(visitor, ClassReader.SKIP_CODE);
        return visitor.result;
    } catch (IOException ex) {
        throw new UncheckedIOException(ex);
    }
}

作为奖励,resp。为了创建一些测试用例,以下方法增加了为运行时类'source:

构建层次结构的能力
public static Map<String, Set<String>> getClassHierarchy(Class<?> context)
                                        throws IOException, URISyntaxException {
    Path p;
    URI clURI = context.getResource(context.getSimpleName()+".class").toURI();
    if(clURI.getScheme().equals("jrt")) p = Paths.get(URI.create("jrt:/modules"));
    else {
        if(!clURI.getScheme().equals("file")) try {
            FileSystems.getFileSystem(clURI);
        } catch(FileSystemNotFoundException ex) {
            FileSystems.newFileSystem(clURI, Collections.emptyMap());
        }
        String qn = context.getName();
        p = Paths.get(clURI).getParent();
        for(int ix = qn.indexOf('.'); ix>0; ix = qn.indexOf('.', ix+1)) p = p.getParent();
    }
    return getClassHierarchy(p);
}

然后,你可以做

Map<String, Set<String>> hierarchy = getClassHierarchy(Number.class);
System.out.println("Direct subclasses of "+Number.class);
hierarchy.getOrDefault("java.lang.Number", Collections.emptySet())
         .forEach(System.out::println);

并获取

Direct subclasses of class java.lang.Number
java.lang.Float
java.math.BigDecimal
java.util.concurrent.atomic.AtomicLong
java.lang.Double
java.lang.Long
java.util.concurrent.atomic.AtomicInteger
java.lang.Short
java.math.BigInteger
java.lang.Byte
java.util.concurrent.atomic.Striped64
java.lang.Integer

Map<String, Set<String>> hierarchy = getClassHierarchy(Number.class);
System.out.println("All subclasses of "+Number.class);
printAllClasses(hierarchy, "java.lang.Number", "  ");
private static void printAllClasses(
        Map<String, Set<String>> hierarchy, String parent, String i) {
    hierarchy.getOrDefault(parent, Collections.emptySet())
        .forEach(x -> {
            System.out.println(i+x);
            printAllClasses(hierarchy, x, i+"  ");
    });
}

获取

All subclasses of class java.lang.Number
  java.lang.Float
  java.math.BigDecimal
  java.util.concurrent.atomic.AtomicLong
  java.lang.Double
  java.lang.Long
  java.util.concurrent.atomic.AtomicInteger
  java.lang.Short
  java.math.BigInteger
  java.lang.Byte
  java.util.concurrent.atomic.Striped64
    java.util.concurrent.atomic.LongAdder
    java.util.concurrent.atomic.LongAccumulator
    java.util.concurrent.atomic.DoubleAdder
    java.util.concurrent.atomic.DoubleAccumulator
  java.lang.Integer

答案 1 :(得分:3)

免责声明:如果您有多个具有相同名称的类,并且未考虑包名称,则此解决方案可能无效。

我认为您可以通过跟踪要在List中查找的类来执行此操作,并使用while循环直到已经探索了列表中的所有值。

以下是一些创建Map<String, List<String>>的代码,key是类名,value是子类列表。

public class Test {

    private static Path appPath = //your path

    private static Map<String, List<String>> classes = new HashMap<>();
    private static List<String> classesToDo = new ArrayList<>();

    public static void main(String[] args) throws IOException {

        classesToDo.add("AnswerValueValidatorBase");

        while(classesToDo.size() > 0) {
            String className = classesToDo.remove(0);
            List<String> subclasses = getDirectSubclasses(className);
            if(subclasses.size() > 0)
                classes.put(className, subclasses);
            classesToDo.addAll(subclasses);
        }

        System.out.println(classes);
    }

    private static List<String> getDirectSubclasses(String className) throws IOException {
        List<String> subclasses = new ArrayList<>();
        Files.walk(appPath)
             .filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".java"))
             .forEach(path -> {
                 try {
                      List<String> lines = Files.readAllLines(path);
                      Pattern pattern = Pattern.compile("\\bextends "+className+"\\b");
                      Matcher matcher = pattern.matcher(lines.stream().collect(Collectors.joining(" ")));
                      boolean isChild = matcher.find();
                      if(isChild) {
                          String fileName = path.getFileName().toString();
                          String clazzName = fileName.substring(0, fileName.lastIndexOf("."));
                          subclasses.add(clazzName);
                      }
                  } catch(IOException e) {
                  //handle IOE
                  }
        });

        return subclasses;
    }
}

在我的项目上运行它会返回看起来正确的内容

{
    AnswerValueValidatorBase=[SingleNumericValidator, DefaultValidator, RatingValidator, ArrayValidatorBase, DocumentValidator],
    ArrayValidatorBase=[MultiNumericValidator, StringArrayValidator, IntegerArrayValidator, MultiCheckboxValidator], 
    DefaultValidator=[IntegerValidator, DateValidator, StringValidator, CountryValidator, PercentageValidator], 
    IntegerArrayValidator=[MultiPercentageValidator, RankValidator, MultiDropValidator, MultiRadioValidator, CheckboxValidator], 
    SingleNumericValidator=[SliderValidator], 
    MultiNumericValidator=[MultiSliderValidator], 
    StringArrayValidator=[MultiTextValidator, ChecklistValidator]
}

编辑

这样做的递归方式是

public class Test {

    private static Path appPath = // your path 

    public static void main(String[] args) throws IOException {

        List<String> classesToDo = new ArrayList<>();
        classesToDo.add("AnswerValueValidatorBase");

        Map<String, List<String>> classesMap = getSubclasses(new HashMap<>(), classesToDo);

        System.out.println(classesMap);
    }

    private static Map<String, List<String>> getSubclasses(Map<String, List<String>> classesMap, List<String> classesToDo) throws IOException {
        if(classesToDo.size() == 0) {
            return classesMap;
        } else {
            String className = classesToDo.remove(0);
            List<String> subclasses = getDirectSubclasses(className);
            if(subclasses.size() > 0)
                classesMap.put(className, subclasses);
            classesToDo.addAll(subclasses);
            return getSubclasses(classesMap, classesToDo);
        }
    }

    private static List<String> getDirectSubclasses(String className) throws IOException {
        // same as above
    }

}