задний план
Недавно, когда я работал над новым проектом, было введено архитектурное требование, то есть необходимо было проверить спецификацию кодирования, спецификацию классификации модулей, спецификацию зависимостей классов и т. д. проекта.
Много раз мы разрабатываем спецификации для таких проектов, как:
- Обязательно в структуре пакета проекта
service
на слои нельзя ссылатьсяcontroller
Класс слоя (этот пример немного экстремальный). - Жесткие правила определены в
controller
в упаковкеController
Имя класса класса заканчивается на «Контроллер», имя входного параметра метода заканчивается на «Запрос», а имя возвращаемого параметра заканчивается на «Ответ». - типы enum должны быть помещены в
common.constant
Под пакетом он заканчивается именем класса Enum.
Есть много других спецификаций, которые, возможно, потребуется настроить, что в конечном итоге может привести к созданию документа. Однако кто может гарантировать, что все люди, разрабатывающие параметры, будут развиваться в соответствии со спецификациями документа? Для того, чтобы обеспечить реализацию спецификации,Archunit
В форме модульных тестов, просканировав все классы в пакете classpath (даже Jar), закодируйте каждую спецификацию в виде модульных тестов.Если в коде проекта есть нарушение соответствующей спецификации модульного теста, модульный тест будет not pass. , чтобы вы могли полностью контролировать структуру проекта и спецификации кодирования с уровня CI/CD. Дата написания этой статьи2019-02-16
,тогдаArchunit
Последняя версия0.9.3
,использоватьJDK 8
.
Введение
Archunit— бесплатная, простая и расширяемая библиотека классов для изучения архитектуры кода Java. Предоставляет другие функции, такие как проверка зависимостей пакетов и классов, вызов зависимостей иерархии и аспектов, а также циклическая проверка зависимостей. Он работает путем импорта структуры кода всех классов на основеJava
Анализ байт-кода достигает этого.Основное внимание Archunit направлено на автоматическое тестирование архитектуры кода и правил кодирования с использованием любой распространенной среды модульного тестирования Java..
импортировать зависимости
Вообще говоря, обычно используемые тестовые фреймворки:Junit4
, нужно представитьJunit4
иArchunit
:
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
<version>0.9.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
зависимости проектаslf4j
, поэтому лучше ввести тестовую зависимостьslf4j
реализация, напримерlogback
:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>test</scope>
</dependency>
как пользоваться
В основном из следующих двух аспектов, чтобы ввести использование:
- Укажите параметры для сканирования классов.
- Встроенные определения правил.
Укажите параметры для сканирования классов
Предпосылкой оценки кода или правил зависимостей является импорт всех классов, которые необходимо проанализировать.Импорт сканирования класса зависит отClassFileImporter
, нижний уровень опирается на структуру байт-кода ASM для анализа байт-кода файла класса, и производительность будет намного выше, чем структура сканирования классов на основе отражения.ClassFileImporter
Необязательные параметры конструктора:ImportOption(s)
, правило сканирования может пройтиImportOption
Реализация интерфейса, дополнительные правила, предоставляемые по умолчанию:
// 不包含测试类
ImportOption.Predefined.DONT_INCLUDE_TESTS
// 不包含Jar包里面的类
ImportOption.Predefined.DONT_INCLUDE_JARS
// 不包含Jar和Jrt包里面的类,JDK9的特性
ImportOption.Predefined.DONT_INCLUDE_ARCHIVES
Например, мы реализуем пользовательскийImportOption
Реализация для указания путей пакетов, которые необходимо исключить из сканирования:
public class DontIncludePackagesImportOption implements ImportOption {
private final Set<Pattern> EXCLUDED_PATTERN;
public DontIncludePackagesImportOption(String... packages) {
EXCLUDED_PATTERN = new HashSet<>(8);
for (String eachPackage : packages) {
EXCLUDED_PATTERN.add(Pattern.compile(String.format(".*/%s/.*", eachPackage.replace("/", "."))));
}
}
@Override
public boolean includes(Location location) {
for (Pattern pattern : EXCLUDED_PATTERN) {
if (location.matches(pattern)) {
return false;
}
}
return true;
}
}
ImportOption
Интерфейс имеет только один метод:
boolean includes(Location location)
в,Location
Содержит метаданные для оценки таких атрибутов, как информация о пути и является ли файл JAR, что удобно для использования регулярных выражений или прямой логической оценки.
Тогда мы можем достичь вышеуказанногоDontIncludePackagesImportOption
строитьClassFileImporter
Пример:
ImportOptions importOptions = new ImportOptions()
// 不扫描jar包
.with(ImportOption.Predefined.DONT_INCLUDE_JARS)
// 排除不扫描的包
.with(new DontIncludePackagesImportOption("com.sample..support"));
ClassFileImporter classFileImporter = new ClassFileImporter(importOptions);
получитьClassFileImporter
После инстанса мы можем импортировать класс в проект через соответствующий метод:
// 指定类型导入单个类
public JavaClass importClass(Class<?> clazz)
// 指定类型导入多个类
public JavaClasses importClasses(Class<?>... classes)
public JavaClasses importClasses(Collection<Class<?>> classes)
// 通过指定路径导入类
public JavaClasses importUrl(URL url)
public JavaClasses importUrls(Collection<URL> urls)
public JavaClasses importLocations(Collection<Location> locations)
// 通过类路径导入类
public JavaClasses importClasspath()
public JavaClasses importClasspath(ImportOptions options)
// 通过文件路径导入类
public JavaClasses importPath(String path)
public JavaClasses importPath(Path path)
public JavaClasses importPaths(String... paths)
public JavaClasses importPaths(Path... paths)
public JavaClasses importPaths(Collection<Path> paths)
// 通过Jar文件对象导入类
public JavaClasses importJar(JarFile jar)
public JavaClasses importJars(JarFile... jarFiles)
public JavaClasses importJars(Iterable<JarFile> jarFiles)
// 通过包路径导入类 - 这个是比较常用的方法
public JavaClasses importPackages(Collection<String> packages)
public JavaClasses importPackages(String... packages)
public JavaClasses importPackagesOf(Class<?>... classes)
public JavaClasses importPackagesOf(Collection<Class<?>> classes)
Метод импорта класса предоставляет многомерные параметры, что очень удобно в использовании. Например, вы хотите импортироватьcom.sample
Все классы ниже пакета, просто нужно это:
public class ClassFileImporterTest {
@Test
public void testImportBootstarpClass() throws Exception {
ImportOptions importOptions = new ImportOptions()
// 不扫描jar包
.with(ImportOption.Predefined.DONT_INCLUDE_JARS)
// 排除不扫描的包
.with(new DontIncludePackagesImportOption("com.sample..support"));
ClassFileImporter classFileImporter = new ClassFileImporter(importOptions);
long start = System.currentTimeMillis();
JavaClasses javaClasses = classFileImporter.importPackages("com.sample");
long end = System.currentTimeMillis();
System.out.println(String.format("Found %d classes,cost %d ms", javaClasses.size(), end - start));
}
}
принадлежитJavaClasses
даJavaClass
Коллекция , которую можно просто сравнить с отражениемClass
Набор правил кода и правил зависимостей, используемых позже, сильно зависит отJavaClasses
илиJavaClass
.
Встроенные определения правил
После сканирования и импорта классов нам нужно определить правила проверки, а затем применить их ко всем импортированным классам, чтобы мы могли завершить фильтрацию всех классов — или применить правила ко всем классам и сделать утверждения.
Определение правила зависит отArchRuleDefinition
класс, созданные правилаArchRule
Например, обычно используется процесс создания экземпляра правила.ArchRuleDefinition
Методы потока класса, эти методы потока определены в соответствии с логикой человеческого мышления, и начать работу относительно просто, например:
ArchRule archRule = ArchRuleDefinition.noClasses()
// 在service包下的所有类
.that().resideInAPackage("..service..")
// 不能调用controller包下的任意类
.should().accessClassesThat().resideInAPackage("..controller..")
// 断言描述 - 不满足规则的时候打印出来的原因
.because("不能在service包中调用controller中的类");
// 对所有的JavaClasses进行判断
archRule.check(classes);
Выше показан пользовательский новыйArchRule
например, некоторые часто используемые были встроены для насArchRule
реализации, они находятся вGeneralCodingRules
середина:
- NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS: невозможно вызвать System.out, System.err или (Exception.) printStackTrace.
- NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS: классы не могут напрямую создавать общие исключения Throwable, Exception или RuntimeException.
- NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING: нельзя использовать
java.util.logging
Компонент журнала по пути к пакету.
более встроенныйArchRule
Или используйте общие встроенные правила, вы можете обратиться кофициальный пример.
Базовый пример использования
Базовый пример использования в основном проверяет все классы проекта по некоторым общим спецификациям кодирования или правилам написания спецификаций проекта.
проверка зависимости пакета
ArchRule archRule = ArchRuleDefinition.noClasses()
.that().resideInAPackage("..com.source..")
.should().dependOnClassesThat().resideInAPackage("..com.target..");
ArchRule archRule = ArchRuleDefinition.classes()
.that().resideInAPackage("..com.foo..")
.should().onlyAccessClassesThat().resideInAnyPackage("..com.source..", "..com.foo..");
проверка зависимости класса
ArchRule archRule = ArchRuleDefinition.classes()
.that().haveNameMatching(".*Bar")
.should().onlyBeAccessed().byClassesThat().haveSimpleName("Bar");
Класс включен в проверку отношения пакета
ArchRule archRule = ArchRuleDefinition.classes()
.that().haveSimpleNameStartingWith("Foo")
.should().resideInAPackage("com.foo");
Проверка наследственных отношений
ArchRule archRule = ArchRuleDefinition.classes()
.that().implement(Collection.class)
.should().haveSimpleNameEndingWith("Connection");
ArchRule archRule = ArchRuleDefinition.classes()
.that().areAssignableTo(EntityManager.class)
.should().onlyBeAccessed().byAnyPackage("..persistence..");
Проверка аннотации
ArchRule archRule = ArchRuleDefinition.classes()
.that().areAssignableTo(EntityManager.class)
.should().onlyBeAccessed().byClassesThat().areAnnotatedWith(Transactional.class)
Проверка взаимосвязи вызовов логического уровня
Например, структура проекта выглядит следующим образом:
- com.myapp.controller
SomeControllerOne.class
SomeControllerTwo.class
- com.myapp.service
SomeServiceOne.class
SomeServiceTwo.class
- com.myapp.persistence
SomePersistenceManager
Например, мы оговариваем:
- путь к пакету
com.myapp.controller
На классы в не могут ссылаться другие иерархические пакеты. - путь к пакету
com.myapp.service
Занятия могут быть толькоcom.myapp.controller
ссылка на класс в . - путь к пакету
com.myapp.persistence
Занятия могут быть толькоcom.myapp.service
ссылка на класс в .
Правила написания следующие:
layeredArchitecture()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")
Циклическая проверка зависимостей
Например, структура проекта выглядит следующим образом:
- com.myapp.moduleone
ClassOneInModuleOne.class
ClassTwoInModuleOne.class
- com.myapp.moduletwo
ClassOneInModuleTwo.class
ClassTwoInModuleTwo.class
- com.myapp.modulethree
ClassOneInModuleThree.class
ClassTwoInModuleThree.class
Например, мы оговариваем:com.myapp.moduleone
,com.myapp.moduletwo
иcom.myapp.modulethree
Классы в трех путях пакетов не могут образовывать циклический буфер зависимостей, например:
ClassOneInModuleOne -> ClassOneInModuleTwo -> ClassOneInModuleThree -> ClassOneInModuleOne
Правила написания следующие:
slices().matching("com.myapp.(*)..").should().beFreeOfCycles()
Основной API
API разделен на три уровня, наиболее важными из которых являются уровень «Основной», уровень «Язык» и уровень «Библиотека».
API основного уровня
API уровня ядра ArchUnit в основном похож на собственный API отражения Java, напримерJavaMethod
иJavaField
соответствует в родном отраженииMethod
иField
, которые предоставляют такие вещи, какgetName()
,getMethods()
,getType()
иgetParameters()
и другие методы.
Кроме того, ArchUnit расширяет некоторые API для описания отношений между зависимыми кодами, такими какJavaMethodCall
,JavaConstructorCall
илиJavaFieldAccess
. Он также предоставляет API-интерфейсы, такие как импорт отношений доступа между классами Java и другими классами Java, такими какJavaClass#getAccessesFromSelf()
.
Вместо этого вам нужно импортировать скомпилированные классы Java в путь к классам или в пакет Jar.ClassFileImporter
Выполните эту функцию:
JavaClasses classes = new ClassFileImporter().importPackages("com.mycompany.myapp");
API уровня Ланга
API уровня Core очень мощный и предоставляет необходимую информацию о статической структуре Java-программы, но прямому использованию API уровня Core будет не хватать выразительности для модульного тестирования, особенно с точки зрения архитектурных правил.
По этой причине ArchUnit предоставляет API уровня Lang, который предоставляет мощный синтаксис для абстрактного выражения правил. Большинство API уровня Lang используют потоковое программирование для определения методов.Например, правила для указания определений пакетов и взаимосвязей вызовов следующие:
ArchRule rule =
classes()
// 定义在service包下的所欲类
.that().resideInAPackage("..service..")
// 只能被controller包或者service包中的类访问
.should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");
После написания правил можно сканировать на основе импорта всех скомпилированных классов:
JavaClasses classes = new ClassFileImporter().importPackage("com.myapp");
ArchRule rule = // 定义的规则
rule.check(classes);
API слоя библиотеки
API уровня библиотеки предоставляет более сложные и мощные предопределенные правила с помощью статических фабричных методов. Класс входа:
com.tngtech.archunit.library.Architectures
В настоящее время это обеспечивает удобную проверку только для многоуровневых архитектур, но в будущем может быть расширено до таких стилей, как шестиугольная архитектура\конвейеры и фильтры, разделение бизнес-логики и технической инфраструктуры.
Есть несколько других относительно мощных функций:
- Функция среза кода, запись
com.tngtech.archunit.library.dependencies.SlicesRuleDefinition
. - Общие правила кодирования, запись
com.tngtech.archunit.library.GeneralCodingRules
. -
PlantUML
Поддержка компонентов, функции находятся в пути к пакетуcom.tngtech.archunit.library.plantuml
Вниз.
Пишите сложные правила
Вообще говоря, встроенные правила могут не соответствовать некоторым сложным правилам проверки спецификации, поэтому необходимо написать собственные правила. Вот лишь одно из относительно сложных правил, упомянутых ранее:
- определено в
controller
в упаковкеController
Имя класса класса заканчивается на «Контроллер», имя входного параметра метода заканчивается на «Запрос», а имя возвращаемого параметра заканчивается на «Ответ».
Примеры официально предоставленных пользовательских правил:
DescribedPredicate<JavaClass> haveAFieldAnnotatedWithPayload =
new DescribedPredicate<JavaClass>("have a field annotated with @Payload"){
@Override
public boolean apply(JavaClass input) {
boolean someFieldAnnotatedWithPayload = // iterate fields and check for @Payload
return someFieldAnnotatedWithPayload;
}
};
ArchCondition<JavaClass> onlyBeAccessedBySecuredMethods =
new ArchCondition<JavaClass>("only be accessed by @Secured methods") {
@Override
public void check(JavaClass item, ConditionEvents events) {
for (JavaMethodCall call : item.getMethodCallsToSelf()) {
if (!call.getOrigin().isAnnotatedWith(Secured.class)) {
String message = String.format(
"Method %s is not @Secured", call.getOrigin().getFullName());
events.add(SimpleConditionEvent.violated(call, message));
}
}
}
};
classes().that(haveAFieldAnnotatedWithPayload).should(onlyBeAccessedBySecuredMethods);
Нам просто нужно сымитировать его реализацию следующим образом:
public class ArchunitTest {
@Test
public void controller_class_rule() {
JavaClasses classes = new ClassFileImporter().importPackages("club.throwable");
DescribedPredicate<JavaClass> predicate =
new DescribedPredicate<JavaClass>("定义在club.throwable.controller包下的所有类") {
@Override
public boolean apply(JavaClass input) {
return null != input.getPackageName() && input.getPackageName().contains("club.throwable.controller");
}
};
ArchCondition<JavaClass> condition1 = new ArchCondition<JavaClass>("类名称以Controller结尾") {
@Override
public void check(JavaClass javaClass, ConditionEvents conditionEvents) {
String name = javaClass.getName();
if (!name.endsWith("Controller")) {
conditionEvents.add(SimpleConditionEvent.violated(javaClass, String.format("当前控制器类[%s]命名不以\"Controller\"结尾", name)));
}
}
};
ArchCondition<JavaClass> condition2 = new ArchCondition<JavaClass>("方法的入参类型命名以\"Request\"结尾,返回参数命名以\"Response\"结尾") {
@Override
public void check(JavaClass javaClass, ConditionEvents conditionEvents) {
Set<JavaMethod> javaMethods = javaClass.getMethods();
String className = javaClass.getName();
// 其实这里要做严谨一点需要考虑是否使用了泛型参数,这里暂时简化了
for (JavaMethod javaMethod : javaMethods) {
Method method = javaMethod.reflect();
Class<?>[] parameterTypes = method.getParameterTypes();
for (Class parameterType : parameterTypes) {
if (!parameterType.getName().endsWith("Request")) {
conditionEvents.add(SimpleConditionEvent.violated(method,
String.format("当前控制器类[%s]的[%s]方法入参不以\"Request\"结尾", className, method.getName())));
}
}
Class<?> returnType = method.getReturnType();
if (!returnType.getName().endsWith("Response")) {
conditionEvents.add(SimpleConditionEvent.violated(method,
String.format("当前控制器类[%s]的[%s]方法返回参数不以\"Response\"结尾", className, method.getName())));
}
}
}
};
ArchRuleDefinition.classes()
.that(predicate)
.should(condition1)
.andShould(condition2)
.because("定义在controller包下的Controller类的类名称以\"Controller\"结尾,方法的入参类型命名以\"Request\"结尾,返回参数命名以\"Response\"结尾")
.check(classes);
}
}
Поскольку все необходимые статические свойства скомпилированного класса импортируются, в основном все спецификации, о которых только можно подумать, могут быть написаны, а дополнительный контент или реализация могут быть исследованы самостоятельно.
резюме
Представлено в рамках недавнего проектаArchunit
, и выполнил некоторые спецификации кодирования и архитектурные спецификации, которые сыграли очень очевидный эффект. Спецификации предыдущих устных или письменных документов могут напрямую контролироваться модульными тестами Когда проект построен, обязательно выполнять модульные тесты Только все модульные тесты могут быть построены и упакованы (запрещено использовать-Dmaven.test.skip=true
параметры), что имеет весьма очевидный эффект.
Использованная литература:
(Конец этой статьи e-a-2019216 c-1-d)
Технический публичный аккаунт ("Throwable Digest"), который время от времени выкладывает оригинальные технические статьи автора (никогда не занимайтесь плагиатом и не перепечатывайте):