1. Overview
1.概述
In this article, we’ll show how to check the architecture of a system using ArchUnit.
在这篇文章中,我们将展示如何使用ArchUnit来检查系统的架构。
2. What Is ArchUnit?
2.什么是ArchUnit?
The link between architecture traits and maintainability is a well-studied topic in the software industry. Defining a sound architecture for our systems is not enough, though. We need to verify that the code implemented adheres to it.
架构特征和可维护性之间的联系是软件行业中一个经过充分研究的话题。但是,为我们的系统定义一个合理的架构是不够的。我们需要验证所实施的代码是否遵守了它。
Simply put, ArchUnit is a test library that allows us to verify that an application adheres to a given set of architectural rules. But, what is an architectural rule? Even more, what do we mean by architecture in this context?
简单地说,ArchUnit是一个测试库,它允许我们验证一个应用程序是否遵守了一组特定的架构规则。但是,什么是架构规则?更有甚者,在这种情况下,我们的架构是什么意思?
Let’s start with the latter. Here, we use the term architecture to refer to the way we organize the different classes in our application into packages.
让我们从后者开始。在这里,我们用架构一词来指我们将应用程序中的不同类组织成包的方式。
The architecture of a system also defines how packages or groups of packages – also known as layers – interact. In more practical terms, it defines whether code in a given package can call a method in a class belonging to another one. For instance, let’s suppose that our application’s architecture contains three layers: presentation, service, and persistence.
系统的架构还定义了包或包组–也被称为层–如何互动。用更实际的术语来说,它定义了一个给定包中的代码是否可以调用属于另一个包的类中的方法。例如,让我们假设我们的应用程序的架构包含三个层。呈现、服务和持久性。
One way to visualize how those layers interact is by using a UML package diagram with a package representing each layer:
一种可视化这些层如何互动的方法是使用UML包图,用一个包代表每个层。
Just by looking at this diagram, we can figure out some rules:
仅仅通过看这个图,我们就可以弄清楚一些规则。
- Presentation classes should only depend on service classes
- Service classes should only depend on persistence classes
- Persistence classes should not depend on anyone else
Looking at those rules, we can now go back and answer our original question. In this context, an architectural rule is an assertion about the way our application classes interact with each other.
看看这些规则,我们现在可以回去回答我们最初的问题了。在这种情况下,架构规则是关于我们的应用程序类之间相互作用方式的断言。
So now, how do we check that our implementation observes those rules? Here is where ArchUnit comes in. It allows us to express our architectural constraints using a fluent API and validate them alongside other tests during a regular build.
那么现在,我们如何检查我们的实现是否遵守了这些规则?这就是ArchUnit的作用。它允许我们使用流畅的API来表达我们的架构约束,并在常规构建期间与其他测试一起验证它们。
3. ArchUnit Project Setup
3.ArchUnit项目设置
ArchUnit integrates nicely with the JUnit test framework, and so, they are typically used together. All we have to do is add the archunit-junit4 dependency to match our JUnit version:
ArchUnit与JUnit测试框架整合得很好,因此,它们通常一起使用。 我们所要做的就是添加archunit-junit4依赖,以匹配我们的JUnit版本。
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit4</artifactId>
<version>0.14.1</version>
<scope>test</scope>
</dependency>
As its artifactId implies, this dependency is specific for the JUnit 4 framework.
正如其artifactId所暗示的那样,这个依赖是针对JUnit4框架的。
There’s also an archunit-junit5 dependency if we are using JUnit 5:
如果我们使用JUnit5,还有一个archunit-junit5依赖项。
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>0.14.1</version>
<scope>test</scope>
</dependency>
4. Writing ArchUnit Tests
4.编写ArchUnit测试
Once we’ve added the appropriate dependency to our project, let’s start writing our architecture tests. Our test application will be a simple SpringBoot REST application that queries Smurfs. For simplicity, this test application only contains the Controller, Service, and Repository classes.
一旦我们将适当的依赖性添加到我们的项目中,让我们开始编写我们的架构测试。我们的测试应用程序将是一个简单的SpringBoot REST应用程序,用于查询Smurfs>。为了简单起见,这个测试应用程序只包含Controller、Service、和Repository类。
We want to verify that this application complies with the rules we’ve mentioned before. So, let’s start with a simple test for the “presentation classes should only depend on service classes” rule.
我们想验证这个应用程序是否符合我们之前提到的规则。因此,让我们从 “演示类应只依赖于服务类 “规则的简单测试开始。
4.1. Our First Test
4.1.我们的第一次测试
The first step is to create a set of Java classes that will be checked for rules violations. We do this by instantiating the ClassFileImporter class and then using one of its importXXX() methods:
第一步是创建一组将被检查是否违反规则的Java类。我们通过实例化ClassFileImporter类,然后使用其importXXX()方法来做到这一点。
JavaClasses jc = new ClassFileImporter()
.importPackages("com.baeldung.archunit.smurfs");
In this case, the JavaClasses instance contains all classes from our main application package and its sub-packages. We can think of this object as being analogous to a typical test subject used in regular unit tests, as it will be the target for rule evaluations.
在这种情况下,JavaClasses实例包含了所有来自我们的主应用程序包及其子包的类。我们可以把这个对象看作是类似于常规单元测试中使用的典型测试对象,因为它将是规则评估的目标。
Architectural rules use one of the static methods from the ArchRuleDefinition class as the starting point for its fluent API calls. Let’s try to implement the first rule defined above using this API. We’ll use the classes() method as our anchor and add additional constraints from there:
架构规则使用ArchRuleDefinition类中的一个静态方法作为其fluent API调用的起点。让我们试着用这个API来实现上面定义的第一条规则。我们将使用classes()方法作为我们的锚点,并从那里添加额外的约束。
ArchRule r1 = classes()
.that().resideInAPackage("..presentation..")
.should().onlyDependOnClassesThat()
.resideInAPackage("..service..");
r1.check(jc);
Notice that we need to call the check() method of the rule we’ve created to run the check. This method takes a JavaClasses object and will throw an exception if there’s a violation.
注意,我们需要调用我们所创建的规则的check()方法来运行检查。这个方法需要一个JavaClasses对象,如果有违反,将抛出一个异常。
This all looks good, but we’ll get a list of errors if we try to run it against our code:
这一切看起来都很好,但如果我们试图针对我们的代码运行它,我们会得到一个错误列表。
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] -
Rule 'classes that reside in a package '..presentation..' should only
depend on classes that reside in a package '..service..'' was violated (6 times):
... error list omitted
Why? The main problem with this rule is the onlyDependsOnClassesThat(). Despite what we’ve put in the package diagram, our actual implementation has dependencies on JVM and Spring framework classes, hence the error.
为什么?这条规则的主要问题是onlyDependsOnClassesThat()。尽管我们在包图中写了什么,我们的实际实现对JVM和Spring框架类有依赖,因此出现了错误。
4.2. Rewriting Our First Test
4.2.重写我们的第一个测试
One way to solve this error is to add a clause that takes into account those additional dependencies:
解决这个错误的方法之一是添加一个考虑到这些额外依赖关系的子句。
ArchRule r1 = classes()
.that().resideInAPackage("..presentation..")
.should().onlyDependOnClassesThat()
.resideInAPackage("..service..", "java..", "javax..", "org.springframework..");
With this change, our check will stop failing. This approach, however, suffers from maintainability issues and feels a bit hacky. We can avoid those issues rewriting our rule using the noClasses() static method as our starting point:
有了这个变化,我们的检查将不再失败。然而,这种方法存在可维护性问题,而且感觉有点儿黑。我们可以避免这些问题,使用noClasses()静态方法作为起点重写我们的规则。
ArchRule r1 = noClasses()
.that().resideInAPackage("..presentation..")
.should().dependOnClassesThat()
.resideInAPackage("..persistence..");
Of course, we can also point that this approach is deny-based instead of the allow-based one we had before. The critical point is that whatever approach we choose, ArchUnit will usually be flexible enough to express our rules.
当然,我们也可以指出,这种方法是deny-based,而不是我们之前的allow-based。关键的一点是,无论我们选择什么方法,ArchUnit通常都会有足够的灵活性来表达我们的规则。
5. Using the Library API
5.使用Library API
ArchUnit makes the creation of complex architectural rules an easy task thanks to its built-in rules. Those, in turn, can also be combined, allowing us to create rules using a higher level of abstraction. Out of the box, ArchUnit offers the Library API, a collection of prepackaged rules that address common architecture concerns:
ArchUnit由于其内置的规则,使创建复杂的架构规则成为一项简单的任务。反过来,这些规则也可以被组合,使我们能够使用更高层次的抽象来创建规则。开箱即用,ArchUnit提供了Library API,这是一组预包装的规则,解决了常见的架构问题。
- Architectures: Support for layered and onion (a.k.a. Hexagonal or “ports and adapters”) architectures rule checks
- Slices: Used to detect circular dependencies, or “cycles”
- General: Collection of rules related to best coding practices such as logging, use of exceptions, etc.
- PlantUML: Checks whether our code base adheres to a given UML model
- Freeze Arch Rules: Save violations for later use, allowing to report only new ones. Particularly useful to manage technical debts
Covering all those rules is out of scope for this introduction, but let’s take a look at the Architecture rule package. In particular, let’s rewrite the rules in the previous section using the layered architecture rules. Using these rules requires two steps: first, we define the layers of our application. Then, we define which layer accesses are allowed:
涵盖所有这些规则超出了本介绍的范围,但让我们看一下Architecture规则包。特别是,让我们使用分层架构规则重写上一节中的规则。使用这些规则需要两个步骤:首先,我们定义我们应用程序的层。然后,我们定义哪些层的访问是允许的。
LayeredArchitecture arch = layeredArchitecture()
// Define layers
.layer("Presentation").definedBy("..presentation..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")
// Add constraints
.whereLayer("Presentation").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Presentation")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service");
arch.check(jc);
Here, layeredArchitecture() is a static method from the Architectures class. When invoked, it returns a new LayeredArchitecture object, which we then use to define names layers and assertions regarding their dependencies. This object implements the ArchRule interface so that we can use it just like any other rule.
这里,layeredArchitecture()是Architectures类中的一个静态方法。当被调用时,它返回一个新的LayeredArchitecture对象,然后我们用它来定义名称层和关于其依赖性的断言。这个对象实现了ArchRule接口,因此我们可以像其他规则一样使用它。
The cool thing about this particular API is that it allows us to create in just a few lines of code rules that would otherwise require us to combine multiple individual rules.
这个特别的API的酷之处在于,它允许我们只用几行代码就能创建规则,否则我们需要结合多个单独的规则。
6. Conclusion
6.结语
In this article, we’ve explored the basics of using ArchUnit in our projects. Adopting this tool is a relatively simple task that can have a positive impact on overall quality and reduce maintenance costs in the long run.
在这篇文章中,我们已经探讨了在项目中使用ArchUnit的基本知识。采用这一工具是一项相对简单的工作,从长远来看,它可以对整体质量产生积极影响,并降低维护成本。
As usual, all code is available over on GitHub.
像往常一样,所有的代码都可以在GitHub上找到。