Русский
Русский
English
Статистика
Реклама

Перевод Обнаружение и удаление кода без ссылок с помощью ArchUnit

Когда вы поддерживаете большой Java-проект в течение длительного периода, моменты, когда вы, наконец, можете удалить неиспользуемый код, могут быть очень приятными.Больше не нужно поддерживать и переносить версии библиотеки или поддерживать темные закоулки кода, который больше не используется.Но выяснение того, какие части кодовой базы можно удалить, может быть проблемой, и инструменты в этой области, похоже, отстают от новых практик разработки на Java.В этом посте мы опишем подход к поиску кода без ссылок с помощью ArchUnit, который позволяет итеративно обнаруживать и удалять неиспользуемый код из ваших проектов Java.

ArchUnit

ArchUnit- это бесплатная, простая и расширяемая библиотека для проверки архитектуры вашего Java-кода с использованием любой простой среды модульного тестирования Java.То есть ArchUnit может проверять зависимости между пакетами и классами, слоями и срезами, проверять циклические зависимости и многое другое.Это делается путем анализа заданного байт-кода Java и импорта всех классов в структуру Java кода.

- Сайт Archunit

Сам ArchUnit уже рассматривался ранее Нильсом, который подчеркивал его полезность для контроля архитектурных соглашений в проекте.

В этом посте мы расширим пользовательские правила, кратко упомянутые ранее, а также используем новый стиль тестирования ArchUnit с JUnit5 и лямбда-выражениями.Мы будем использовать граф зависимостей между классами и методами, создаваемый ArchUnit, для обнаружения классов и методов, на которые нет ссылок и которые являются кандидатами на удаление.Мы также рассмотрим, как общие, так и конкретные способы предотвращения искажения результатов ложными срабатываниями.

Репозиторий GitHub и структура тестов

Кэтому посту прилагается репозиторий GitHub.Он содержитминимальное приложение Spring Boot, а такжеправила ArchUnit для поиска классов и методов, на которые нетссылок.

На момент написаниямы использовали ArchUnit 0.15.0.В частности, мы используемподдержку JUnit5черезcom.tngtech.archunit:archunit-junit5:0.15.0.Это приводит к базовой структуре теста:

@AnalyzeClasses(  packagesOf = ArchunitUnusedRuleApplication.class,  importOptions = {    ImportOption.DoNotIncludeArchives.class,    ImportOption.DoNotIncludeJars.class,    ImportOption.DoNotIncludeTests.class})class ArchunitUnusedRuleApplicationTests {  @ArchTest  static ArchRule classesShouldNotBeUnused = classes()    .that()      ...    .should(      ...    );  @ArchTest  static ArchRule methodsShouldNotBeUnused = methods()    .that()      ...    .should(      ...    );}

Обратите внимание, как мы ограничиваем классы, анализируемые с помощью аргументов packagesOf иimportOptionsв аннотации @AnalyzeClasses().Кроме того, использование аннотации @ArchTest встатическом поле типа ArchRule избавляет нас от необходимости вызывать ArchRule.check(JavaClasses),как показано в предыдущем посте в блоге.

Спомощью селекторов classes() и methods() из ArchRuleDefinition выбираем элементы, которыемы хотим проверить с помощью нашего правила. Обычно эти элементы затем дополнительно ограничиваются последовательностью вызовов после.that(), чтобы отсеять потенциально ложные срабатывания. Наконец, с помощью.should()мы проверяем, что все оставшиеся классы удовлетворяют заданному условию, и при обнаружении каких-либо классов вызываем исключение.

Обнаружение неиспользуемых методов

Имея указанную выше базовую структуру, мы можем приступить к определению нашего первого правила для обнаружения неиспользуемых методов.Как мы уже говорили ранее, ArchUnit строит граф зависимостей между блоками кода Java, который мы можем использовать для поиска блоков кода, на которые никогда не ссылаются другие блоки кода Java.

Конечно, в типичном приложении Spring Boot есть причины, по которым метод никогда не вызывается напрямую, в частности, когда аннотируется конечная точка сети, прослушиватель сообщений или обработчик команд, событий или исключений.В этих случаях фреймворк будет вызывать методы за вас, поэтому вы не хотите, чтобы они случайно помечались как неиспользуемые в вашем правиле тестирования.То же самое касается общих методов, добавляемых, в частности, при использовании Lombok's@Dataor@Value, которые добавляютequals,hashCodeиtoStringв классы.

Собирая все эти ограничения вместе, мы приходим к следующему правилу ArchRule, чтобы найти неиспользуемые методы.

