[英]Enforcing layered architecture in Java
给定一个用Java编写的软件系统,由三层组成,A -> B -> C,即A层使用B层,B层使用C层。
我想确保一层的类只能访问同一层的类或其直接依赖项,即 B 应该能够访问 C 但不能访问 A。A 也应该能够访问 B 但不能访问 C。
有没有一种简单的方法来强制执行这样的限制? 理想情况下,如果有人试图访问错误层的类,我希望 eclipse 立即抱怨。
该软件目前使用maven。 因此,我尝试将 A、B 和 C 放入不同的 maven 模块并正确声明依赖项。 这可以很好地阻止 B 访问 A,但不会阻止 A 访问 C。
接下来我尝试从对 B 的依赖中排除 C。这现在也阻止了从 A 到 C 的访问。但是现在我不再能够使用复制依赖来收集运行时所需的所有传递依赖。
有没有一种好方法可以让我清晰地分离层,而且还可以让我收集所有需要的运行时依赖项?
在 maven 中,您可以使用 maven-macker-plugin 作为以下示例:
<build>
<plugins>
<plugin>
<groupId>de.andrena.tools.macker</groupId>
<artifactId>macker-maven-plugin</artifactId>
<version>1.0.2</version>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>macker</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
这是一个示例 macker-rules.xml 示例文件:(将其与 pom.xml 放在同一级别)
<?xml version="1.0"?>
<macker>
<ruleset name="Layering rules">
<var name="base" value="org.example" />
<pattern name="appl" class="${base}.**" />
<pattern name="common" class="${base}.common.**" />
<pattern name="persistence" class="${base}.persistence.**" />
<pattern name="business" class="${base}.business.**" />
<pattern name="web" class="${base}.web.**" />
<!-- =============================================================== -->
<!-- Common -->
<!-- =============================================================== -->
<access-rule>
<message>zugriff auf common; von überall gestattet</message>
<deny>
<to pattern="common" />
<allow>
<from>
<include pattern="appl" />
</from>
</allow>
</deny>
</access-rule>
<!-- =============================================================== -->
<!-- Persistence -->
<!-- =============================================================== -->
<access-rule>
<message>zugriff auf persistence; von web und business gestattet</message>
<deny>
<to pattern="persistence" />
<allow>
<from>
<include pattern="persistence" />
<include pattern="web" />
<include pattern="business" />
</from>
</allow>
</deny>
</access-rule>
<!-- =============================================================== -->
<!-- Business -->
<!-- =============================================================== -->
<access-rule>
<message>zugriff auf business; nur von web gestattet</message>
<deny>
<to pattern="business" />
<allow>
<from>
<include pattern="business" />
<include pattern="web" />
</from>
</allow>
</deny>
</access-rule>
<!-- =============================================================== -->
<!-- Web -->
<!-- =============================================================== -->
<access-rule>
<message>zugriff auf web; von nirgends gestattet</message>
<deny>
<to pattern="web" />
<allow>
<from>
<include pattern="web" />
</from>
</allow>
</deny>
</access-rule>
<!-- =============================================================== -->
<!-- Libraries gebunden an ein spezifisches Modul -->
<!-- =============================================================== -->
<access-rule>
<message>nur in web erlaubt</message>
<deny>
<to>
<include class="javax.faces.**" />
<include class="javax.servlet.**" />
<include class="javax.ws.*" />
<include class="javax.enterprise.*" />
</to>
<allow>
<from pattern="web" />
</allow>
</deny>
</access-rule>
<access-rule>
<message>nur in business und persistence erlaubt</message>
<deny>
<to>
<include class="javax.ejb.**" />
<include class="java.sql.**" />
<include class="javax.sql.**" />
<include class="javax.persistence.**" />
</to>
<allow>
<from>
<include pattern="business" />
<include pattern="persistence" />
</from>
</allow>
</deny>
</access-rule>
</ruleset>
</macker>
在一个简单的多模块 maven 项目中,只需将 macer-rules.xml 放在一个中心位置并指向它存储的目录。 那么你需要在你的父 pom.xml 中配置插件
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>de.andrena.tools.macker</groupId>
<artifactId>macker-maven-plugin</artifactId>
<version>1.0.2</version>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>macker</goal>
</goals>
<configuration>
<rulesDirectory>../</rulesDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
如果我是你,我会执行以下步骤:
嗯——有趣。 我以前肯定遇到过这个问题,但从未尝试过实施解决方案。 我想知道您是否可以将接口作为抽象层引入 - 类似于 Facade 模式的东西,然后声明对其的依赖关系。
例如,对于层 B 和 C,创建仅包含这些层的接口的新 maven 项目,我们将这些项目称为 B' 和 C'。 然后,您将只声明对接口层的依赖关系,而不是实现层。
所以 A 将取决于 B'(仅)。 B 将依赖于 B'(因为它将实现那里声明的接口)和 C'。 那么C将取决于C'。 这将防止“A 使用 C”问题,但您将无法获得运行时依赖项。
从那里,您需要使用 maven 范围标签来获取运行时依赖项( http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html )。 这是我真正没有探索过的部分,但我认为您可以使用“运行时”范围来添加依赖项。 因此,您需要添加 A 依赖于 B(具有运行时范围),类似地,B 依赖于 C(具有运行时范围)。 使用运行时范围不会引入编译时依赖关系,因此应该避免重新引入“A 使用 C”问题。 但是,我不确定这是否会提供您正在寻找的完整传递依赖闭包。
我很想知道你是否能提出一个可行的解决方案。
可能这不是您正在寻找的解决方案,我还没有尝试过,但也许您可以尝试使用 checkstyle。
想象一下,模块 C 中的包被称为“ org.project.modulec... ”,模块 B 中的包“ org.project.moduleb.... ”和模块 A 中的包“ org.project.modulea...”。 ”。
您可以在每个模块中配置 maven-checkstyle-plugin 并查找非法包名称。 即在模块 A 中将名为 org.project.modulec 的包的导入配置为非法。 查看http://checkstyle.sourceforge.net/config_imports.html (IllegalImport)
您可以配置 maven-checkstyle-plugin 并且每次编译时检查非法导入并使编译失败。
也许你可以在 A 的 pom 中试试这个:
<dependency>
<groupId>the.groupId</groupId>
<artifactId>moduleB</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>the.groupId</groupId>
<artifactId>moduleC</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>the.groupId</groupId>
<artifactId>moduleC</artifactId>
<version>1.0</version>
<scope>runtime</scope>
</dependency>
这能帮到你吗?
我会建议一些我自己从未真正尝试过的东西——使用 JDepend 编写单元测试来验证架构依赖性。 JDepend 文档给出了一个“依赖约束测试”的例子。 两个主要的警告是
我所知道的最好的解决方案是Structure101 软件。 它允许您定义有关代码依赖项的规则,并在 IDE 中或在构建期间直接检查它们。
有一个名为archunit的项目。
我以前从未使用过它,但您可以编写 JUnit 测试来验证您的架构。
只需要添加如下依赖,就可以开始写测试了。
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
<version>0.13.1</version>
<scope>test</scope>
</dependency>
您将有测试错误,但不是编译时警告,而是不依赖于 IDE。
我会从模块 B 中提取接口,即你将有 B 和 B-Impl
在这种情况下,您将获得以下依赖项:
为了组装部署工件,您可以创建一个单独的模块,而无需任何依赖于 A 和 B-Impl 的代码
您可以为 Eclipse 中的类路径工件定义访问规则。 访问规则可用于将模式(例如“com.example.*”)映射到分辨率(例如“Forbidden”)。 当定义了对受限位置的导入时,这会导致编译器警告。
虽然这对于小型代码集非常有效,但在大型项目中定义访问规则可能非常乏味。 请记住,这是一个专有的 Eclipse 特性,因此访问规则存储在 Eclpise 特定的项目配置中。
要定义访问规则,请遵循以下点击路径:项目属性 > Java 构建路径 > 库 > [您的库或 Maven 模块] > 访问规则 > 单击“编辑”
访问规则也可以在设置菜单中全局定义。
看起来你正在尝试做一些 maven 开箱即用的事情。
如果模块 A 依赖于带有 exclude C 子句的 B,则 C 类在没有显式依赖于 C 的情况下不能在 A 中访问。但是它们存在于 B 中,因为 B 直接依赖于它们。
然后当你打包你的解决方案时,你在模块 R 上运行程序集或其他任何东西,它是 A、B 和 C 的父级,并毫不费力地收集它们的依赖项。
您可以通过使您的 JAR 工件成为强制执行这些层的OSGI包来实现这一点。 通过使用 OSGI 指令或使用工具支持手工制作 JAR-MANIFEST(也可以通过 Maven)。 如果您使用 Maven,您可以在各种 Maven 插件之间进行选择来实现这一点。 对于像 Eclipse 这样的 IDE,您可以在不同的 Eclipse 插件(如PDE或bndtools )之间进行选择。
如果你想这样做,你需要一个只能在A 层中定义的对象,它是B 层需要的键。 层 C 也是一样:它只能通过提供一个只能从层 B创建的键(一个对象)来访问。
这是我刚刚创建的代码,它向您展示了如何使用3 个类实现这个想法:
甲类:
public class A
{
/* only A can create an instance of AKey */
public final class AKey
{
private AKey() {
}
}
public A() {
B b = new B(new AKey());
b.f();
}
}
乙类:
public class B
{
/* only B can create an instance of BKey */
public final class BKey
{
private BKey() {
}
}
/* B wants an instance of AKey, and only A can create it */
public B(A.AKey key) {
if (key == null)
throw new IllegalArgumentException();
C c = new C(new BKey());
c.g();
}
public void f() {
System.out.println("I'm a method of B");
}
}
C类:
public class C
{
/* C wants an instance of BKey, and only B can create it */
public C(B.BKey key) {
if (key == null)
throw new IllegalArgumentException();
}
public void g() {
System.out.println("I'm a method of C");
}
}
现在,如果您想将此行为扩展到特定的Layer ,您可以按如下所示执行:
A层:
public abstract class AbstractA
{
/* only SUBCLASSES can create an instance of AKey */
public final class AKey
{
protected AKey() {
}
}
}
public class A extends AbstractA
{
public A() {
B b = new B(new AKey());
b.f();
BB bb = new BB(new AKey());
bb.f();
}
}
public class AA extends AbstractA
{
public AA() {
B b = new B(new AKey());
b.f();
BB bb = new BB(new AKey());
bb.f();
}
}
B层:
public abstract class AbstractB
{
/* only SUBCLASSES can create an instance of BKey */
public final class BKey
{
protected BKey() {
}
}
}
public class B extends AbstractB
{
/* B wants an instance of AKey, and only A Layer can create it */
public B(AbstractA.AKey key) {
if (key == null)
throw new IllegalArgumentException();
C c = new C(new BKey());
c.g();
CC cc = new CC(new BKey());
cc.g();
}
public void f() {
System.out.println("I'm a method of B");
}
}
public class BB extends AbstractB
{
/* BB wants an instance of AKey, and only A Layer can create it */
public BB(AbstractA.AKey key) {
if (key == null)
throw new IllegalArgumentException();
C c = new C(new BKey());
c.g();
CC cc = new CC(new BKey());
cc.g();
}
public void f() {
System.out.println("I'm a method of BB");
}
}
C层:
public class C
{
/* C wants an instance of BKey, and only B Layer can create it */
public C(B.BKey key) {
if (key == null)
throw new IllegalArgumentException();
}
public void g() {
System.out.println("I'm a method of C");
}
}
public class CC
{
/* CC wants an instance of BKey, and only B Layer can create it */
public CC(B.BKey key) {
if (key == null)
throw new IllegalArgumentException();
}
public void g() {
System.out.println("I'm a method of CC");
}
}
对每一层依此类推。
对于软件结构,您需要利用最佳编码实践和设计模式。 我概述了以下几点肯定会有所帮助。
- 对象的创建应该只在专门的工厂类中完成
- 您应该编码并仅公开层之间必要的“接口”
- 您应该利用包范围(默认范围)类的可见性。
- 如有必要,您应该将代码拆分为单独的子项目,并(如果需要)创建单独的 jar 以确保适当的层间依赖关系。
拥有良好的系统设计将完成并超越您的目标。
您可以使用Sonargraph 的新 DSL 描述您的架构:
artifact A
{
// Pattern matching classes belonging to A
include "**/a/**"
connect to B
}
artifact B
{
include "**/b/**"
connect to C
}
artifact C
{
include "**/c/**"
}
DSL 在一系列BLOG 文章中进行了描述。
然后,您可以在构建中通过 Maven 或 Gradle 或类似方式运行 Sonargraph,并在发生规则违规时使构建失败。
为什么不简单地为每一层使用不同的项目? 您将它们放入您的工作区并根据需要管理构建依赖项。
如果您经常使用 Spring 框架,则可以使用https://github.com/odrotbohm/moduliths查看强制模式,Oliver 也针对此主题提供了一些不错的视频演示。 使用 Java 本机访问修饰符(公共、私有)也有很大帮助。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.