首页/新闻资讯/正文详情

jMolecules与ArchUnit实战:DDD注解、架构规则与编译期织入

发布时间:2026/11/4 5:05:09 来源:云帆数科 栏目:资讯中心
jMolecules与ArchUnit实战:DDD注解、架构规则与编译期织入
我第一次接触 jMolecules 的时候第一反应是这不就是给领域模型贴标签吗能有多大用。真正在一个中型项目里把它跑完一轮之后观点变了。它解决的从来不是怎么写出一个聚合根而是怎么让领域模型在代码里表达出真实的架构意图并且让这个意图可以被机器校验、被框架识别。这两件事靠口头约定和评审是守不住的。这篇内容围绕 jMolecules 这套注解体系展开讲的是怎么用注解把 DDD 的战术模式落到 Java 代码里怎么用 ArchUnit 把架构约束变成 CI 里会红的规则怎么用编译期织入让业务代码不依赖具体的持久化框架。适合已经写过一些 DDD 代码、但总觉得形似神不似的同学也适合正在纠结领域层要不要引入 JPA 注解的团队参考。全文基于常见的 1.x 版本实践展开具体 API 签名以你实际引入的依赖源码为准。1. jMolecules 到底在解决什么把领域意图从框架里剥出来1.1 一个典型困境注解写满了架构还是散的先看一个特别常见的领域层写法。一个订单聚合根类头上挂着Entity主键字段挂着Id和GeneratedValue某个集合字段挂着OneToMany(cascade CascadeType.ALL)类底下还压着 Lombok 的Data。代码能跑数据库也能建表但问题在于这个类现在同时承担了三件事——它是领域概念的表达、是持久化映射的载体、还是序列化的数据容器。三层职责压在一个类上代价会在项目变大的时候一点点暴露出来。想换个存储实现得把领域类的注解全翻一遍想给领域层写纯单元测试得先拉起一个 ORM 上下文想校验聚合之间不能直接持有对象引用这条规矩没有任何工具能在编译期或测试期帮你拦一下全靠人眼。更麻烦的是新人进来看到满屏的框架注解根本分不清哪些字段是业务真正关心的哪些只是为了凑映射。这就是 jMolecules 想撬动的那个点。它的核心主张很朴素领域模型应该用一套与框架无关的词汇来描述自己。你是聚合根就是聚合根是值对象就是值对象这个身份不该由Entity来定义。至于这个语义最终怎么翻译成数据库表、怎么翻译成事件总线上的消息那是基础设施层的事交给集成模块去桥接。所以它不是又一个 DDD 脚手架也不是代码生成器它更像一层语义中间层往上业务代码只依赖几行注解往下各种集成模块负责把这些注解翻译成具体框架需要的东西。翻译可以发生在编译期字节码织入也可以发生在测试期ArchUnit 规则校验还可以发生在运行期Spring Data 的元注解识别。1.2 jMolecules 的三层分工把这套东西拆开看它其实由几个相对独立的部分组成理解这个分工比记住注解名更重要。模块作用位置主要职责jmolecules-ddd源码层提供 DDD 战术模式的注解与少量类型接口jmolecules-archunit测试层把注解语义转成可执行的架构规则jmolecules-integrations编译期/运行期与持久化、事件、依赖注入等框架桥接jmolecules-bytebuddy编译期字节码织入自动补齐 equals/hashCode、映射注解等这个分层带来的直接好处是依赖倒置得比较干净。领域模块的pom.xml里只需要一个jmolecules-ddd它的传递依赖极少几乎不会污染你的类路径。持久化相关的集成依赖放在基础设施模块里领域模块对它一无所知。我在实际项目里踩过一个相关的坑值得提前说jMolecules 同时提供了注解和类型接口两套表达方式包名分别是org.jmolecules.ddd.annotation和org.jmolecules.ddd.types。比如聚合根既可以写AggregateRoot注解也可以implements AggregateRootOrder, OrderId接口。这两者不是替代关系语义上略有差别混用的时候最容易出现导入错包的情况——IDE 自动补全一顺手导进来的是注解包里的同名类编译能过但 ArchUnit 规则匹配不到你会对着一堆规则明明写了却不生效发半天呆。我后来的做法是在领域模块里统一用注解表达语义类型接口只在确实需要泛型约束比如仓储泛型参数的地方使用团队内写进代码规范。2. 把战术模式翻译成注解核心原语的语义边界2.1 聚合根、实体、值对象三者不能混着用DDD 的战术模式里最容易含糊的就是这三个概念。很多人写出来的值对象其实是个带 setter 的 POJO写出来的实体其实没有身份标识写出来的聚合根其实只是张表。jMolecules 的价值在于它逼你把这三个词选清楚因为选完之后规则会来检查你选得对不对。聚合根Aggregate Root是一致性与事务的边界。它有全局唯一的标识外部只能通过它来访问聚合内部的对象。在 jMolecules 里用AggregateRoot标注同时它的标识字段要用Identity标出来这样字节码织入才知道该基于哪个字段生成equals/hashCode。实体Entity有身份、生命周期内可变但它的身份只在聚合内部有意义。典型例子是订单里的订单行每一行有自己的行号但你不会拿行号去全局查一条订单行。用Entity标注。值对象Value Object没有身份靠属性值判等天然应该不可变。用ValueObject标注。Java 17 之后首选record因为它把不可变和值语义都帮你做了。这三者的边界一旦在代码里显式化很多设计问题会自动浮现。我见过一个很典型的反例某团队的金额类被标成了Entity因为它有一个自增主键。这就是把持久化实现当成了领域语义——金额这种东西换了任何存储都不需要主键它就是个值对象主键是映射层的产物不该反向污染模型。2.2 仓储、领域事件、领域服务、模块的标注规矩仓储Repository在 jMolecules 里用Repository标注约定是接口而非实现。这一条非常关键仓储的抽象属于领域层实现属于基础设施层。用接口表达领域层就只依赖自己的抽象具体的 JPA 实现、MyBatis 实现、内存实现可以随便换。ArchUnit 里有对应的规则可以强制仓储必须是接口CI 里直接拦住那些图省事写成类的做法。领域事件Domain Event用DomainEvent标注。事件对象应该是不可变的字段全部 final理想状态是用record。它表达的是领域里发生了一件已经发生的事所以命名用过去式字段是事件的完整快照不要塞聚合根引用进去——那样事件就变成了对可变状态的悬挂指针序列化出去之后语义就变了。领域服务Domain Service用Service标注指的是那种跨多个聚合、又不适合塞进任何一个聚合的无状态操作。要注意这个Service是 jMolecules 的不是依赖注入框架那个。它们的语义确实相近但前者是架构语义后者是容器语义理论上可以叠加使用。模块Module这块容易被忽略。jMolecules 提供了模块化表达的方式可以用包级别的package-info.java声明也可以在类型上标注。模块化的意义在于把限界上下文这个偏概念的东西落成 ArchUnit 能检查的实实在在的边界。2.3 容易被忽略的成员类注解Identity、Member、Reference这三个注解是让整套体系活起来的关键也是最容易漏掉的。Identity标在聚合根或实体的标识字段上。它的作用不止是标记更重要的是给字节码织入提供依据——织入器会根据这个字段生成正确的equals/hashCode让两个从数据库里分别加载出来的同一个订单对象能判等。没有它你就得自己手写还得保证所有实体写法一致。Member用在值对象或实体的组成字段上告诉织入器这个字段参与值语义计算。比如一个Money值对象里有金额和币种两个字段都标上Member织入器生成equals的时候就会把它们都算进去。Reference用于跨聚合的引用。DDD 的一条硬规矩是聚合之间只通过标识引用不持有对方的对象引用。这条规矩靠人守很难靠规则守就容易多了——Reference就是在代码里显式声明我这里引用的是另一个聚合的 ID不是一个对象。ArchUnit 能据此检查你有没有偷偷把对方聚合根塞进来。注解标在哪典型误用Identity聚合根/实体的标识字段标在值对象的普通字段上Member值对象的组成字段用在可变字段上导致判等不稳定Reference跨聚合的引用字段引用的目标是聚合内部实体而非聚合根3. 动手搭一个订单域从依赖到可运行的骨架3.1 依赖引入与包结构约定先看依赖。领域模块只需要核心注解包版本号以你实际使用的发布版为准dependency groupIdorg.jmolecules/groupId artifactIdjmolecules-ddd/artifactId version1.10.0/version /dependency测试模块里加架构规则依赖dependency groupIdorg.jmolecules.integrations/groupId artifactIdjmolecules-archunit/artifactId version1.10.0/version scopetest/scope /dependency包结构我建议按一个聚合一个包来切而不是按entity/service/repository这种技术分层。前者天然对应限界上下文里的聚合边界后者切完之后聚合内部的对象被打散到不同包里模块化规则根本写不出来。com.example.shop.order ├── Order.java // 聚合根 ├── OrderId.java // 标识值对象 ├── OrderLine.java // 聚合内实体 ├── OrderStatus.java // 枚举 ├── OrderRepository.java // 仓储接口 └── events └── OrderPlaced.java // 领域事件这个结构看起来平平无奇但它让哪些东西属于同一个聚合在物理结构上一目了然。后面写 ArchUnit 规则的时候你会感谢当初这个决定。3.2 值对象与标识的写法先写标识和值对象。用record是最省事的不可变性、值语义、equals/hashCode全都是语言层面保证的package com.example.shop.order; import org.jmolecules.ddd.annotation.ValueObject; ValueObject public record OrderId(String value) { public OrderId { if (value null || value.isBlank()) { throw new IllegalArgumentException(订单号不能为空); } } public static OrderId of(String value) { return new OrderId(value); } }这里有几个细节值得展开。构造函数里的校验是必须的——值对象一旦创建出来就必须是合法的这是它区别于普通 POJO 的核心特征。static of(...)工厂方法不是必须的但强烈建议保留jMolecules 的Identifier类型接口约定了一个of(...)形式的工厂字节码织入在有需要的时候能顺着这个约定补齐创建逻辑即使你不用接口统一工厂方法也能让调用方的写法保持一致。再看一个复合值对象ValueObject public record Money(BigDecimal amount, String currency) { public Money { if (amount null || amount.signum() 0) { throw new IllegalArgumentException(金额不能为负); } Objects.requireNonNull(currency, 币种不能为空); } public Money add(Money other) { if (!currency.equals(other.currency)) { throw new IllegalArgumentException(币种不一致无法相加); } return new Money(amount.add(other.amount), currency); } }add方法返回新对象而不是修改自身这是值对象的标配写法。用 record 的时候这一点是强制的因为字段都是 final你想改也改不了。我个人的体会是一旦用惯了 record 写值对象就很难再回去了因为你不用再担心某个方法不小心改了状态导致隔了三个调用栈之后出现莫名其妙的数据。3.3 聚合根的内部一致性维护聚合根的写法要点在于所有对内部状态的修改都必须经过聚合根的方法并且这些方法要维护业务不变式。package com.example.shop.order; import org.jmolecules.ddd.annotation.AggregateRoot; import org.jmolecules.ddd.annotation.Identity; import java.util.ArrayList; import java.util.List; AggregateRoot public class Order { Identity private final OrderId id; private final String customerId; private final ListOrderLine lines; private OrderStatus status; private Order(OrderId id, String customerId) { this.id id; this.customerId customerId; this.lines new ArrayList(); this.status OrderStatus.CREATED; } public static Order create(OrderId id, String customerId) { return new Order(id, customerId); } public void addLine(String sku, int quantity, Money unitPrice) { if (status ! OrderStatus.CREATED) { throw new IllegalStateException(订单已提交不能再修改); } if (lines.stream().anyMatch(l - l.sku().equals(sku))) { throw new IllegalArgumentException(同一 SKU 不能重复添加); } lines.add(new OrderLine(sku, quantity, unitPrice)); } public void submit() { if (lines.isEmpty()) { throw new IllegalStateException(空订单不能提交); } this.status OrderStatus.SUBMITTED; } public OrderId getId() { return id; } public ListOrderLine getLines() { return List.copyOf(lines); } public OrderStatus getStatus() { return status; } }几点说明。构造函数私有、通过静态工厂创建这样做的好处是创建路径唯一所有的初始化逻辑都集中在一处。addLine里的两道校验就是聚合根存在的意义——如果把这些判断散到应用服务层一旦有第二个调用方就很容易漏掉其中一条。getLines()返回List.copyOf(...)而不是直接返回内部列表这是防御性拷贝。别小看这一行我见过太多聚合内部状态被外部偷偷改掉的 bug排查起来极其痛苦因为从调用栈上看不出任何问题。至于Identity标在id字段上带来的实际收益字节码织入之后这个类的equals/hashCode会基于id生成两个从不同查询里加载出来的同一订单对象能被正确判等塞进Set也不会出现重复。手写这段逻辑当然可以但一个项目里几十个实体你能保证每一处都写对吗我反正是保证不了。3.4 仓储与领域事件的落点仓储接口定义在领域层实现放在基础设施层package com.example.shop.order; import org.jmolecules.ddd.annotation.Repository; import java.util.Optional; Repository public interface OrderRepository { OptionalOrder findById(OrderId id); void save(Order order); void delete(Order order); }接口里的方法签名要用领域语言别出现selectByPrimaryKey这种。findById收的是OrderId值对象而不是String这层类型安全是免费的不用白不用。领域事件package com.example.shop.order.events; import org.jmolecules.ddd.annotation.DomainEvent; import java.time.Instant; DomainEvent public record OrderPlaced( String orderId, String customerId, Instant occurredAt ) {}事件里存String类型的 orderId 而不是OrderId对象这是我个人的偏好——事件要跨进程传输用最原始的类型序列化最省心也不容易在反序列化时踩到构造函数校验的坑。当然如果你的序列化方案能处理好值对象直接用OrderId也没问题。关于事件的发布时机一个常见的做法是在应用服务层收集聚合根产生的领域事件提交事务后再发布。不要在聚合根的方法里直接调用事件发布器那样聚合就依赖了基础设施前面辛苦建立的干净依赖关系一下就破了。4. 让架构规则跑起来ArchUnit 与 jMolecules 的组合拳4.1 现成规则集的用法注解写得再规范没人检查也是白搭。jMolecules 的 ArchUnit 集成模块提供了一批开箱即用的规则把注解语义翻译成了可执行的断言。典型用法是写一个架构测试类import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.lang.ArchRule; import org.jmolecules.archunit.JMoleculesDddRules; AnalyzeClasses(packages com.example.shop) class DddArchitectureTest { ArchTest static final ArchRule ddd_rules JMoleculesDddRules.all(); }JMoleculesDddRules里包含的规则大致覆盖这几个方向聚合之间的关系是否只通过标识、领域事件是否不可变、仓储是否声明为接口、值对象是否真的不可变、实体是否有标识。具体的方法名和覆盖范围会随版本演进建议引入依赖后直接翻一下这个类的源码比自己猜要快得多。这里有个实践建议先不要一次性把all()全打开。在一个存量的、注解覆盖不全的项目里直接上全套规则CI 会红成一片团队第一反应是把这堆测试删掉那就适得其反了。我的做法是分三步走——第一步只开仓储必须是接口这种低风险高收益的规则先把结构性的问题修掉第二步开聚合引用相关的规则这块需要改设计周期长一点第三步再开值对象不可变这类细粒度规则。4.2 自己写规则的三种常见模式现成规则不可能覆盖你的所有约定自定义规则是必须的。我总结了三种最常用的模式。第一种依赖方向约束。这是最基础也最有价值的规则拦住的是领域层反向依赖基础设施这种结构性错误。ArchTest static final ArchRule 聚合不应依赖仓储 noClasses() .that().areAnnotatedWith(AggregateRoot.class) .should().dependOnClassesThat() .areAnnotatedWith(Repository.class);第二种命名与标注的一致性。这类规则用来防止语义漂移——比如有人写了个聚合根但忘了加注解或者加注解的类名字一看就不对。ArchTest static final ArchRule 聚合根应位于聚合包根部 classes() .that().areAnnotatedWith(AggregateRoot.class) .should().resideInAPackage(..order..) .orShould().resideInAPackage(..customer..) .orShould().resideInAPackage(..product..);第三种跨聚合引用的收紧。这是 DDD 落地里最容易破防的一条也是最值得用规则守住的。ArchTest static final ArchRule 聚合之间不得持有对象引用 noClasses() .that().areAnnotatedWith(AggregateRoot.class) .should().dependOnClassesThat() .areAnnotatedWith(AggregateRoot.class) .andShould().notDependOnClassesThat() .areAssignableTo(Object.class) .because(聚合之间只允许通过标识引用不允许直接持有对方聚合根对象);注意最后这条规则的写法有点绕实际写的时候通常会配合Reference注解做更精确的判定允许依赖对方的值对象比如CustomerId但禁止依赖对方的聚合根类本身。这个粒度需要根据你的项目结构调整。4.3 规则失败后的排查路径ArchUnit 的失败信息有时候不太友好尤其是涉及传递依赖的时候。分享一个我常用的排查顺序。先看失败信息里给出的完整类路径确认被抓住的到底是哪个类。很多时候问题不在被标注的那个类上而在于它的某个字段的类型引起的传递依赖。比如你禁止聚合依赖仓储结果某个聚合根里有个方法返回了ListOrderRepository这就是个隐藏的违例。然后去确认注解有没有生效。前面提过注解包和接口包混用的问题ArchUnit 按注解匹配的时候导错包就直接匹配不上。我的习惯是在架构测试里加一条自检规则ArchTest static final ArchRule 至少存在一个聚合根 classes() .that().areAnnotatedWith(AggregateRoot.class) .should().haveSimpleNameEndingWith(Order) .allowEmptyShould(true);用allowEmptyShould(true)配合一条宽泛的规则如果匹配数量是零说明注解根本没被扫到问题在扫描路径或者注解导入上而不是规则本身写错了。这一步能省掉大量自我怀疑的时间。最后是性能问题。ArchUnit 的类扫描在大项目上并不便宜几百个模块的话跑一次可能要好几分钟。可以配置缓存AnalyzeClasses( packages com.example.shop, importOptions ImportOption.DoNotIncludeTests.class, cacheMode CacheMode.PER_CLASS )CacheMode.PER_CLASS在本地开发时效果明显CI 上一般用默认值就好。另外记得把测试类排除掉否则测试里的 Mock 对象会被当成真实类参与分析产生一堆莫名其妙的违例。5. 编译期织入ByteBuddy 把注解变成真实行为5.1 织入插件配置前面的注解本质上只是元数据它们自身不产生任何运行时行为。真正让Identity变成自动生成 equals/hashCode的是编译期织入这个环节。Maven 项目的配置大概长这样plugin groupIdnet.bytebuddy/groupId artifactIdbyte-buddy-maven-plugin/artifactId version1.14.9/version executions execution goals goaltransform/goal /goals /execution /executions configuration transformations transformationorg.jmolecules.bytebuddy.JMoleculesPlugin/transformation /transformations /configuration /pluginGradle 项目的做法类似用byteBuddy { transformation { plugin.set(...) } }配置。具体插件坐标和 transformation 类名以你所用版本为准配置前建议先确认一下依赖包里到底有哪些插件类。这里有个坑要提前说这个插件的执行时机在compile之后、package之前它处理的是编译产物的字节码。也就是说你在 IDE 里直接点运行的时候如果 IDE 没有走完整的 Maven/Gradle 生命周期织入是没有发生的。源码里看起来一切正常但equals行为跟 CI 上构建出来的包不一样。我在一个项目里被这个问题坑了整整半天本地测试全绿打包上线之后判等逻辑挂了。5.2 织入到底改了什么理解织入做了哪些事比记住配置更重要。概括起来主要是三类改造。第一类是补齐对象语义方法。带Identity的类会得到基于标识字段的equals/hashCode带Member的类会得到基于成员字段的版本带ValueObject的类会得到更完整的toString。这类改造的价值在于一致性。手写的话你可能在这个类里基于 id 判等在另一个类里基于全部字段判等第三个类忘了写hashCode。这些问题单看代码都不明显出问题的时候往往是在Set去重、Map查值、缓存 key 这些地方现象诡异排查成本极高。第二类是注入框架注解。这是我觉得最有意思的一部分jMolecules 可以把 JPA 的Entity、Embeddable、Id这些注解在字节码层面补到你的领域类上。也就是说你的源码里一个 JPA 注解都没有但字节码里该有的都有ORM 框架照样能正确识别。这一步的意义重大。领域层的源码从此对持久化框架完全无感你可以拿同样的领域模型去适配不同的存储方案也可以在纯单元测试里直接 new 一个聚合根不需要任何 ORM 上下文。第三类是方法生成。比如给值对象补of(...)工厂方法给Identifier实现类补标准方法等。这部分用得相对少但涉及的时候能省不少样板代码。5.3 与持久化框架的桥接织入完成之后桥接就是水到渠成的事。以 JPA 为例你需要在基础设施层提供一个仓储实现package com.example.shop.infrastructure.persistence; import org.springframework.stereotype.Component; import com.example.shop.order.*; Component public class JpaOrderRepository implements OrderRepository { private final OrderEntityRepository entityRepository; public JpaOrderRepository(OrderEntityRepository entityRepository) { this.entityRepository entityRepository; } Override public OptionalOrder findById(OrderId id) { return entityRepository.findById(id.value()).map(this::toDomain); } Override public void save(Order order) { entityRepository.save(toEntity(order)); } Override public void delete(Order order) { entityRepository.deleteById(order.getId().value()); } private Order toDomain(OrderRecord record) { // 组装领域对象 } private OrderRecord toEntity(Order order) { // 转成持久化对象 } }看到这里可能有人会问既然字节码能自动注入 JPA 注解为什么还要写这层转换答案是取决于你的取舍。如果你选择让领域对象直接由 ORM 管理也就是织入方案那确实不需要转换层Order类本身就是实体类OrderRepository直接继承 Spring Data 的接口就行。但如果你对领域模型的纯净度要求很高比如需要延迟加载、需要对聚合做复杂的重建逻辑那加一层转换更稳妥。两种方案我都用过。织入方案的代码量少得多对中小项目非常合适转换层方案更灵活但样板代码会显著增加需要配套的对象映射工具。选哪个主要看你团队对领域层零框架依赖这条底线的坚持程度。Spring Data 的适配还有个细节jMolecules 的Repository注解在集成模块的支持下能被识别为仓储标记从而让 Spring Data 的接口扫描正常工作。如果你的项目里已经有一堆继承JpaRepository的接口迁移的时候可以逐步替换不必一次性全改。6. 上线前最容易翻车的几个点6.1 record 与织入的冲突这是我在实际项目里遇到的第一个真问题。用record写值对象非常舒服但如果你同时想用字节码织入去生成equals/hashCode会发现织不进去。原因是record的equals/hashCode是final的字节码层面改不了。好消息是这通常不构成问题——record自带的值语义本来就是正确的你根本不需要织入。真正需要警惕的是混合场景一个类用record写了但又在里面标了Identity这时候你会以为标识语义生效了实际上并没有record 的判等还是基于全部字段。处理原则很简单record 就老老实实做值对象需要身份语义的类用普通类写。两者别混。我现在的习惯是只要这个类需要被持久化框架管理、需要参与聚合的一致性维护就不用 record纯粹的数据载体、DTO、事件对象一律用 record。6.2 IDE 与 CI 的行为差异前面提过一次这里展开说。字节码织入带来的最大困扰就是本地好使、打包不好使或者反过来。根源在于 IDE 的增量编译通常不触发完整的插件执行链。你改了代码IDE 编译了但没跑 ByteBuddy 插件于是运行时的字节码和 CI 构建产物不一致。几个应对办法。第一把织入相关的行为验证放到集成测试里而不是单元测试里确保测试跑的是走完整构建流程的产物。第二在团队里统一构建方式本地验证重要改动时用命令行跑一次完整构建别只信 IDE。第三如果条件允许用 IDE 的委托构建给 Maven/Gradle功能让 IDE 复用构建工具的编译流程虽然会慢一点但一致性有保障。我个人现在的做法是涉及聚合根判等、领域事件序列化这类跟织入强相关的逻辑一定要在 CI 上跑集成测试覆盖本地跑通不算数。6.3 规则集失控与性能架构规则的价值在于长期守护但如果规则写得太多太细维护成本会反噬。我见过一个项目最后积累了两百多条 ArchUnit 规则跑一次架构测试要八分钟开发者提交前根本不愿意本地跑全靠 CI 兜底反馈周期长得离谱。而且规则之间互相冲突的情况时有发生改一条挂三条。控制规模的经验是规则要跟着架构决策走而不是跟着代码现状走。每加一条规则先问一句这条规则守住的是哪个设计决策如果它不存在会发生什么坏事答不上来的规则就删掉。另外善用.because(...)把决策原因写清楚半年后回头看的时候你会感谢自己。性能上还有个小技巧把架构测试按关注点拆成多个测试类用AnalyzeClasses的包范围做区分这样改某个模块的时候只需要跑对应那部分规则。整体上比一个巨型测试类要快失败信息也更容易定位。6.4 版本对齐与依赖管理最后一个务实的问题。jMolecules 的各个模块核心注解、ArchUnit 集成、ByteBuddy 集成、各框架适配版本需要保持一致混用不同版本很容易出现注解在但规则不认或者插件跑但什么都不做的情况。建议在父 POM 里用dependencyManagement统一管理版本properties jmolecules.version1.10.0/jmolecules.version /properties dependencyManagement dependencies dependency groupIdorg.jmolecules/groupId artifactIdjmolecules-bom/artifactId version${jmolecules.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement如果项目里同时有多个模块依赖不同版本用mvn dependency:tree查一下有没有版本冲突这个排查动作在引入新集成模块的时候一定要做一次。我遇到过一次 ArchUnit 规则完全不生效的情况最后查出来是传递依赖带进来了一个旧版本的注解包类加载的时候优先命中了旧版本注解类型对不上规则自然匹配不到。这类问题靠读代码是发现不了的只能靠依赖树。引入 jMolecules 这件事我个人最大的体会是它的收益不在写代码的那几天而在项目跑到第二年的时候。前期的注解加得规不规范当时看不出差别等到要换存储、要拆分模块、要新人接手的时候那些能用机器校验的架构约束价值才会真正显现出来。所以如果准备上建议一开始就把注解规范和架构测试一起立起来别只加注解不写规则——那样的话这套东西就真的只是给类贴了几个标签而已。

