プロジェクト研究スキーマレベル法定フレームワークArchunit

背景

需要構造の導入は、コーディングプロジェクトの仕様、ちょうど触れ分類モジュールの仕様、クラス依存の規範を、チェックする必要があるときに最近、新しいプロジェクトをやって、ちょうど研究されるように。

多くの場合、我々のような、プロジェクトの仕様を開発します。

  • 必須項目のパケット構造service層は、参照することができないcontrollerベース層(この場合は多少極端に)。
  • で定義された必須controllerのパッケージController、クラス型パラメータ「コントローラ」最後に、クラス名、「要求」という名前のメソッド、「レスポンス」終わりという名前の戻りパラメータを終了します。
  • 列挙型を配置する必要がありますcommon.constant列挙型クラス名の最後にパッケージの下に。

他のカスタム仕様を必要とするかもしれない、ことがあり、最終的に出力文書の多くがあります。しかし、誰が開発したすべてのパラメータがスタッフは仕様書に基づいて開発されることを保証することができますか?、標準化されたインプリメンテーションを確保するためArchunit、対応する単一のプローブ仕様項目コードの違反がある場合、次に、ユニットテストの形で各仕様の符号化のために、パッケージ(さえJAR)内のすべてのクラスのクラスパスをスキャンしてユニットテストの形であなたは完全にCI / CDレベルからのプロジェクトのためのアーキテクチャやコーディング標準を制御することができるようにユニットテストは、合格しないだろう。この記事では、日付書き込まれ2019-02-16、時間Archunitの最新バージョンを0.9.3、使用することをJDK 8

簡単な紹介

Archunitは検査アーキテクチャJavaコードのための無料、シンプル、拡張可能なライブラリです。依存関係やクラスを提供するために、パッケージをチェックし、依存関係のレベルファセット、循環依存関係のチェックや他の関数を呼び出します。それに基づいて、すべてのクラス、導入することにより、コードの構造Javaこの分析を実現するバイトコードを。Archunit主な関心事は、コードアーキテクチャとコーディング規則をテスト自動的に一般的なJavaユニットテストフレームワークを使用することです

依存性の導入

一般的には、最も一般的に使用されるテストフレームワークはJunit4、導入する必要があるJunit4Archunit

<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>
复制代码

使い方

主に使用の導入、次の2つの側面から:

  • スキャンパラメータを指定したクラス。
  • ビルトインルールの定義。

スキャンパラメータを指定したクラス

コードまたはルールが前提に依存して決定する必要が分析しようとするすべてのクラスをインポートすることで、導入された走査型に依存し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));
    }
}
复制代码

取得JavaClassesJavaClassに反映させることができる単純な類推するため、設定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のかのprintStackTrace(例外)を呼び出すことはできません。
  • NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS:ジェネリッククラスは、直接例外のThrowable、例外またはの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.moduleonecom.myapp.moduletwocom.myapp.modulethree3つのパッケージには、例えば、遅い依存円形経路クラスを形成することはできません。

ClassOneInModuleOne -> ClassOneInModuleTwo -> ClassOneInModuleThree -> ClassOneInModuleOne
复制代码

次のように書くのルールは以下のとおりです。

slices().matching("com.myapp.(*)..").should().beFreeOfCycles()
复制代码

コアAPI

APIが3層に分割され、最も重要なのは「コア」層、層と層「ラング」「図書館」です。

Core层API

ネイティブJava APIのAPIに最も類似反射ArchUnitコア層は、例えばJavaMethod、およびJavaField一次反射に対応するMethodField、それらが提供するようにgetName()getMethods()getType()およびgetParameters()が挙げられます。

加えて、拡張APIのArchUnitの数は、例えば、依存コードとの間の関係を記述しJavaMethodCallJavaConstructorCallまたはJavaFieldAccessまた、それはアクセスや、APIなど、他のJavaクラスとの関係の中に、例えば、Javaクラスを提供しますJavaClass#getAccessesFromSelf()

そして、下のクラスパスまたはJARパッケージがコンパイルされたJavaクラスをインポートする必要がArchUnitが提供するClassFileImporterこの機能を達成するために:

JavaClasses classes = new ClassFileImporter().importPackages("com.mycompany.myapp");
复制代码

ラング层API

コアAPI層は、Javaプログラムの静的な構造についての情報を提供する必要性が非常に強いですが、直接、特に建築ルールのパフォーマンスで、パフォーマンスの欠如のためのコアAPI層のユニットテストを使用しています。

このため、ArchUnitはAPIラング層を提供し、それが抽象的に表現し、強力な構文規則を提供します。APIラング層流をほとんど例えば次のようにパケット呼関係を指定されたルール及び定義などのプログラミング方法を定義する使用。

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パラメータ)を、非常に大きな成果を果たしてきました。

参考文献:

(エンド本明細書にEA-2019216 C-1-D)

おすすめ

転載: juejin.im/post/5d98befd51882576e4408e46