在 Java 中实施分层架构
Posted
技术标签:
【中文标题】在 Java 中实施分层架构【英文标题】:Enforcing layered architecture in Java 【发布时间】:2015-03-15 02:39:23 【问题描述】:给定一个用 Java 编写的软件系统,由 A -> B -> C 三层组成,即 A 层使用 B 层,B 层使用 C 层。
我想确保一层的类只能访问同一层的类或其直接依赖项,即 B 应该能够访问 C 但不能访问 A。A 也应该能够访问 B 但不是 C。
有没有简单的方法来强制执行这样的限制?理想情况下,如果尝试访问错误层的类,我希望 eclipse 立即抱怨。
软件目前使用maven。因此,我尝试将 A、B 和 C 放入不同的 maven 模块并正确声明依赖关系。这可以很好地防止 B 访问 A,但不会阻止 A 访问 C。
接下来我尝试将 C 从对 B 的依赖中排除。这现在也阻止了从 A 到 C 的访问。但是现在我不再能够使用复制依赖来收集运行时所需的所有传递依赖。
有没有一种好方法可以让我清晰地分离层,但也可以让我收集所有需要的运行时依赖项?
【问题讨论】:
我主要是在寻找一个 Maven 解决方案。然而,使用其他环境的优雅解决方案肯定也会很有趣。 运行时范围不应该做正确的事吗? 目前无法在答案中全部输入,但您可能对此感兴趣:java-tutorial.ch/architecture/… @michas 为什么首先需要复制依赖项?为什么不是 jar-with-dependencies,也不是组装,也不是其他任何东西?请说明您的构建过程。 @JeorMattan,即使是像 jar-with-dependencies 这样的东西也应该与 copy-dependencies 插件有完全相同的问题。如果您排除(传递)依赖项,它们对两者都不可用。 【参考方案1】:在 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 项目中,只需将 macker-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>
【讨论】:
【参考方案2】:如果我是你,我会执行以下步骤:
为每一层创建两个模块。一个用于接口,另一个用于实现。 做一个适当的 maven 依赖,避免传递依赖。 安装Sonargraph-Architect plugin in eclipse。它可以让你配置你的层规则。【讨论】:
【参考方案3】:也许你可以在 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>
这对你有帮助吗?
【讨论】:
不错的收获。我从未尝试过,但我想它很好地解决了这个问题。 我猜这是唯一的 maven-only 解决方案,不需要单独的组装项目。但是它非常冗长,因此会出错。 eclipse也不关心运行时范围。 :( - 似乎唯一明确的解决方案是使用其他答案的附加工具之一。【参考方案4】:嗯嗯 - 有趣。我之前肯定遇到过这个问题,但从未尝试过实施解决方案。我想知道您是否可以将接口作为抽象层引入 - 类似于 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”问题。但是,我不确定这是否会提供您正在寻找的完整的传递依赖闭包。
如果您能提出一个可行的解决方案,我很想知道。
【讨论】:
在尝试此操作之前,我强烈建议您也考虑一下此设置将引入的任何更改的阻力。【参考方案5】:可能这不是您正在寻找的解决方案,我没有尝试过,但也许您可以尝试使用 checkstyle。
假设模块 C 中的包被称为“org.project.modulec...”,模块 B 中的包被称为“org.project.moduleb....”和模块 A 中的包“org.project.modulea....”。
您可以在每个模块中配置 maven-checkstyle-plugin 并查找非法包名称。 IE。在模块 A 中,将名为 org.project.modulec 的包的导入配置为非法。 看http://checkstyle.sourceforge.net/config_imports.html(IllegalImport)
你可以配置maven-checkstyle-plugin,每次编译时检查非法导入,使编译失败。
【讨论】:
【参考方案6】:我会建议一些我自己从未真正尝试过的东西——使用 JDepend 编写单元测试来验证架构依赖关系。 JDepend documentation 给出了一个“依赖约束测试”的例子。两个主要的警告是
-
我还没有看到社区采用这种做法,
JDepend 项目似乎已被放弃。
【讨论】:
【参考方案7】:我知道的最好的解决方案是Structure101 software。它允许您定义有关代码依赖项的规则,并在 IDE 中或在构建期间检查它们。
【讨论】:
【参考方案8】:有一个项目叫archunit。
我以前从未使用过它,但您可以编写 JUnit 测试来验证您的架构。
只需要添加如下依赖,就可以开始编写测试了。
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
<version>0.13.1</version>
<scope>test</scope>
</dependency>
您会有测试错误,但不会出现编译时警告,但不依赖于 IDE。
【讨论】:
【参考方案9】:我会从模块 B 中提取接口,即你将拥有 B 和 B-Impl
在这种情况下,您将获得以下依赖项:
A 依赖于 B B-Impl 依赖于 B 和 C为了组装部署工件,您可以创建一个单独的模块,而无需任何依赖于 A 和 B-Impl 的代码
【讨论】:
【参考方案10】:您可以在 Eclipse 中定义类路径工件的访问规则。访问规则可用于映射模式,例如"com.example.*" 到一个决议,例如“禁止”。当定义到受限位置的导入时,这会导致编译器警告。
虽然这对于小型代码集非常有效,但在大型项目中定义访问规则可能非常乏味。请记住,这是一个专有的 Eclipse 功能,因此访问规则存储在 Eclpise 特定的项目配置中。
要定义访问规则,请遵循以下点击路径: 项目属性 > Java 构建路径 > 库 > [您的库或 Maven 模块] > 访问规则 > 单击“编辑”
也可以在“设置”菜单中全局定义访问规则。
【讨论】:
【参考方案11】:看起来你正在尝试做一些 maven 开箱即用的事情。
如果模块 A 使用 exclude C 子句依赖于 B,则在没有明确依赖 C 的情况下,C 类在 A 中是不可访问的。但它们存在于 B 中,因为 B 直接依赖于它们。
然后,当您打包解决方案时,您可以在模块 R(A、B 和 C 的父级)上运行程序集或其他任何东西,并毫不费力地收集它们的依赖关系。
【讨论】:
【参考方案12】:您可以通过将您的 JAR 工件 OSGI 捆绑来强制执行此类层来实现此目的。通过使用 OSGI 指令或使用工具支持手工制作 JAR-MANIFEST(也可以通过 Maven)。如果你使用 Maven,你可以选择各种 Maven 插件来实现这一点。对于像 Eclipse 这样的 IDE 也是如此,您可以在其中选择不同的 Eclipse 插件,如 PDE 或 bndtools。
构建时设计层控制的替代工具是Macker。还有一个maven plugin。
【讨论】:
【参考方案13】:如果你想这样做,你需要一个对象,它只能在A层中定义,并且是B层需要的key 。 Layer C 也是一样的:只能通过提供 key(一个对象)来访问它,该密钥只能从 Layer B创建>.
这是我刚刚创建的代码,它向您展示了如何使用 3 个类来实现这个想法:
A 类:
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();
B 类:
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");
现在,如果您想将此行为扩展到特定的层,您可以如下所示进行:
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");
每一层都以此类推。
【讨论】:
【参考方案14】:对于软件结构,您需要利用最佳编码实践和设计模式。我在下面列出了一些肯定会有所帮助的要点。
对象的创建只能在专门的工厂类中完成 您应该编写代码并仅公开层之间必要的“接口” 您应该利用包范围(默认一个)类的可见性。 如有必要,您应该将代码拆分为单独的子项目,并(如果需要)创建单独的 jar 以确保正确的层间 依赖。
拥有良好的系统设计将完成并超越您的目标。
【讨论】:
【参考方案15】:您可以使用Sonargraph's new 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 articles 中进行了描述。
然后,您可以在构建中通过 Maven 或 Gradle 或类似方式运行 Sonargraph,并在发生规则违规时使构建失败。
【讨论】:
【参考方案16】:如果你经常使用 Spring 框架,你可以看看使用@987654321 的强制模式@ Oliver 也有一些关于这个主题的精彩视频演示。使用 java 本机访问修饰符(公共、私有)也有很大帮助。
【讨论】:
【参考方案17】:为什么不简单地为每一层使用不同的项目?您可以将它们放入您的工作区并根据需要管理构建依赖项。
【讨论】:
您应该在 cmets 中发布此类建议,因为这不是一个全面的答案。以上是关于在 Java 中实施分层架构的主要内容,如果未能解决你的问题,请参考以下文章