相关推荐

Agent开发必懂:Skill与Tool的区别及技能库自演进与动态加载
Agent开发必懂:Skill与Tool的区别及技能库自演进与动态加载

我们做 Agent 开发的时间一长,几乎都会被同一个问题卡住:Skill 和 Tool 到底有什么区别?很多刚入门的同学把两者混为一谈,甚至直接在 Agent 里把几百个 API 封装成“技能”塞进去,结果上下文爆炸、指令选择困难、维护成… · 2026/11/4 5:04:08

排序算法:从经典到现代的工程实践与优化
排序算法:从经典到现代的工程实践与优化

1. 排序算法概述与核心价值排序算法是计算机科学中最基础也最重要的算法类别之一。从早期的冒泡排序到现代机器学习中的排序网络,排序算法的发展几乎贯穿了整个计算机发展史。在实际工程中,排序操作占用了大量计算资源,根据Google的研究&… · 2026/11/4 5:04:08

调节性T细胞(Treg)研究全攻略:从表型鉴定到体外扩增实操
调节性T细胞(Treg)研究全攻略:从表型鉴定到体外扩增实操

说起 treg 这个缩写,在免疫学圈子里没人会觉得陌生——调节性 T 细胞(Regulatory T cell)。没进实验室之前,我也以为这只是教科书上“抑制免疫反应”的几句话,直到真正上手做分离、做培养、做功能实验,才理… · 2026/11/4 5:04:08

hindsight:让Agent学会事后复盘的记忆工具
hindsight:让Agent学会事后复盘的记忆工具

1. 为什么“事后复盘”这件事值得单独做一个工具做Agent开发的人都有一个共同的痛点:Agent跑完一轮任务,你问它“刚才为什么选了那个方案”,它要么胡编一个理由,要么干脆把上下文忘干净了。这不是模型不够聪明,而是记忆… · 2026/11/4 10:45:21

隐马尔可夫模型原理与工程实践全解析
隐马尔可夫模型原理与工程实践全解析

1. 隐马尔可夫模型基础概念解析 隐马尔可夫模型(Hidden Markov Model, HMM)作为序列数据分析的重要工具,在语音识别、生物信息学、自然语言处理等领域有着广泛应用。我第一次接触HMM是在研究生时期的语音识别课程上,当时就被它&qu… · 2026/11/4 10:45:21

机房供配电系统合规性要求与运维实践
机房供配电系统合规性要求与运维实践

1. 机房供配电合规性:企业不可忽视的生命线上周刚处理完某金融企业因UPS电池组温度超标被勒令整改的案例,再次印证了机房供配电系统合规性绝不是"应付检查"的表面功夫。去年某电商平台就因柴油发电机油路系统缺陷导致灾备失效,直接… · 2026/11/4 10:45:21

Model-Optimizer:面向ONNX模型的结构性冗余精准消除工具
Model-Optimizer:面向ONNX模型的结构性冗余精准消除工具

1. 这不是“一键加速”,而是模型瘦身的手术刀——Model-Optimizer到底在解决什么问题? Model-Optimizer这个词最近在技术社区里频繁刷屏,但很多人点开项目页第一反应是:“这又是个包装精美的GUI工具?”其实完全不是。我… · 2026/11/4 10:45:21

博士论文的引用句,直接引和间接引分开改,硕词AI(官方网站:www.shuociai.com)给了两类改法
博士论文的引用句,直接引和间接引分开改,硕词AI(官方网站:www.shuociai.com)给了两类改法

博士论文降重,最容易出问题的就是引用句。直接引、间接引混着改,结果是:该保留的原话被你改了意思,该转述的段落却原样照搬。降重前先把引用句分成两类,分开改,效率和正确率都高得多。博士论文动辄几百条引… · 2026/11/4 10:45:21

无刷电机AX/BY/CZ绕组命名与120度电角度解析
无刷电机AX/BY/CZ绕组命名与120度电角度解析

1. 一句话讲清楚:AX/BY/CZ到底在说什么先把结论放前面:直流无刷电机里常说的“AX/BY/CZ”,本质上不是三条互相垂直的轴线,而是三组绕组的编号方式。A、B、C代表三相,X、Y、Z代表对应的绕组末端。所谓“怎么划分”&… · 2026/11/4 10:44:20

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38

AI辅助技术栈评估:提升研发决策效率60%
AI辅助技术栈评估:提升研发决策效率60%

1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17

从生成Token到直接决策:Jev如何重塑AI Agent工程实践
从生成Token到直接决策:Jev如何重塑AI Agent工程实践

1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17

32G统一内存跑大模型:内存账、TPS实测与端云分流
32G统一内存跑大模型:内存账、TPS实测与端云分流

想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码