@ArchTeststatic ArchRule methodsShouldNotBeUnused = methods()  .that().doNotHaveName("equals")  .and().doNotHaveName("hashCode")  .and().doNotHaveName("toString")  .and().doNotHaveName("main")  .and().areNotMetaAnnotatedWith(RequestMapping.class)  .and(not(methodHasAnnotationThatEndsWith("Handler")    .or(methodHasAnnotationThatEndsWith("Listener"))    .or(methodHasAnnotationThatEndsWith("Scheduled"))    .and(declaredIn(describe("component", clazz -> clazz.isMetaAnnotatedWith(Component.class))))))  .should(new ArchCondition<>("not be unreferenced") {    @Override    public void check(JavaMethod javaMethod, ConditionEvents events) {      Set<JavaMethodCall> accesses = new HashSet<>(javaMethod.getAccessesToSelf());      accesses.removeAll(javaMethod.getAccessesFromSelf());      if (accesses.isEmpty()) {        events.add(new SimpleConditionEvent(javaMethod, false, String.format("%s is unreferenced in %s",          javaMethod.getDescription(), javaMethod.getSourceCodeLocation())));      }    }  });static DescribedPredicate<JavaMethod> methodHasAnnotationThatEndsWith(String suffix) {  return describe(String.format("has annotation that ends with '%s'", suffix),   method -> method.getAnnotations().stream()     .anyMatch(annotation -> annotation.getRawType().getFullName().endsWith(suffix)));}

Обнаружение неиспользуемых классов

Чтобы обнаружить целые классы, на которые нет ссылок из других классов, мы можем применить тот же подход с несколькими незначительными изменениями.Мы также не хотели бы ошибочно идентифицировать любой@Componentобъект, содержащий конечную точку, прослушиватель (listener) или обработчик, поэтому нам нужен еще один настраиваемый предикат.В нашей проверке состояния мы также контролируем обнаружение в JavaClass.getDirectDependenciesToSelf() каких-либо зависимостей, чтобы отсеять еще один источник ложных срабатываний.

В конце концов, мы получаем следующее правило ArchRule для поиска неиспользуемых классов.

@ArchTeststatic ArchRule classesShouldNotBeUnused = classes()  .that().areNotMetaAnnotatedWith(org.springframework.context.annotation.Configuration.class)  .and().areNotMetaAnnotatedWith(org.springframework.stereotype.Controller.class)  .and(not(classHasMethodWithAnnotationThatEndsWith("Handler")    .or(classHasMethodWithAnnotationThatEndsWith("Listener"))    .or(classHasMethodWithAnnotationThatEndsWith("Scheduled"))    .and(metaAnnotatedWith(Component.class))))  .should(new ArchCondition<>("not be unreferenced") {    @Override    public void check(JavaClass javaClass, ConditionEvents events) {      Set<JavaAccess<?>> accesses = new HashSet<>(javaClass.getAccessesToSelf());      accesses.removeAll(javaClass.getAccessesFromSelf());      if (accesses.isEmpty() && javaClass.getDirectDependenciesToSelf().isEmpty()) {        events.add(new SimpleConditionEvent(javaClass, false, String.format("%s is unreferenced in %s",          javaClass.getDescription(), javaClass.getSourceCodeLocation())));      }    }  });static DescribedPredicate<JavaClass> classHasMethodWithAnnotationThatEndsWith(String suffix) {  return describe(String.format("has method with annotation that ends with '%s'", suffix),    clazz -> clazz.getMethods().stream()      .flatMap(method -> method.getAnnotations().stream())      .anyMatch(annotation -> annotation.getRawType().getFullName().endsWith(suffix)));}

Ограничения

Хотя приведенные выше правила являются отличной отправной точкой для выявления потенциально неиспользуемого кода, к сожалению, именно здесь мы также начнем сталкиваться с некоторыми (текущими) ограничениями ArchUnit.В зависимости от того, как настроен ваш проект, вы можете обнаружить, чтоссылка на метод не рассматривается как зависимость.Или вы можете обнаружить,чтоаргументы шаблонного типане определены как зависимости.И, поскольку ArchUnit работает с байтовым кодом, вы можете обнаружить, чтостроковые константы установлены во время компиляции.

Замораживание ложных (или истинных!) срабатываний

К счастью, есть элегантный способ обработки ложных срабатываний в отношении наших пользовательских условий ArchConditions:Freezing Arch Rules.Передав наше правило ArchRule, в FreezingArchRule.freeze(ArchRule) мы можем записать все текущие нарушения и остановить добавление новых нарушений.

