Найти прямые и косвенные подклассы путем сканирования файловой системы
У меня возникла проблема с написанием алгоритма, который поможет мне сканировать файловую систему и найти все подклассы определенного класса.
Подробности:
У меня есть приложение, которое сканирует внешнее приложение, используя 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
}
}