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

2022-09-04 20:33:20

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

详:

我有一个应用程序,它扫描外部应用程序,在检索时使用,我在读取文件时检查“扩展超类”,如果单词退出,我将类名添加到我的列表中,如下所示:nioFiles.walk()

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
        }

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

笔记:

  • 扫描超过60万个文件
  • 我不知道有多少个直接/间接的子类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我还注意到另一个问题是,当我检测到一个时,我得到的文件名可能是也可能不是子类名称,因为子类可能是同一文件中的或非公共的。subclasscurrentPath.getFileName()nestedclass


答案 1

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

因此,使用 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);
    }
}

作为奖励,或者为了创建一些测试用例,以下方法增加了为运行时类的源代码构建层次结构的功能:

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

答案 2

免责声明:如果您有多个具有相同名称的类,则此解决方案可能不起作用,因为它不考虑包名称。

我认为你可以通过跟踪类来查找,并使用while循环,直到列表中的所有值都被探索完毕。List

这里有一些代码,它创建了一个,key是类名,值是子类的列表。Map<String, List<String>>

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
    }

}