Когда правила вводятся в готовые проекты, часто возникают сотни или даже тысячи нарушений, и их слишком много, чтобы исправить их немедленно.Единственный способ справиться с такими обширными нарушениями - это использовать итеративный подход, который предотвращает дальнейшее ухудшение кодовой базы.FreezingArchRule может помочь в таких сценариях, записывая все существующие нарушения в хранилище ViolationStore.Последовательные запуски будут сообщать только о новых нарушениях и игнорировать известные нарушения.Если нарушения исправлены, FreezingArchRule автоматически удалит сохраненные известные нарушения, чтобы предотвратить регресс.

- Сайт Archunit

Если вы заметили какие-либо общие закономерности в нарушениях, конечно, предпочтительнее исключить такие классы из анализа с помощью предиката .that().Однако для конкретных нарушений замораживание - отличный способ подтвердить их существование в кодовой базе, не загрязняя общее правило.

Протестируйте сами правила ArchUnit

Наконец, вы хотите убедиться, что правила, которые вы создаете, действительно обнаруживают нарушения, когда они заданы.Для этого вы можете настроить модульные тесты, которые импортируют классы, специально созданные с имеющимися нарушениями, и подтверждают, что нарушение обнаруживается.Этот шаг, конечно, необязателен, но рекомендуется, особенно при совместном использовании правил в нескольких проектах.Примерный тест может выглядеть так.

@Nestedclass VerifyRulesThemselves {  @Test  void verifyClassesShouldNotBeUnused() {     JavaClasses javaClasses = new ClassFileImporter()       .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_ARCHIVES)       .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)       .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)       .importPackagesOf(ArchunitUnusedRuleApplication.class);     AssertionError error = assertThrows(AssertionError.class,       () -> classesShouldNotBeUnused.check(javaClasses));     assertEquals("""       Architecture Violation [Priority: MEDIUM] - Rule 'classes that are not meta-annotated with @Configuration and are not meta-annotated with @Controller and not has method with annotation that ends with 'Handler' or has method with annotation that ends with 'Listener' or has method with annotation that ends with 'Scheduled' and meta-annotated with @Component should not be unreferenced' was violated (3 times):       Class <com.github.timtebeek.archunit.ComponentD> is unreferenced in (ArchunitUnusedRuleApplication.java:0)       Class <com.github.timtebeek.archunit.ModelF> is unreferenced in (ArchunitUnusedRuleApplication.java:0)       Class <com.github.timtebeek.archunit.PathsE> is unreferenced in (ArchunitUnusedRuleApplication.java:0)""",       error.getMessage());  }  @Test  void verifyMethodsShouldNotBeUnused() {    JavaClasses javaClasses = new ClassFileImporter()      .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_ARCHIVES)      .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)      .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)      .importPackagesOf(ArchunitUnusedRuleApplication.class);    AssertionError error = assertThrows(AssertionError.class,      () -> methodsShouldNotBeUnused.check(javaClasses));    assertEquals("""      Architecture Violation [Priority: MEDIUM] - Rule 'methods that do not have name 'equals' and do not have name 'hashCode' and do not have name 'toString' and do not have name 'main' and are not meta-annotated with @RequestMapping and not has annotation that ends with 'Handler' or has annotation that ends with 'Listener' or has annotation that ends with 'Scheduled' and declared in component should not be unreferenced' was violated (2 times):      Method <com.github.timtebeek.archunit.ComponentD.doSomething(com.github.timtebeek.archunit.ModelD)> is unreferenced in (ArchunitUnusedRuleApplication.java:102)      Method <com.github.timtebeek.archunit.ModelF.toUpper()> is unreferenced in (ArchunitUnusedRuleApplication.java:143)""",      error.getMessage());  }}

Заключение

Используя приведенные выше правила, вы можете быть уверены, что новые изменения кода случайно не оставят без ссылки ни новый ни старый код.Любые изменения того, что было ранее или на которое сейчас нет ссылок, будут сохраняться в замороженном хранилище прямо внутри репозитория.Вместе эти правила помогут сохранить вашу кодовую базу не больше, чем она должна быть, позволяя вам сосредоточиться на том, что на самом деле используется.И вы начнете итеративно обнаруживать и удалять неиспользуемый код и смотреть, что появляется дальше, когда удаленные конечные точки, методы и классы больше не ссылаются на соответствующие зависимости.

Источник: habr.com
К списку статей
Опубликовано: 05.04.2021 12:20:40
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Java

Testing

Тестирование

Поддержка

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru