Найти прямые и косвенные подклассы путем сканирования файловой системы

У меня возникла проблема с написанием алгоритма, который поможет мне сканировать файловую систему и найти все подклассы определенного класса.

Подробности:

У меня есть приложение, которое сканирует внешнее приложение, используя 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 тысяч файлов
  • У меня нет идеи, сколько прямых/косвенных подклассов 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;

}

Любая помощь действительно оценена!

Edit 2 Я также заметил еще одну проблему: когда я обнаруживаю subclass я получаю имя файла currentPath.getFileName() которое может быть или не быть подклассовым именем, поскольку подкласс может быть nested или непубличным class в том же файле,

Ответы

Ответ 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

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Это решение может не работать, если у вас несколько классов с тем же именем, что и имена пакетов не учитываются.

Я думаю, вы можете сделать это с отслеживанием классов для поиска в List и использовать цикл while, пока не будут изучены все значения в списке.

Вот немного кода, который создает 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
    }

}