Spring Modulith: 模块管理高级篇

178 阅读5分钟

通过上节内容,我们了解了 Spring Modulith 默认的模块封装机制:位于模块内部子包(如 internal)的类型默认是不可见的,外部模块无法直接访问,从而保证了模块的内聚性。例如,在我们的示例中,orderService 默认情况下是无法直接访问 notification 模块的 internal 包下的 NiujiaoNotificationService 的。

但在某些特定场景下,我们可能需要更灵活地控制模块的访问权限,或者需要绕过默认的封装限制。Spring Modulith 提供了 开放应用程序模块 (Open Application Modules)显式应用程序模块依赖 (Explicit Application Module Dependencies) 等机制来满足这些需求。

Open Application Modules (开放应用程序模块)

有时,我们可能需要完全开放一个模块的内部实现,允许其他模块自由访问其内部的任何类型,即使它们位于内部子包中。这通常用于处理遗留模块、共享基础代码模块,或者在模块边界尚不明确时的过渡阶段。

要将一个应用程序模块声明为“开放”,您需要在该模块根包的 package-info.java 文件上使用 @ApplicationModule(type = Type.OPEN) 注解。

// notification\package-info.java
@org.springframework.modulith.ApplicationModule(type = org.springframework.modulith.ApplicationModule.Type.OPEN)
package org.niujiao.spring_modulith_tutorials.notification;

代码示例:让 OrderService 直接访问 NiujiaoNotificationService

在将 notification 模块设置为 OPEN 后,order 模块现在就可以直接依赖 NiujiaoNotificationService 了。修改 OrderService

// order/OrderService.java
package org.niujiao.spring_modulith_tutorials.order;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.niujiao.spring_modulith_tutorials.notification.Notification;
// 直接引入 notification 模块的内部实现类
import org.niujiao.spring_modulith_tutorials.notification.internal.NiujiaoNotificationService;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
@Slf4j
public class OrderService {

    // 现在直接依赖 notification 模块的内部服务
    private final NiujiaoNotificationService niujiaoNotificationService;

    public void createOrder(Order order) {
        log.info("Order Module: Attempting to create order.");
        order.setNo(System.currentTimeMillis() + "");
        log.info("Order Module: Order created successfully, order number: {}", order.getNo());

        Notification notification = new Notification("order_create",
                "Order created, order no is " + order.getNo() + ". Items: " + order.getItems());

        // 直接调用内部服务
        niujiaoNotificationService.sendInternalNotification(
            String.format("Notification for type '%s' with content: '%s'", notification.getType(), notification.getContent()));
        log.info("Order Module: Notification sent for order creation.");
    }
}

运行测试验证:

运行 SpringModulithTutorialsApplicationTests 中的 verifyApplicationModuleModel() 测试方法。

// SpringModulithTutorialsApplicationTests.java
@Test
void verifyApplicationModuleModel() {
    System.out.println("\n--- 验证 Spring Modulith 模块结构 ---");
    ApplicationModules modules = ApplicationModules.of(SpringModulithTutorialsApplication.class);
    System.out.println("--- 检测到的模块信息 ---");
    modules.forEach(System.out::println);
    System.out.println("--------------------");
    modules.verify(); // 验证应该会成功
    System.out.println("--- 模块结构验证成功!---");
    System.out.println("--- 验证 Spring Modulith 模块结构完毕 ---\n");
}

测试应该会成功通过。观察控制台打印的模块信息,您会发现 notification 模块的内部 Bean(如 NiujiaoNotificationService)现在可能被标记为可以从外部访问(具体标记取决于 Modulith 版本和日志格式)。

Open Application Modules 的效果:

将一个模块声明为 OPEN 会对模块验证产生以下影响:

  • 其他模块通常允许访问该开放模块的任何内部类型,包括位于内部子包中的类型。
  • 该模块的所有类型,包括位于其根包子包中的类型,都会被添加到未命名(<<UNNAMED>>)的命名接口中,除非它们已被显式分配到其他命名接口。

注意: OPEN 模块会绕过 Spring Modulith 默认提供的强大的封装性检查,这通常不推荐作为常态。它更多用于特殊情况或逐步重构。

Named Interfaces(命名接口)

命名接口它是更精细地定义模块对外 API 的方式。为 notification.internal 包添加的 @NamedInterface("internal")

// notification\internal\package-info.java
@org.springframework.modulith.NamedInterface("internal")
package org.niujiao.spring_modulith_tutorials.notification.internal;

这个注解使得 notification.internal 包中的 NiujiaoNotificationService 成为了 notification 模块中名为 "internal" 的命名接口的一部分。

