欢迎你来读这篇博客,这篇博客主要是关于 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 > <dependency > <groupId > org.jmolecules.integrations</groupId > <artifactId > jmolecules-spring</artifactId > <version > ${jmolecules.integrations.version}</version > </dependency > <dependency > <groupId > org.jmolecules.integrations</groupId > <artifactId > jmolecules-jackson</artifactId > <version > ${jmolecules.integrations.version}</version > </dependency > <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> { private Customer customer; }
更推荐:
1 2 3 4 5 public class Order implements AggregateRoot <Order, OrderId> { 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 () { } }
这种方式虽然多了 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; private Customer customer; }
如果 Customer 也是聚合根,jMolecules + ArchUnit 就可以发现这种危险依赖。
更合理的写法:
1 2 3 4 5 6 7 8 9 @AggregateRoot public class Order { @Identity private OrderId 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,它会让单字段 @ValueObject 和 Identifier 的序列化更自然。
依赖:
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.AggregateRootpublic class Order { @jakarta .persistence.Id @org .jmolecules.ddd.annotation.Identity private UUID id; }
优点:
缺点:
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; }
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 的工程效率,同时又不想让领域层到处都是 @TableName、BaseMapper、LambdaQueryWrapper。
十七、常见反模式 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 项目。它不会自动让项目变好,但它能让架构问题更早暴露出来。就像体检报告不会替你健身,但至少能告诉你哪里快爆了。
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。
参考资料