jMolecules 深度实战:让 DDD 与架构规则真正写进 Java 代码

欢迎你来读这篇博客,这篇博客主要是关于 jMolecules 的深入介绍和工程化实战。

如果说 ArchUnit 是“架构警察”,Spring Modulith 是“模块边界管理员”,那么 jMolecules 就像是给代码里的每个类发一张“架构身份证”:这个类是聚合根、这个类是值对象、这个接口是仓储、这个包是领域层、这个包是基础设施层。没有身份证也能写代码,但项目大了以后,谁是谁真的很重要。

序言

在很多 Java 项目里,我们经常会说自己用了 DDD、分层架构、六边形架构、洋葱架构,甚至还会在文档里画很多架构图。

比如我们会说:

1
2
3
4
5
6
7
order 是订单聚合
Order 是聚合根
OrderLine 是聚合内部实体
Money 是值对象
OrderRepository 是领域仓储接口
OrderApplicationService 是应用服务
OrderJpaRepository 是基础设施实现

但问题是,这些信息很多时候只存在于:

1
2
3
4
1. 架构设计文档里
2. 团队口头约定里
3. 老员工脑子里
4. 代码命名习惯里

时间一长,代码开始变成这样:

1
2
3
4
5
6
Controller 直接调用 Mapper
Domain 直接依赖 JPA / MyBatis / Redis
ValueObject 里塞了 Repository
Order 聚合直接引用 Customer 聚合对象
基础设施层反过来污染领域模型
模块边界只剩目录结构,没有约束能力

架构不是一下子坏掉的,而是一点一点“氧化”的。jMolecules 要解决的就是这个问题:

把架构概念直接表达在 Java 代码中,让代码自己说明它承担的架构角色,并让工具能够基于这些角色做验证、文档生成和技术集成。

正文

一、jMolecules 是什么?

jMolecules 是一组 Java 架构抽象库,用来在代码中显式表达 DDD、事件、CQRS、分层架构、六边形架构、洋葱架构等设计概念。

它的核心价值不是“帮你写业务代码”,而是:

1
2
3
4
5
让代码具有架构语义
让架构概念不只存在于文档
让工具能够识别这些架构概念
让架构规则可以自动验证
减少领域模型对技术框架的依赖

一句话总结:

jMolecules 是一个让 Java 代码变得“架构可读、架构可查、架构可验证”的工具库。

它主要提供两类表达方式:

1
2
1. Annotation-based model:通过注解表达架构角色
2. Type-based model:通过 Java 类型系统表达架构角色

二、jMolecules 解决的不是代码运行问题,而是架构退化问题

很多人第一次看到 jMolecules 会有疑问:

我已经有 Spring 的 @Service、@Repository,也有 JPA 的 @Entity,为什么还需要 jMolecules?

这个问题非常关键。

Spring、JPA、MyBatis 这些注解表达的是“技术角色”:

1
2
3
4
@Service:这是一个 Spring Bean
@Repository:这是一个 Spring 数据访问组件
@Entity:这是一个 JPA 持久化实体
@Mapper:这是一个 MyBatis Mapper

而 jMolecules 表达的是“架构角色”或“领域角色”:

1
2
3
4
5
6
7
8
@AggregateRoot:这是一个 DDD 聚合根
@Entity:这是一个 DDD 实体,不是 JPA 实体
@ValueObject:这是一个值对象
@Repository:这是一个 DDD 仓储概念,不只是数据库访问类
@DomainEvent:这是一个领域事件
@DomainLayer:这是领域层
@ApplicationLayer:这是应用层
@InfrastructureLayer:这是基础设施层

它们关注点不同:

类型 关注点 示例
Spring 注解 运行时 Bean 管理 @Service, @Component
JPA 注解 ORM 持久化映射 @Entity, @Embeddable
MyBatis 注解 SQL 映射 @Mapper
jMolecules 注解 架构语义表达 @AggregateRoot, @ValueObject

所以,jMolecules 的核心不是替代 Spring,而是补上 Java 语言和主流框架缺失的“架构语义层”。

三、版本选择

本文以 2026 年 6 月能查到的主线版本为基准:

1
2
3
4
5
6
JDK:21
Spring Boot:3.5.x
jMolecules DDD:2.0.1
jMolecules BOM:2025.0.2
jMolecules Integrations:0.33.0
ArchUnit:1.4.x 左右

如果你的项目还在 Spring Boot 2.x / JDK 8 / JDK 11 上,也可以使用 jMolecules 1.x 系列,但新项目建议直接看 2.x。

jMolecules 2.x 引入了 stereotype metamodel,也就是“架构角色元模型”。简单说,以前 jMolecules 更多是一些注解和接口;到了 2.x,它开始把这些架构角色用统一的元数据描述出来,让工具、IDE、文档生成器、架构验证工具更容易识别这些角色。

四、Maven 依赖

4.1 基础 DDD 依赖

如果你只想表达 DDD 概念,最小依赖如下:

1
2
3
4
5
6
7
8
9
10
11
<properties>
<jmolecules.version>2.0.1</jmolecules.version>
</properties>

<dependencies>
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-ddd</artifactId>
<version>${jmolecules.version}</version>
</dependency>
</dependencies>

4.2 使用 BOM 管理版本

如果你使用多个 jMolecules 模块,建议用 BOM:

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-bom</artifactId>
<version>2025.0.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

然后依赖可以不写版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependencies>
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-ddd</artifactId>
</dependency>

<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-layered-architecture</artifactId>
</dependency>

<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-hexagonal-architecture</artifactId>
</dependency>
</dependencies>

4.3 Spring / Jackson / ArchUnit 集成依赖

如果你要在 Spring Boot 项目中使用 jMolecules 的运行时集成,可以加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<properties>
<jmolecules.integrations.version>0.33.0</jmolecules.integrations.version>
</properties>

<dependencies>
<!-- Spring 运行时集成:Identifier 转换、AssociationResolver 等 -->
<dependency>
<groupId>org.jmolecules.integrations</groupId>
<artifactId>jmolecules-spring</artifactId>
<version>${jmolecules.integrations.version}</version>
</dependency>

<!-- Jackson 集成:让 Identifier、单字段 ValueObject 序列化更自然 -->
<dependency>
<groupId>org.jmolecules.integrations</groupId>
<artifactId>jmolecules-jackson</artifactId>
<version>${jmolecules.integrations.version}</version>
</dependency>

<!-- ArchUnit 规则:基于 jMolecules 语义做架构校验 -->
<dependency>
<groupId>org.jmolecules.integrations</groupId>
<artifactId>jmolecules-archunit</artifactId>
<version>${jmolecules.integrations.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

如果你不需要运行时转换,只想让代码具备架构语义,那么只加 jmolecules-ddd 就够了。

五、Annotation-based model:用注解表达 DDD 角色

注解模式最适合老项目和渐进式改造。它对代码侵入较低,不要求你强行改变类继承结构。

5.1 普通代码的问题

比如我们有一个订单类:

1
2
3
4
5
public class Order {
private Long id;
private List<OrderLine> lines;
private BigDecimal totalAmount;
}

这段代码能运行,但架构语义很弱。

别人看到它,会问:

1
2
3
4
Order 是聚合根吗?
OrderLine 是聚合内部实体吗?
totalAmount 是不是应该建模成 Money 值对象?
id 是领域标识还是数据库主键?

代码没有回答这些问题。

5.2 使用 jMolecules 注解表达领域角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.example.finance.order.domain;

import org.jmolecules.ddd.annotation.AggregateRoot;
import org.jmolecules.ddd.annotation.Identity;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@AggregateRoot
public class Order {

@Identity
private final OrderId id;

private final CustomerId customerId;

private final List<OrderLine> lines = new ArrayList<>();

private OrderStatus status;

private Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
this.status = OrderStatus.CREATED;
}

public static Order create(OrderId id, CustomerId customerId) {
return new Order(id, customerId);
}

public void addLine(ProductId productId, int quantity, Money price) {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("只有创建状态的订单可以添加明细");
}
this.lines.add(OrderLine.create(OrderLineId.next(), productId, quantity, price));
}

public void submit() {
if (lines.isEmpty()) {
throw new IllegalStateException("订单明细不能为空");
}
this.status = OrderStatus.SUBMITTED;
}

public OrderId id() {
return id;
}

public CustomerId customerId() {
return customerId;
}

public List<OrderLine> lines() {
return Collections.unmodifiableList(lines);
}
}

OrderId 可以建模成值对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.finance.order.domain;

import org.jmolecules.ddd.annotation.ValueObject;

import java.util.UUID;

@ValueObject
public record OrderId(UUID value) {

public static OrderId next() {
return new OrderId(UUID.randomUUID());
}

public static OrderId of(UUID value) {
return new OrderId(value);
}
}

金额也应该是值对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.finance.order.domain;

import org.jmolecules.ddd.annotation.ValueObject;

import java.math.BigDecimal;
import java.math.RoundingMode;

@ValueObject
public record Money(BigDecimal amount, String currency) {

public Money {
if (amount == null) {
throw new IllegalArgumentException("amount 不能为空");
}
if (currency == null || currency.isBlank()) {
throw new IllegalArgumentException("currency 不能为空");
}
amount = amount.setScale(2, RoundingMode.HALF_UP);
}

public static Money cny(BigDecimal amount) {
return new Money(amount, "CNY");
}

public Money multiply(int quantity) {
return new Money(amount.multiply(BigDecimal.valueOf(quantity)), currency);
}

public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("币种不一致,不能相加");
}
return new Money(amount.add(other.amount), currency);
}
}

订单明细是聚合内部实体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.finance.order.domain;

import org.jmolecules.ddd.annotation.Entity;
import org.jmolecules.ddd.annotation.Identity;

@Entity
public class OrderLine {

@Identity
private final OrderLineId id;

private final ProductId productId;

private final int quantity;

private final Money price;

private OrderLine(OrderLineId id, ProductId productId, int quantity, Money price) {
if (quantity <= 0) {
throw new IllegalArgumentException("quantity 必须大于 0");
}
this.id = id;
this.productId = productId;
this.quantity = quantity;
this.price = price;
}

public static OrderLine create(OrderLineId id, ProductId productId, int quantity, Money price) {
return new OrderLine(id, productId, quantity, price);
}

public Money subtotal() {
return price.multiply(quantity);
}
}

仓储接口表达为 DDD Repository:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.finance.order.domain;

import org.jmolecules.ddd.annotation.Repository;

import java.util.Optional;

@Repository
public interface Orders {

Order save(Order order);

Optional<Order> findById(OrderId id);
}

注意这里我故意把接口命名为 Orders,而不是 OrderRepository。这背后有一个 DDD 思想:

领域语言应该更接近业务概念,而不是技术模式名。

当然,团队如果更习惯 OrderRepository 也不是不行。工程落地不是拜神,别把命名搞成宗教战争。

六、Type-based model:用 Java 类型系统表达领域关系

注解模式是“贴标签”,类型模式是“让编译器参与约束”。

jMolecules 的 DDD types 大致包括:

1
2
3
4
5
6
Identifier:标识类型
Identifiable:可识别对象
Entity:实体
AggregateRoot:聚合根
Association:跨聚合关联
ValueObject:值对象

6.1 标识类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.finance.order.domain;

import org.jmolecules.ddd.types.Identifier;

import java.util.UUID;

public record OrderId(UUID value) implements Identifier {

public static OrderId next() {
return new OrderId(UUID.randomUUID());
}

public static OrderId of(UUID value) {
return new OrderId(value);
}
}

6.2 聚合根

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.finance.order.domain;

import org.jmolecules.ddd.types.AggregateRoot;

import java.util.ArrayList;
import java.util.List;

public class Order implements AggregateRoot<Order, OrderId> {

private final OrderId id;

private final CustomerId customerId;

private final List<OrderLine> lines = new ArrayList<>();

private OrderStatus status;

private Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
this.status = OrderStatus.CREATED;
}

public static Order create(OrderId id, CustomerId customerId) {
return new Order(id, customerId);
}

@Override
public OrderId getId() {
return id;
}
}

6.3 聚合内部实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.finance.order.domain;

import org.jmolecules.ddd.types.Entity;

public class OrderLine implements Entity<Order, OrderLineId> {

private final OrderLineId id;

private final ProductId productId;

private final int quantity;

private final Money price;

public OrderLine(OrderLineId id, ProductId productId, int quantity, Money price) {
this.id = id;
this.productId = productId;
this.quantity = quantity;
this.price = price;
}

@Override
public OrderLineId getId() {
return id;
}
}

这里的重点不是多写一个 implements Entity<Order, OrderLineId>,而是明确告诉编译器和工具:

1
2
3
OrderLine 是 Order 聚合内部的实体
OrderLineId 是 OrderLine 的标识类型
OrderLine 不能被随便塞到 Customer 聚合里

6.4 类型模式的收益

类型模式比注解模式更强,因为它能表达更严格的关系:

1
2
3
4
1. 每个聚合可以拥有专用 ID 类型
2. 聚合内部实体可以声明自己属于哪个聚合
3. 跨聚合引用可以通过 ID 或 Association 表达
4. ArchUnit 可以基于这些类型关系检查错误引用

比如下面这种代码就是危险的:

1
2
3
4
5
public class Order implements AggregateRoot<Order, OrderId> {

// 不推荐:订单聚合直接持有 Customer 聚合对象
private Customer customer;
}

更推荐:

1
2
3
4
5
public class Order implements AggregateRoot<Order, OrderId> {

// 推荐:跨聚合只引用 ID
private CustomerId customerId;
}

如果你需要表达显式关联,也可以使用 Association

七、实战案例:finance-order-demo

下面用一个简化的财务订单模块来演示 jMolecules 的落地方式。

7.1 业务背景

我们假设有一个财务系统,里面包含:

1
2
3
4
订单模块 order
客户模块 customer
支付模块 payment
结算模块 settlement

订单创建后会发布领域事件,支付模块监听订单提交事件并初始化支付单,结算模块后续根据订单和支付结果生成结算数据。

7.2 推荐包结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
com.example.finance
├── FinanceApplication.java
├── order
│ ├── api
│ │ ├── OrderFacade.java
│ │ └── CreateOrderCommand.java
│ ├── application
│ │ └── OrderApplicationService.java
│ ├── domain
│ │ ├── Order.java
│ │ ├── OrderId.java
│ │ ├── OrderLine.java
│ │ ├── OrderLineId.java
│ │ ├── Money.java
│ │ ├── Orders.java
│ │ ├── OrderSubmitted.java
│ │ └── package-info.java
│ └── infrastructure
│ ├── OrderJpaEntity.java
│ ├── OrderJpaRepository.java
│ └── JpaOrders.java
├── payment
│ ├── application
│ ├── domain
│ └── infrastructure
└── settlement
├── application
├── domain
└── infrastructure

这个结构里有几个关键点:

1
2
3
4
api:对其他模块暴露的入口
application:应用服务,负责编排用例
domain:领域模型、聚合、值对象、领域事件、领域仓储接口
infrastructure:数据库、消息、外部接口、技术实现

7.3 用 package-info.java 标记架构层

jMolecules 不只可以标记类,也可以标记包。

领域层:

1
2
3
4
@DomainLayer
package com.example.finance.order.domain;

import org.jmolecules.architecture.layered.DomainLayer;

应用层:

1
2
3
4
@ApplicationLayer
package com.example.finance.order.application;

import org.jmolecules.architecture.layered.ApplicationLayer;

基础设施层:

1
2
3
4
@InfrastructureLayer
package com.example.finance.order.infrastructure;

import org.jmolecules.architecture.layered.InfrastructureLayer;

接口层:

1
2
3
4
@InterfaceLayer
package com.example.finance.order.api;

import org.jmolecules.architecture.layered.InterfaceLayer;

这样做的好处是,包不再只是目录,而是有明确架构含义。

八、订单聚合完整示例

8.1 OrderId

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.finance.order.domain;

import org.jmolecules.ddd.annotation.ValueObject;

import java.util.UUID;

@ValueObject
public record OrderId(UUID value) {

public static OrderId next() {
return new OrderId(UUID.randomUUID());
}

public static OrderId of(UUID value) {
return new OrderId(value);
}
}

8.2 CustomerId

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.finance.order.domain;

import org.jmolecules.ddd.annotation.ValueObject;

import java.util.UUID;

@ValueObject
public record CustomerId(UUID value) {

public static CustomerId of(UUID value) {
return new CustomerId(value);
}
}

这里有一个重要细节:订单聚合里使用 CustomerId,而不是直接引用 Customer 对象。

原因是:

1
2
3
Order 和 Customer 是不同聚合
跨聚合引用应该优先使用 ID
不要把两个聚合强行揉成一个事务一致性边界

8.3 OrderLineId

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.finance.order.domain;

import org.jmolecules.ddd.annotation.ValueObject;

import java.util.UUID;

@ValueObject
public record OrderLineId(UUID value) {

public static OrderLineId next() {
return new OrderLineId(UUID.randomUUID());
}
}

8.4 ProductId

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.finance.order.domain;

import org.jmolecules.ddd.annotation.ValueObject;

import java.util.UUID;

@ValueObject
public record ProductId(UUID value) {

public static ProductId of(UUID value) {
return new ProductId(value);
}
}

8.5 OrderStatus

1
2
3
4
5
6
7
package com.example.finance.order.domain;

public enum OrderStatus {
CREATED,
SUBMITTED,
CANCELLED
}

8.6 Money

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.example.finance.order.domain;

import org.jmolecules.ddd.annotation.ValueObject;

import java.math.BigDecimal;
import java.math.RoundingMode;

@ValueObject
public record Money(BigDecimal amount, String currency) {

public Money {
if (amount == null) {
throw new IllegalArgumentException("amount 不能为空");
}
if (currency == null || currency.isBlank()) {
throw new IllegalArgumentException("currency 不能为空");
}
amount = amount.setScale(2, RoundingMode.HALF_UP);
}

public static Money zeroCny() {
return new Money(BigDecimal.ZERO, "CNY");
}

public static Money cny(BigDecimal amount) {
return new Money(amount, "CNY");
}

public Money add(Money other) {
assertSameCurrency(other);
return new Money(amount.add(other.amount), currency);
}

public Money multiply(int quantity) {
if (quantity < 0) {
throw new IllegalArgumentException("quantity 不能为负数");
}
return new Money(amount.multiply(BigDecimal.valueOf(quantity)), currency);
}

private void assertSameCurrency(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("不同币种不能直接计算");
}
}
}

8.7 OrderLine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.finance.order.domain;

import org.jmolecules.ddd.annotation.Entity;
import org.jmolecules.ddd.annotation.Identity;

@Entity
public class OrderLine {

@Identity
private final OrderLineId id;

private final ProductId productId;

private final int quantity;

private final Money price;

private OrderLine(OrderLineId id, ProductId productId, int quantity, Money price) {
if (quantity <= 0) {
throw new IllegalArgumentException("quantity 必须大于 0");
}
this.id = id;
this.productId = productId;
this.quantity = quantity;
this.price = price;
}

public static OrderLine create(ProductId productId, int quantity, Money price) {
return new OrderLine(OrderLineId.next(), productId, quantity, price);
}

public Money subtotal() {
return price.multiply(quantity);
}
}

8.8 OrderSubmitted 领域事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.finance.order.domain;

import org.jmolecules.event.annotation.DomainEvent;

import java.time.Instant;

@DomainEvent
public record OrderSubmitted(
OrderId orderId,
CustomerId customerId,
Instant occurredAt
) {

public static OrderSubmitted now(Order order) {
return new OrderSubmitted(order.id(), order.customerId(), Instant.now());
}
}

领域事件不是“MQ 消息对象”,它首先是领域事实。

OrderSubmitted 表达的是:

1
2
3
订单已经被提交
这是领域里已经发生的事实
别的模块可以基于这个事实做后续动作

8.9 Order 聚合根

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.example.finance.order.domain;

import org.jmolecules.ddd.annotation.AggregateRoot;
import org.jmolecules.ddd.annotation.Identity;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@AggregateRoot
public class Order {

@Identity
private final OrderId id;

private final CustomerId customerId;

private final List<OrderLine> lines = new ArrayList<>();

private OrderStatus status;

private Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
this.status = OrderStatus.CREATED;
}

public static Order create(OrderId id, CustomerId customerId) {
return new Order(id, customerId);
}

public void addLine(ProductId productId, int quantity, Money price) {
requireCreatedStatus();
this.lines.add(OrderLine.create(productId, quantity, price));
}

public OrderSubmitted submit() {
requireCreatedStatus();
if (lines.isEmpty()) {
throw new IllegalStateException("订单明细不能为空");
}
this.status = OrderStatus.SUBMITTED;
return OrderSubmitted.now(this);
}

public Money totalAmount() {
return lines.stream()
.map(OrderLine::subtotal)
.reduce(Money.zeroCny(), Money::add);
}

private void requireCreatedStatus() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("当前订单状态不允许该操作:" + status);
}
}

public OrderId id() {
return id;
}

public CustomerId customerId() {
return customerId;
}

public List<OrderLine> lines() {
return Collections.unmodifiableList(lines);
}
}

这段代码有几个设计点:

1
2
3
4
5
1. Order 是聚合根,负责维护订单内部一致性
2. OrderLine 只能通过 Order 的行为被添加
3. Money 是值对象,负责金额计算规则
4. submit() 返回领域事件,表达业务事实
5. Order 只持有 CustomerId,不直接依赖 Customer 聚合对象

这就是 jMolecules 和 DDD 结合的核心:不是给贫血模型贴几个注解,而是让注解和领域行为一起出现。

九、应用服务:编排用例,而不是写业务规则

应用服务负责事务、权限、输入输出转换、领域对象加载和保存、事件发布。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.example.finance.order.application;

import com.example.finance.order.api.CreateOrderCommand;
import com.example.finance.order.domain.CustomerId;
import com.example.finance.order.domain.Money;
import com.example.finance.order.domain.Order;
import com.example.finance.order.domain.OrderId;
import com.example.finance.order.domain.OrderSubmitted;
import com.example.finance.order.domain.Orders;
import com.example.finance.order.domain.ProductId;
import org.jmolecules.ddd.annotation.Service;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderApplicationService {

private final Orders orders;

private final ApplicationEventPublisher publisher;

public OrderApplicationService(Orders orders, ApplicationEventPublisher publisher) {
this.orders = orders;
this.publisher = publisher;
}

@Transactional
public OrderId create(CreateOrderCommand command) {
Order order = Order.create(OrderId.next(), CustomerId.of(command.customerId()));

command.lines().forEach(line -> order.addLine(
ProductId.of(line.productId()),
line.quantity(),
Money.cny(line.price())
));

OrderSubmitted event = order.submit();

orders.save(order);
publisher.publishEvent(event);

return order.id();
}
}

这里用了 org.jmolecules.ddd.annotation.Service,它表达的是 DDD 语义上的服务。实际 Spring 项目里你有三种选择:

1
2
3
1. 同时加 Spring @Service 和 jMolecules @Service
2. 使用 jmolecules-spring / ByteBuddy 集成,让 jMolecules 注解转换为 Spring 能识别的组件
3. 保守一点:应用层继续用 Spring @Service,领域层使用 jMolecules 注解表达语义

生产项目里,我更建议第 3 种起步:

1
2
3
不要一上来就搞太多魔法增强
先让代码语义清晰
再逐步引入运行时集成

架构治理不是魔法学院招生,别第一天就让团队背咒语。

十、基础设施实现:不要让技术模型污染领域模型

领域层定义仓储接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.finance.order.domain;

import org.jmolecules.ddd.annotation.Repository;

import java.util.Optional;

@Repository
public interface Orders {

Order save(Order order);

Optional<Order> findById(OrderId id);
}

基础设施层实现它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.finance.order.infrastructure;

import com.example.finance.order.domain.Order;
import com.example.finance.order.domain.OrderId;
import com.example.finance.order.domain.Orders;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public class JpaOrders implements Orders {

private final OrderJpaRepository repository;

private final OrderMapper mapper;

public JpaOrders(OrderJpaRepository repository, OrderMapper mapper) {
this.repository = repository;
this.mapper = mapper;
}

@Override
public Order save(Order order) {
OrderJpaEntity entity = mapper.toEntity(order);
OrderJpaEntity saved = repository.save(entity);
return mapper.toDomain(saved);
}

@Override
public Optional<Order> findById(OrderId id) {
return repository.findById(id.value())
.map(mapper::toDomain);
}
}

JPA Entity 放基础设施层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.finance.order.infrastructure;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import java.math.BigDecimal;
import java.util.UUID;

@Entity
@Table(name = "finance_order")
public class OrderJpaEntity {

@Id
private UUID id;

@Column(name = "customer_id", nullable = false)
private UUID customerId;

@Column(name = "status", nullable = false)
private String status;

@Column(name = "total_amount", nullable = false)
private BigDecimal totalAmount;

protected OrderJpaEntity() {
}

// getter/setter 省略
}

这种方式虽然多了 mapper,但边界很干净:

1
2
3
4
domain 不知道 JPA
domain 不知道数据库表
domain 不知道 MyBatis / Hibernate
infrastructure 负责技术适配

如果你的项目更偏 CRUD,也可以让领域对象直接带 JPA 注解。不是绝对不行,只是要知道代价:领域模型会被 ORM 生命周期、懒加载、无参构造、setter、代理对象等问题影响。

十一、与 ArchUnit 组合:让 jMolecules 规则自动校验

jMolecules 本身表达语义,ArchUnit 可以检查语义是否被破坏。

11.1 添加测试依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.4.1</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.jmolecules.integrations</groupId>
<artifactId>jmolecules-archunit</artifactId>
<version>0.33.0</version>
<scope>test</scope>
</dependency>

11.2 使用 jMolecules 内置 DDD 规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.finance.arch;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import org.jmolecules.archunit.JMoleculesDddRules;
import org.junit.jupiter.api.Test;

class DddArchitectureTest {

@Test
void verify_jmolecules_ddd_rules() {
JavaClasses classes = new ClassFileImporter()
.importPackages("com.example.finance");

JMoleculesDddRules.all().check(classes);
}
}

JMoleculesDddRules.all() 会组合多条 DDD 规则,例如:

1
2
3
4
1. 聚合内部实体应该声明自己属于同一个聚合
2. 一个聚合引用另一个聚合时,应该通过 ID 或 Association,而不是直接引用聚合对象
3. 标注了 @Entity / @AggregateRoot 的类应该声明 @Identity
4. 值对象和标识类型不应该引用可识别对象

11.3 示例:错误的跨聚合引用

假设你写了这样的代码:

1
2
3
4
5
6
7
8
9
@AggregateRoot
public class Order {

@Identity
private OrderId id;

// 错误:直接引用 Customer 聚合
private Customer customer;
}

如果 Customer 也是聚合根,jMolecules + ArchUnit 就可以发现这种危险依赖。

更合理的写法:

1
2
3
4
5
6
7
8
9
@AggregateRoot
public class Order {

@Identity
private OrderId id;

// 正确:跨聚合引用 ID
private CustomerId customerId;
}

这条规则对复杂系统很重要。很多项目聚合越写越大的根源就是:

1
2
3
4
5
订单持有客户
客户持有账户
账户持有合同
合同持有结算单
最后整个系统变成一个巨型对象图

然后你会发现事务边界爆炸、查询爆炸、序列化爆炸、性能爆炸。俗称:Java 烟花秀。

十二、自己补充 ArchUnit 规则

jMolecules 的内置规则很好,但真实项目通常还需要团队自己的规则。

12.1 领域层不能依赖基础设施层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.example.finance.arch;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import org.junit.jupiter.api.Test;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

class LayerArchitectureTest {

private final JavaClasses classes = new ClassFileImporter()
.importPackages("com.example.finance");

@Test
void domain_should_not_depend_on_infrastructure() {
noClasses()
.that()
.resideInAPackage("..domain..")
.should()
.dependOnClassesThat()
.resideInAnyPackage(
"..infrastructure..",
"..adapter..",
"org.springframework.web..",
"jakarta.persistence..",
"org.mybatis.."
)
.check(classes);
}
}

12.2 Controller 不能直接访问 Repository

1
2
3
4
5
6
7
8
9
10
@Test
void api_should_not_access_repository_directly() {
noClasses()
.that()
.resideInAnyPackage("..api..", "..controller..")
.should()
.dependOnClassesThat()
.resideInAnyPackage("..repository..", "..mapper..", "..infrastructure..")
.check(classes);
}

12.3 领域对象不要使用 Spring 注入

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void domain_should_not_use_spring_dependency_injection() {
noClasses()
.that()
.resideInAPackage("..domain..")
.should()
.beAnnotatedWith(org.springframework.stereotype.Component.class)
.orShould()
.beAnnotatedWith(org.springframework.stereotype.Service.class)
.orShould()
.beAnnotatedWith(org.springframework.beans.factory.annotation.Autowired.class)
.check(classes);
}

领域对象应该通过构造、方法参数和领域行为表达依赖,不应该把 Spring 容器伸进领域对象里。

十三、与 Spring Modulith 组合

jMolecules 和 Spring Modulith 是非常适合一起使用的。

它们分工不同:

工具 作用
jMolecules 表达类、接口、包的架构语义
Spring Modulith 识别 Spring Boot 应用模块并验证模块边界
ArchUnit 编写和执行架构规则测试

组合之后可以形成:

1
2
3
Spring Modulith:检查模块边界
jMolecules:标记模块内部 DDD 角色
ArchUnit:验证角色之间的依赖规则

13.1 模块结构示例

1
2
3
4
5
6
7
8
9
10
11
12
13
com.example.finance
├── order
│ ├── OrderApi.java
│ └── internal
│ ├── domain
│ ├── application
│ └── infrastructure
├── payment
│ ├── PaymentApi.java
│ └── internal
└── settlement
├── SettlementApi.java
└── internal

13.2 设计原则

1
2
3
4
5
1. 模块根包暴露 API
2. internal 包隐藏模块内部实现
3. 领域对象用 jMolecules 标记 DDD 角色
4. 模块之间通过 API 或事件协作
5. 不允许跨模块访问 internal

13.3 Spring Modulith 验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.finance.arch;

import com.example.finance.FinanceApplication;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;

class ModulithArchitectureTest {

@Test
void verify_modules() {
ApplicationModules modules = ApplicationModules.of(FinanceApplication.class);
modules.verify();
}
}

这样你就可以同时验证:

1
2
3
模块之间有没有非法依赖
模块内部 DDD 角色是否合理
领域层有没有被技术框架污染

十四、与 Spring Boot JSON 参数绑定集成

如果你的接口里直接使用 OrderId 这类单字段值对象,常见问题是 JSON 绑定。

比如你希望请求参数是:

1
2
3
{
"orderId": "7e03cc66-0889-4e66-bbc2-7d22f612c850"
}

而不是:

1
2
3
4
5
{
"orderId": {
"value": "7e03cc66-0889-4e66-bbc2-7d22f612c850"
}
}

这时候可以使用 jmolecules-jackson,它会让单字段 @ValueObjectIdentifier 的序列化更自然。

依赖:

1
2
3
4
5
<dependency>
<groupId>org.jmolecules.integrations</groupId>
<artifactId>jmolecules-jackson</artifactId>
<version>0.33.0</version>
</dependency>

对于 Spring MVC 参数转换,可以使用 jmolecules-spring

例如:

1
2
3
4
@GetMapping("/orders/{id}")
public OrderDetailResponse detail(@PathVariable OrderId id) {
return orderFacade.detail(id);
}

如果你的 OrderId 暴露了静态工厂方法:

1
2
3
4
5
6
public record OrderId(UUID value) implements Identifier {

public static OrderId of(UUID value) {
return new OrderId(value);
}
}

Spring 集成可以帮助做基础类型到 Identifier 的转换。

不过生产上我仍然建议:

1
2
3
1. 对外 API 层可以使用 String / UUID / Long
2. 进入 application 层后再转换成领域 ID
3. 如果团队习惯强类型 ID,再逐步引入 jmolecules-spring

不要为了“优雅”让前后端联调第一天就开始玄学报错。

十五、jMolecules 与 JPA / MyBatis 的落地策略

15.1 策略一:领域模型和持久化模型分离

这是最干净的方式。

1
2
3
domain.Order          领域模型
infrastructure.OrderJpaEntity 持久化模型
OrderMapper 双向转换

优点:

1
2
3
4
领域层最干净
不会被 ORM 约束绑架
方便做 DDD 行为建模
适合复杂业务

缺点:

1
2
3
代码量更多
需要维护 mapper
简单 CRUD 会显得重

15.2 策略二:领域模型直接加 JPA 注解

1
2
3
4
5
6
7
@jakarta.persistence.Entity
@org.jmolecules.ddd.annotation.AggregateRoot
public class Order {
@jakarta.persistence.Id
@org.jmolecules.ddd.annotation.Identity
private UUID id;
}

优点:

1
2
3
开发快
代码少
适合简单业务

缺点:

1
2
3
领域模型不纯
容易被 ORM 生命周期影响
懒加载和代理对象容易引入隐藏复杂度

15.3 策略三:使用 jMolecules JPA / ByteBuddy 集成

jMolecules integrations 提供了 ByteBuddy、JPA 等技术集成,用于把 jMolecules 的模式转换成一些技术框架需要的元数据或样板代码。

但我的建议是:

1
2
3
新团队:先别急着用字节码增强
老项目:先用注解表达语义 + ArchUnit 校验
架构稳定后:再评估是否引入更深层的技术集成

因为字节码增强一旦出问题,排查成本不低。架构治理要稳,不要把“可维护性工程”做成“灵异事件研究所”。

十六、jMolecules 与 MyBatis-Plus 的实践建议

很多国内项目使用 MyBatis-Plus,而不是 JPA。

推荐结构:

1
2
3
4
5
order.domain.Order                 DDD 聚合根
order.domain.Orders 领域仓储接口
order.infrastructure.OrderDO 数据库对象
order.infrastructure.OrderMapper MyBatis Mapper
order.infrastructure.MybatisOrders 仓储实现

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.finance.order.infrastructure;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.math.BigDecimal;
import java.util.UUID;

@TableName("finance_order")
public class OrderDO {

@TableId
private UUID id;

private UUID customerId;

private String status;

private BigDecimal totalAmount;

// getter/setter 省略
}

Mapper:

1
2
3
4
5
6
7
8
package com.example.finance.order.infrastructure;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderMapper extends BaseMapper<OrderDO> {
}

仓储实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.finance.order.infrastructure;

import com.example.finance.order.domain.Order;
import com.example.finance.order.domain.OrderId;
import com.example.finance.order.domain.Orders;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public class MybatisOrders implements Orders {

private final OrderMapper orderMapper;

private final OrderDomainMapper mapper;

public MybatisOrders(OrderMapper orderMapper, OrderDomainMapper mapper) {
this.orderMapper = orderMapper;
this.mapper = mapper;
}

@Override
public Order save(Order order) {
OrderDO orderDO = mapper.toDO(order);
orderMapper.insertOrUpdate(orderDO);
return order;
}

@Override
public Optional<Order> findById(OrderId id) {
return Optional.ofNullable(orderMapper.selectById(id.value()))
.map(mapper::toDomain);
}
}

这种模式适合你想保留 MyBatis-Plus 的工程效率,同时又不想让领域层到处都是 @TableNameBaseMapperLambdaQueryWrapper

十七、常见反模式

17.1 只贴注解,不写领域行为

错误示例:

1
2
3
4
5
6
7
8
9
10
11
@AggregateRoot
public class Order {
@Identity
private Long id;
private Integer status;
private BigDecimal amount;

public void setStatus(Integer status) {
this.status = status;
}
}

这只是“带注解的贫血模型”。

更好的方式是把业务规则放进聚合行为:

1
2
3
4
5
6
public void submit() {
if (lines.isEmpty()) {
throw new IllegalStateException("订单明细不能为空");
}
this.status = OrderStatus.SUBMITTED;
}

17.2 为了 DDD 把所有东西都拆成值对象

不是每个字段都必须建模成值对象。

适合值对象的通常是:

1
2
3
4
5
6
金额 Money
地址 Address
手机号 PhoneNumber
邮箱 Email
时间范围 DateRange
业务编号 BillNo

不一定需要值对象的:

1
2
3
普通备注 remark
简单展示名称 displayName
不承载规则的普通字段

值对象的价值是承载规则,不是增加仪式感。

17.3 聚合之间直接互相引用

错误:

1
2
3
4
5
public class Settlement {
private Order order;
private Customer customer;
private Payment payment;
}

更推荐:

1
2
3
4
5
public class Settlement {
private OrderId orderId;
private CustomerId customerId;
private PaymentId paymentId;
}

聚合之间不要互相套娃,不然最后不是 DDD,是俄罗斯套娃挑战赛。

17.4 领域层依赖 Spring、MyBatis、JPA

领域层尽量不要依赖:

1
2
3
4
5
6
7
8
org.springframework.web
org.springframework.data
org.mybatis
jakarta.persistence
javax.persistence
redis client
http client
mq client

领域层应该表达业务规则,不应该知道技术细节。

17.5 把 jMolecules 当代码生成器

jMolecules 不是 Lombok,也不是 MyBatis Generator。它的第一价值是表达架构语义。

如果你期望它“一键生成 DDD 项目”,那会失望。

十八、老项目如何渐进式引入 jMolecules

不要一上来全量改造。比较稳的路径是:

第一步:只标记核心领域对象

先选一个最核心的模块,比如订单、结算、支付。

1
2
3
4
5
Order -> @AggregateRoot
OrderLine -> @Entity
Money -> @ValueObject
Orders -> @Repository
OrderSubmitted -> @DomainEvent

不要全项目撒注解。注解撒太多,最后就像日志打太多一样,失去信号价值。

第二步:补 ArchUnit 规则

先加最基础的规则:

1
2
3
4
domain 不依赖 infrastructure
controller 不直接访问 mapper
聚合之间不直接引用对象
@AggregateRoot / @Entity 必须有 @Identity

第三步:CI 中执行架构测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
name: build