模块信息中的命名接口:

运行测试时打印的模块信息会显示这些命名接口:

# Notification
> Logical name: notification
> Base package: org.niujiao.spring_modulith_tutorials.notification
> Excluded packages: none
> Named interfaces:
  + NamedInterface: name=<<UNNAMED>>, types=[ o.n.s.n.Notification, o.n.s.n.NotificationFacadeService ] // 默认命名接口
  + NamedInterface: name=internal, types=[ o.n.s.n.i.NiujiaoNotificationService ] // 显式命名接口
> Spring beans:
  + ….NotificationFacadeService
  + ….internal.NiujiaoNotificationService

这里清晰地列出了 notification 模块暴露的两种命名接口。

Explicit Application Module Dependencies (显式应用程序模块依赖)

消费方模块可以显式地控制允许其依赖哪些模块的哪些部分。这正是通过在消费方模块(如 order 模块)的 @ApplicationModule 注解中设置 allowedDependencies 属性来实现的。这种方式让依赖关系更加清晰和受控。

回顾一下 notification 模块的信息,我们知道它默认会暴露根包下的公共类型(如 NotificationNotificationFacadeService)作为其 API 的一部分。Spring Modulith 将这部分自动识别为名为 <<UNNAMED>> 的命名接口。

接下来,我们将看到如何在 order 模块上配置 allowedDependencies,以显式地声明它允许依赖 notification 模块的这些暴露内容。

代码示例:order 模块显式依赖 notification 模块

首先,将 notification 模块恢复为默认的 CLOSED 类型(移除或注释掉 notification/package-info.java 中的 type = Type.OPEN)。确保 OrderService 当前依赖的是 notification 模块默认接口中的类型,例如 NotificationNotificationFacadeService

允许依赖默认接口(根包公共类型):

如果您希望 order 模块能够依赖 notification 模块根包下的公共类型(如 NotificationNotificationFacadeService),只需要在 allowedDependencies 中使用目标模块的名称。在order包下创建一个 package-info.java 文件

// order/package-info.java
@org.springframework.modulith.ApplicationModule(
   allowedDependencies = { "notification" } // 允许依赖 notification 模块的默认接口 (<<UNNAMED>>)
)
package org.niujiao.spring_modulith_tutorials.order;

// 定义 order 模块并允许依赖 notification 模块的默认接口

在这种配置下,如果 OrderService.java 依赖了 org.niujiao.spring_modulith_tutorials.notification.Notificationorg.niujiao.spring_modulith_tutorials.notification.NotificationFacadeService,运行测试方法 verifyApplicationModuleModel() 将会成功。

如果 OrderService 尝试依赖 notification.internal.NiujiaoNotificationService,则会验证失败,因为 internal 接口未被允许。

允许依赖特定命名接口:

如果您只希望 order 模块依赖 notification 模块中名为 "internal" 的命名接口(对应 notification.internal 包中的类型),您可以在 allowedDependencies 中这样指定:

// order/package-info.java
@org.springframework.modulith.ApplicationModule(
   allowedDependencies = { "notification :: internal" } // 允许依赖 notification 模块的 "internal" 命名接口
)
package org.niujiao.spring_modulith_tutorials.order;

在这种配置下,只有 notification.internal 包下的类型(如 NiujiaoNotificationService,因为它属于 internal 命名接口)才能被 order 模块依赖。如果 OrderService 依赖 notification.NotificationNotificationFacadeService,则会验证失败。

允许依赖多个目标:

您可以在 allowedDependencies 数组中列出多个允许的目标。例如,允许依赖 notification 模块的默认接口和 internal 接口:

// order/package-info.java
@org.springframework.modulith.ApplicationModule(
   allowedDependencies = { "notification", "notification :: internal" } // 允许依赖 notification 的默认接口 和 "internal" 接口
)
package org.niujiao.spring_modulith_tutorials.order;

如果您希望允许依赖目标模块所有显式声明的命名接口(以及根据规则可能包含的默认接口),可以使用 :: * 通配符:

// order/package-info.java
@org.springframework.modulith.ApplicationModule(
   allowedDependencies = { "notification :: *" } // 允许依赖 notification 模块的所有命名接口 (包括 <<UNNAMED>> 和 internal)
)
package org.niujiao.spring_modulith_tutorials.order;

// 允许依赖 notification 模块的所有命名接口

感谢您的阅读!希望这部分内容对您有所启发。如果您觉得有价值,请不吝点赞支持,并在评论区留下您的想法,一起交流学习!别忘了关注我,获取后续更精彩的教程内容。您的每一个肯定和互动,都是我持续分享知识的动力!