on:
push:
branches: [ "main", "master" ]
pull_request:
branches: [ "main", "master" ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Build
run: mvn -B clean verify

第四步:从 warning 规则过渡到 blocking 规则

老项目问题多,不建议第一天就让 CI 全红。

可以分阶段:

1
2
3
4
阶段 1:只扫描,不阻断
阶段 2:新增代码必须符合规则
阶段 3:核心模块阻断
阶段 4:全项目阻断

第五步:配合 Spring Modulith 做模块化治理

当类级别 DDD 角色比较清楚后,再做模块边界治理:

1
2
3
order 模块不能访问 payment.internal
settlement 只能调用 order 暴露 API
模块之间通过事件解耦

这条路线比“上来就拆微服务”稳得多。

十九、jMolecules、ArchUnit、Spring Modulith 怎么选?

场景 推荐工具
想表达 DDD 角色 jMolecules
想检查架构规则 ArchUnit
想治理模块化单体 Spring Modulith
想让模块图/文档更清晰 Spring Modulith + jMolecules
想检查聚合之间引用是否合理 jMolecules + ArchUnit
想防止 Controller 直连 Mapper ArchUnit
想让领域模型脱离技术框架 jMolecules + 架构规则

简单说:

1
2
3
jMolecules:表达“它是什么”
ArchUnit:检查“它有没有违规”
Spring Modulith:治理“模块之间怎么相处”

这三个组合起来,非常适合中大型 Spring Boot 单体项目和准备从单体走向模块化单体的项目。

二十、生产落地建议

20.1 新项目建议

新项目可以直接采用:

1
2
3
4
5
6
7
1. package 按业务模块划分
2. 模块内按 api / application / domain / infrastructure 分层
3. domain 使用 jMolecules DDD 注解
4. 关键值对象使用 record
5. 聚合之间只引用 ID
6. ArchUnit 加基础规则
7. Spring Modulith 验证模块边界

推荐依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-ddd</artifactId>
<version>2.0.1</version>
</dependency>

<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-layered-architecture</artifactId>
<version>2.0.1</version>
</dependency>

<dependency>
<groupId>org.jmolecules.integrations</groupId>
<artifactId>jmolecules-archunit</artifactId>
<version>0.33.0</version>
<scope>test</scope>
</dependency>

20.2 老项目建议

老项目不要一刀切。

推荐策略:

1
2
3
4
5
6
1. 先选一个核心模块做样板
2. 只给核心领域对象加 jMolecules 注解
3. 保留原有 Spring / MyBatis / JPA 运行方式
4. 用 ArchUnit 检查新增代码
5. 慢慢拆掉 domain 对 infrastructure 的依赖
6. 再考虑 Spring Modulith 模块边界

20.3 团队协作建议

jMolecules 落地失败通常不是技术问题,而是团队共识问题。

需要先约定:

1
2
3
4
5
6
7
8
9
什么是聚合根
什么是实体
什么是值对象
仓储接口放哪里
DTO 能不能进入 domain
跨聚合能不能直接引用对象
领域事件怎么命名
哪些规则 CI 阻断
哪些规则只提示

没有这些约定,注解只会变成新的装饰品。

二十一、完整架构图

flowchart TB
    subgraph OrderModule[订单模块 order]
        API[api\nOrderFacade / Command]
        APP[application\nOrderApplicationService]
        DOMAIN[domain\nOrder / OrderLine / Money / Orders]
        INFRA[infrastructure\nJpaOrders / Mapper / DO]
    end

    subgraph PaymentModule[支付模块 payment]
        PAYAPP[application\nPaymentApplicationService]
        PAYDOMAIN[domain\nPayment / PaymentId]
    end

    API --> APP
    APP --> DOMAIN
    INFRA --> DOMAIN
    APP --> INFRA
    DOMAIN --> EVENT[OrderSubmitted DomainEvent]
    EVENT --> PAYAPP
    PAYAPP --> PAYDOMAIN

    ArchUnit[ArchUnit Rules] -.校验.-> DOMAIN
    JM[jMolecules] -.标记架构语义.-> DOMAIN
    SM[Spring Modulith] -.验证模块边界.-> OrderModule
    SM -.验证模块边界.-> PaymentModule

这个图里的关键不是箭头,而是约束:

1
2
3
4
5
6
7
API 不能绕过 APP 直接访问 INFRA
DOMAIN 不能依赖 INFRA
Order 不能直接引用 Payment 聚合对象
Payment 通过事件感知订单提交
ArchUnit 负责自动检查规则
jMolecules 负责让代码具备架构语义
Spring Modulith 负责模块边界治理

二十二、最终代码检查清单

你可以拿下面这份清单检查自己的项目。

DDD 角色

1
2
3
4
5
6
[ ] 聚合根是否标注 @AggregateRoot
[ ] 实体是否标注 @Entity
[ ] 值对象是否标注 @ValueObject
[ ] 聚合根和实体是否有 @Identity
[ ] 仓储接口是否标注 @Repository
[ ] 领域事件是否标注 @DomainEvent

聚合设计

1
2
3
4
5
[ ] 聚合是否有明确业务行为
[ ] 聚合是否维护自己的内部一致性
[ ] 聚合之间是否只通过 ID 或事件协作
[ ] 聚合内部集合是否避免直接暴露可变引用
[ ] 值对象是否不可变

分层设计

1
2
3
4
5
[ ] domain 是否不依赖 infrastructure
[ ] domain 是否不依赖 controller / web
[ ] domain 是否不依赖 ORM / MyBatis 技术类
[ ] application 是否只做用例编排
[ ] infrastructure 是否负责技术适配

架构自动化

1
2
3
4
[ ] 是否有 jMolecules + ArchUnit 架构测试
[ ] 是否在 CI 中执行架构测试
[ ] 是否有模块边界验证
[ ] 是否对新增代码强制执行规则

启示录

jMolecules 的价值不在于“多几个注解”,而在于让代码表达架构意图。

很多项目的问题不是没有架构,而是架构和代码断裂了:

1
2
3
4
5
6
文档里是 DDD
代码里是事务脚本
图上是分层架构
代码里是互相乱调
会议上说模块边界
实现里直接跨包访问 internal

jMolecules 的思路是把这些架构意图重新拉回代码里,让代码本身具备说明能力。

我比较推荐的落地组合是:

1
2
3
jMolecules:表达架构语义
ArchUnit:执行架构规则
Spring Modulith:治理模块化单体

这套组合特别适合中大型 Spring Boot 项目。它不会自动让项目变好,但它能让架构问题更早暴露出来。就像体检报告不会替你健身,但至少能告诉你哪里快爆了。

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。

参考资料


jMolecules 深度实战:让 DDD 与架构规则真正写进 Java 代码
https://allendericdalexander.github.io/2026/06/12/archtect/arch/jmolecules-practice-blog/
作者
AtLuoFu
发布于
2026年6月12日
许可协议