从零读懂AI智能客服(二):Java 多模块项目如何协作:Maven 依赖、Spring 注入与运行时装配

本文主题:解释模块为什么能互相使用,以及 Maven、Spring 和公共接口分别承担什么职责

适合读者:刚接触 Maven 多模块、Spring 依赖注入和模块化单体架构的 Java 开发者

代码基线:当前学习分支源码快照
上一篇:从零读懂 AI 智能客服后端架构:模块职责与 SSE 聊天链路

下一篇:AI 智能客服为什么不能照搬传统三层架构:CRUD 与 AI 编排分层对比


🐟 这里是yurenpai

27届开发者,主要学习 Java 后端与 AI 应用开发。

这里记录真实项目中的代码调用链、Agent/RAG 工程化、问题排查和开发复盘。

个人理念:

模块之间能否协作,不能只看目录位置,必须同时追踪构建依赖和运行时装配。

写在前面

先说结论:两个模块放在同一个仓库里,并不代表它们可以直接互相调用。真正决定模块协作的是三层机制:Maven 负责建立编译依赖,Spring 负责发现并注入运行时 Bean,公共接口负责控制依赖方向、避免业务模块形成循环依赖。

本文会以"聊天模块调用工作流、工作流反向使用聊天能力"为贯穿案例,重点讲清:

  1. 父子关系、聚合关系和依赖关系有什么区别;
  2. admin 为什么能够把多个业务模块装进同一个应用;
  3. 接口放在公共模块中,为什么能避免 chat ↔ aiflow 的循环依赖;
  4. 如何用 POM、import、接口实现和启动模块反向验证一次跨模块调用。

说明:本文以当前项目源码为例,重点解释模块协作方法;不同项目的模块名称可能不同,但分析步骤可以复用。

代码说明:除明确标注为完整源码外,文中的 POM、Java 代码和目录片段均为根据当前源码整理的简化示意;文中的"已核对"表示完成静态源码核对,不等同于启动或接口测试通过。


一、为什么要拆成这么多模块

假设不拆模块,把所有代码都放在一个项目中:

text 复制代码
src/main/java/com/tst/pharma/
├─ 用户管理
├─ 权限管理
├─ 聊天
├─ 知识库
├─ 工作流
├─ AI 流程
├─ Redis
├─ SSE
├─ 短信
├─ 文件上传
├─ Excel
└─ 定时任务

时间长了会出现几个问题:

text 复制代码
1. 所有代码混在一起,不知道属于哪个业务;
2. 一个聊天模块也能随便依赖用户、短信、工作流内部实现;
3. 公共工具被复制多份;
4. 修改一个模块容易影响其他模块;
5. Maven 依赖越来越混乱;
6. 新人很难判断代码应该放在哪里;
7. 无法控制模块之间的依赖方向。

所以项目进行了两次拆分。

第一次:按大职责拆分

text 复制代码
tst-pharma-admin
├─ 应用启动和组装

tst-pharma-common
├─ 公共基础能力

tst-pharma-modules
├─ 具体业务模块

tst-pharma-extend
└─ 独立扩展服务

第二次:在每个大模块内部继续拆分

例如公共能力拆成:

text 复制代码
tst-pharma-common-core
tst-pharma-common-web
tst-pharma-common-redis
tst-pharma-common-sse
tst-pharma-common-mybatis
tst-pharma-common-satoken

业务能力拆成:

text 复制代码
tst-pharma-system
tst-pharma-chat
tst-pharma-workflow
tst-pharma-aiflow
tst-pharma-generator

这样就可以做到:

text 复制代码
聊天模块需要 SSE
→ 只依赖 common-sse

聊天模块需要 Web 能力
→ 只依赖 common-web

聊天模块不需要短信
→ 就不必直接依赖 common-sms

这就是"模块化"的主要意义:

不是为了把目录弄多,而是为了划分职责、复用能力并控制依赖。


二、先区分三个容易混淆的概念

项目中有三种关系,一定要区分。

1. 父子关系

例如子模块的 pom.xml 中:

xml 复制代码
<parent>
    <groupId>com.tst.pharma</groupId>
    <artifactId>tst-pharma-main-backend</artifactId>
    <version>${revision}</version>
</parent>

它表示子模块继承父工程的:

text 复制代码
版本号
依赖版本管理
Maven 插件
Java 版本
构建配置

但是:

继承父 POM,不代表父模块能够直接使用子模块的 Java 类。


2. 聚合关系

pom.xml 中:

xml 复制代码
<modules>
    <module>tst-pharma-admin</module>
    <module>tst-pharma-common</module>
    <module>tst-pharma-extend</module>
    <module>tst-pharma-modules</module>
</modules>

它表示:

text 复制代码
在根目录执行 Maven 构建时
→ Maven 会一起构建这些模块

但是:

被根 POM 聚合,也不等于模块之间能够互相调用。

例如:

text 复制代码
tst-pharma-chat
tst-pharma-workflow

虽然都被根项目聚合,但如果 chat/pom.xml 没有依赖 workflow,那么 chat 不能直接随意导入 workflow 的类。


3. 依赖关系

真正决定一个模块能否使用另一个模块 Java 类的是:

xml 复制代码
<dependency>
    <groupId>com.tst.pharma</groupId>
    <artifactId>被依赖模块</artifactId>
</dependency>

例如 tst-pharma-chat/pom.xml 中有:

xml 复制代码
<dependency>
    <groupId>com.tst.pharma</groupId>
    <artifactId>tst-pharma-common-chat</artifactId>
</dependency>

<dependency>
    <groupId>com.tst.pharma</groupId>
    <artifactId>tst-pharma-common-sse</artifactId>
</dependency>

所以聊天模块才能使用:

java 复制代码
import com.tst.pharma.common.chat.domain.dto.request.ChatRequest;
import com.tst.pharma.common.sse.core.SseEmitterManager;

可以记成:

text 复制代码
聚合关系
└─ 决定一起构建

父子关系
└─ 决定继承统一配置

依赖关系
└─ 决定是否能使用另一个模块的代码

小鱼点睛

父子关系统一配置,聚合关系决定一起构建,依赖关系决定能否使用对方的 Java 类。两个目录即使紧挨在一起,也不会自动产生代码依赖。


三、项目真实的模块依赖方向

整个项目可以简化成:

text 复制代码
                         tst-pharma-admin
                       主应用启动和组装
                              │
          ┌───────────────────┼────────────────────┐
          │                   │                    │
          ▼                   ▼                    ▼
 tst-pharma-system    tst-pharma-chat      tst-pharma-workflow
 系统管理业务          AI聊天与知识库          传统业务流程
                              │
                              ▼
                     tst-pharma-aiflow
                       AI流程编排

上述业务模块继续依赖:

                    tst-pharma-common-*
                       公共基础组件

更准确地说,admin 直接依赖:

text 复制代码
tst-pharma-system
tst-pharma-generator
tst-pharma-chat
tst-pharma-workflow
tst-pharma-aiflow

因此最终启动 admin 时,这些业务模块都会进入主应用。


四、admin 是怎么把所有业务模块装进来的

文件:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-admin\pom.xml

它声明了:

text 复制代码
admin
├─ 依赖 system
├─ 依赖 generator
├─ 依赖 chat
├─ 依赖 workflow
└─ 依赖 aiflow

构建主应用时,Maven 会把这些模块的编译产物和依赖一起放进最终应用。

可以将最终运行的应用理解成:

text 复制代码
tst-pharma-admin.jar
├─ admin 自己的类
├─ system 模块的类
├─ chat 模块的类
├─ workflow 模块的类
├─ aiflow 模块的类
├─ generator 模块的类
└─ 它们所依赖的 common 类

因此运行时主要是:

text 复制代码
一个 JVM 进程
一个 Spring 容器
一个后端端口 6039

而不是:

text 复制代码
system 启一个端口
chat 启一个端口
workflow 启一个端口
aiflow 再启一个端口

所以当前主体架构属于:

模块化单体应用,不是微服务架构。


五、公共模块是怎么被业务模块使用的

例子一:聊天模块使用 SSE

ChatServiceFacade 中:

java 复制代码
private final SseEmitterManager sseEmitterManager;

SseEmitterManager 不在聊天模块,而在:

text 复制代码
tst-pharma-common-sse

聊天模块的 pom.xml 声明了:

xml 复制代码
<dependency>
    <groupId>com.tst.pharma</groupId>
    <artifactId>tst-pharma-common-sse</artifactId>
</dependency>

完整关系是:

text 复制代码
tst-pharma-chat
   │
   │ Maven 依赖
   ▼
tst-pharma-common-sse
   │
   ├─ SseEmitterManager
   ├─ SseMessageUtils
   ├─ SseEventDto
   └─ SSE 自动配置

于是聊天模块才能:

text 复制代码
建立 SSE
发送 content
发送 done
发送 error
关闭 SSE

例子二:聊天模块使用登录认证

聊天代码中使用:

text 复制代码
LoginHelper.getUserId();
StpUtil.getTokenValue();

这涉及:

text 复制代码
common-satoken
common-redis
Sa-Token

依赖链不一定都由聊天模块直接声明,也可以通过依赖传递获得。

简化理解:

text 复制代码
tst-pharma-chat
   ↓
common-chat / common-sse / common-web
   ↓
common-satoken
   ↓
common-redis
   ↓
Redis

所以一个模块不一定需要把所有底层依赖都重新声明一遍。


例子三:系统模块使用 MyBatis

SysConfigMapper

java 复制代码
public interface SysConfigMapper
        extends BaseMapperPlus<SysConfig, SysConfigVo> {
}

其中 BaseMapperPlus 位于:

text 复制代码
tst-pharma-common-mybatis

关系为:

text 复制代码
tst-pharma-system
   │
   ▼
tst-pharma-common-mybatis
   │
   ▼
MyBatis-Plus
   │
   ▼
MySQL

因此 system 模块不需要自己再实现一套分页、Mapper 基类和数据库配置。


六、模块之间不仅靠 Maven,还靠 Spring 连接

Maven 解决的是:

text 复制代码
编译时能不能看到另一个模块的类

Spring 解决的是:

text 复制代码
程序启动后,具体使用哪个实现对象

启动类:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-admin\src\main\java\com\tst\pharma\TstPharmaApplication.java

位于根包:

java 复制代码
package com.tst.pharma;

并且有:

java 复制代码
@SpringBootApplication

Spring 默认会从启动类所在包向下扫描:

text 复制代码
com.tst.pharma
├─ controller
├─ service
├─ config
├─ factory
├─ workflow
└─ ...

即使类分别位于不同 Maven 模块中,只要最后都被 admin 引入,并且包名属于:

text 复制代码
com.tst.pharma...

Spring 就能发现它们。

例如:

text 复制代码
chat 模块
└─ ChatController
   └─ @Controller

chat 模块
└─ ChatServiceFacade
   └─ @Service

aiflow 模块
└─ WorkflowStarter
   └─ Spring Bean

common-sse 模块
└─ SSE 自动配置
   └─ 注册 SseEmitterManager

它们最后都在同一个 Spring 容器中。


七、Spring 依赖注入是怎么跨模块工作的

例如 ChatController 中:

java 复制代码
private final ChatServiceFacade chatService;

因为类上有:

java 复制代码
@RequiredArgsConstructor

Lombok 会生成类似构造器:

java 复制代码
public ChatController(ChatServiceFacade chatService) {
    this.chatService = chatService;
}

Spring 启动时发现:

text 复制代码
ChatController 需要 ChatServiceFacade

然后又发现:

java 复制代码
@Service
public class ChatServiceFacade {
}

于是完成注入:

text 复制代码
Spring 容器
├─ 创建 ChatServiceFacade
├─ 创建 ChatController
└─ 把 ChatServiceFacade 放进 ChatController

虽然它们处在不同目录甚至不同 Maven 模块,也不影响运行时注入,只要满足:

text 复制代码
1. admin 的 Maven 依赖把两个模块装进来了;
2. Spring 扫描到了对应类;
3. 类注册成了 Bean;
4. 依赖类型能够匹配。

小鱼点睛

Maven 只解决"编译时能不能看见接口和类",真正把接口字段连接到实现对象的是 Spring 容器。只有编译依赖、Bean 扫描和类型匹配同时成立,跨模块注入才会成功。


八、项目中最典型的跨模块设计:common-chat 接口桥梁

这一部分很重要,因为它解释了:

chataiflow 为什么可以互相协作,但没有在 POM 中直接互相依赖。

先看结构:

text 复制代码
tst-pharma-common-chat
├─ IChatService
├─ IChatModelService
└─ IWorkFlowStarterService

它们只是接口和公共协议,不包含具体业务实现。

聊天服务接口

位于:

text 复制代码
tst-pharma-common-chat
└─ IChatService

具体实现位于聊天模块:

java 复制代码
@Service
public class ChatServiceFacade implements IChatService {
}

关系:

text 复制代码
common-chat
└─ 定义 IChatService

chat
└─ ChatServiceFacade 实现 IChatService

工作流启动接口

位于:

text 复制代码
tst-pharma-common-chat
└─ IWorkFlowStarterService

具体实现位于 AI 流程模块:

java 复制代码
public class WorkflowStarter
        implements IWorkFlowStarterService {
}

关系:

text 复制代码
common-chat
└─ 定义 IWorkFlowStarterService

aiflow
└─ WorkflowStarter 实现 IWorkFlowStarterService

chat 调用 aiflow

ChatServiceFacade 中不是直接依赖某个 aiflow 内部类,而是:

java 复制代码
private final IWorkFlowStarterService workFlowStarterService;

流程是:

text 复制代码
ChatServiceFacade
   │
   │ 只认识公共接口
   ▼
IWorkFlowStarterService
   ▲
   │ 由 Spring 在运行时寻找实现
   │
WorkflowStarter

这样 chat 模块在编译时不需要直接依赖 aiflow


aiflow 调用 chat

aiflowWorkflowUtil 中使用:

java 复制代码
private IChatService chatService;
private IChatModelService chatModelService;

流程:

text 复制代码
WorkflowUtil
   │
   │ 只依赖公共接口
   ▼
IChatService
   ▲
   │ Spring 注入实现
   │
ChatServiceFacade

所以整体是:

text 复制代码
                     common-chat
                公共接口和公共请求对象
                  ▲               ▲
                  │               │
                chat           aiflow
             实现聊天接口      实现流程接口

这叫:

通过公共契约解耦业务模块。
小鱼点睛

把接口下沉到双方都能依赖的公共模块,目的不只是"集中存放公共类",而是反转依赖方向:业务模块面向稳定契约协作,避免 chataiflow 在 POM 中相互咬住。


九、为什么接口要放在 common-chat,而不是直接放在 chat

如果 IChatService 放在:

text 复制代码
tst-pharma-chat

那么 aiflow 为了调用它,就必须:

text 复制代码
aiflow → 依赖 chat

同时聊天模块为了启动 AI 工作流,可能又需要:

text 复制代码
chat → 依赖 aiflow

最后形成:

text 复制代码
chat → aiflow
  ↑       ↓
  └───────┘

这就是 Maven 循环依赖。

Maven 无法正常处理这样的模块关系。

当前项目把接口放进公共模块后,变成:

text 复制代码
chat ──────→ common-chat
aiflow ────→ common-chat

然后由 admin 同时引入:

text 复制代码
admin
├─ chat
└─ aiflow

运行时再由 Spring 把实现连接起来。

这个设计可以理解为插座:

text 复制代码
common-chat
└─ 规定插座形状

chat
└─ 提供一种插头

aiflow
└─ 提供或使用另一种插头

Spring
└─ 启动时把兼容的插头插到插座上

十、admin 是最终的组装者

这个项目中,真正知道"我要同时加载哪些业务模块"的是:

text 复制代码
tst-pharma-admin

它的 pom.xml 相当于组装清单:

text 复制代码
主应用需要:
├─ system
├─ generator
├─ chat
├─ workflow
└─ aiflow

因此可以这样理解:

text 复制代码
common
└─ 制造公共零件

modules
├─ 制造用户系统
├─ 制造聊天系统
├─ 制造业务工作流
└─ 制造 AI 工作流

admin
└─ 把所有零件和业务模块装成完整应用

十一、数据库 Mapper 又是怎么跨模块加载的

配置位于:

text 复制代码
D:\Tools\java\aiwork\tst-pharma-main-backend\tst-pharma-admin\src\main\resources\application.yml

其中配置了:

yaml 复制代码
mybatis-plus:
  mapperPackage: com.tst.pharma.**.mapper
  mapperLocations: classpath*:mapper/**/*Mapper.xml
  typeAliasesPackage: com.tst.pharma.**.domain

MyBatis 配置类使用:

java 复制代码
@MapperScan("${mybatis-plus.mapperPackage}")

意思是扫描所有模块中符合下面规则的 Mapper:

text 复制代码
com.tst.pharma.任意内容.mapper

例如:

text 复制代码
system 模块
└─ com.tst.pharma.system.mapper.SysConfigMapper

chat 模块
└─ com.tst.pharma.mapper.ChatMessageMapper

workflow 模块
└─ 对应的 mapper

classpath*: 也表示:

不只查当前模块,还会查依赖 JAR 中的 Mapper XML。

所以启动一个 admin,能够把多个业务模块中的 Mapper 全部注册到同一个 MyBatis/Spring 容器。


十二、配置文件如何影响所有模块

主要运行配置集中在:

text 复制代码
tst-pharma-admin\src\main\resources\application.yml
tst-pharma-admin\src\main\resources\application-dev.yml

这里配置:

text 复制代码
端口
数据库
Redis
MyBatis
Sa-Token
多租户
日志
SSE
上传目录

虽然配置文件在 admin,但 Spring 启动后创建的是一个统一环境。

因此:

text 复制代码
system 模块可以使用数据源
chat 模块可以使用数据源
workflow 模块可以使用 Redis
common-sse 可以读取 SSE 配置
common-satoken 可以读取认证配置

它们不需要各自维护一套 application.yml

可以理解成:

text 复制代码
admin application.yml
          │
          ▼
    Spring Environment
       ├─ system 使用
       ├─ chat 使用
       ├─ workflow 使用
       ├─ aiflow 使用
       └─ common 组件使用

十三、数据库和 Redis 也是模块之间的公共基础设施

当前主要业务模块运行在同一个应用中,并共享:

text 复制代码
MySQL
Redis
登录状态
租户上下文
事务管理器
SSE 连接管理

例如一次聊天请求可能发生:

text 复制代码
chat 模块
├─ 从 chat_model 表查询模型配置
├─ 从 chat_message 表查询历史消息
├─ 从 knowledge_info 表查询知识库配置
├─ 使用 Redis/登录上下文获取认证信息
└─ 使用 SSE 向当前用户发送结果

但要注意:

共享同一个数据库,不代表模块应该随意直接修改其他模块的表。

正常情况下应优先:

text 复制代码
模块 A
→ 调用公共接口
→ 模块 B 的 Service
→ 模块 B 的 Mapper
→ 模块 B 负责自己的表

而不是:

text 复制代码
模块 A
→ 直接拿模块 B 的 Mapper
→ 随意修改模块 B 的表

否则模块边界会再次被破坏。


十四、用聊天请求看模块之间如何真正协作

一次:

http 复制代码
POST /chat/send

背后会经过很多模块。

text 复制代码
客户端请求
   │
   ▼
common-web / common-satoken
├─ Web 请求处理
├─ 登录认证
└─ 用户上下文
   │
   ▼
tst-pharma-chat
├─ ChatController
├─ ChatServiceFacade
├─ 场景分类
└─ 模型调用
   │
   ├──────────────┐
   ▼              ▼
common-sse     common-chat
SSE连接和事件   公共请求对象和接口
   │              │
   │              ├─────────────┐
   │              ▼             ▼
   │           aiflow        chat实现
   │         AI流程启动      模型聊天实现
   │
   ▼
浏览器逐段接收回复

如果是普通聊天:

text 复制代码
ChatController
→ ChatServiceFacade
→ ChatModelService
→ ChatServiceFactory
→ 模型供应商实现
→ LangChain4j
→ 外部模型接口
→ common-sse
→ 前端

如果是 AI 工作流:

text 复制代码
ChatController
→ ChatServiceFacade
→ IWorkFlowStarterService
→ aiflow 中的 WorkflowStarter
→ AI 工作流节点执行
→ 需要模型时调用 IChatService
→ chat 中的 ChatServiceFacade
→ 模型

这就是多个模块之间的真实协作。


十五、extend 为什么没有被直接装进 admin

根 POM 聚合了:

text 复制代码
tst-pharma-extend

所以执行根 Maven 构建时,会一起构建:

text 复制代码
monitor-admin
snailjob-server

但是 admin/pom.xml 没有把它们作为主业务依赖引入。

这说明:

text 复制代码
根 POM 聚合它们
≠ admin 运行时包含它们

它们更可能是独立启动的扩展服务:

text 复制代码
主后端
└─ tst-pharma-admin

监控服务
└─ tst-pharma-monitor-admin

任务调度服务
└─ tst-pharma-snailjob-server

所以再次强调:

text 复制代码
聚合
└─ 一起构建

依赖
└─ 编译和运行时使用

十六、当前项目的核心模块关系图

可以先保存这张图:

text 复制代码
tst-pharma-main-backend
│
├─ tst-pharma-common
│  ├─ common-core
│  │  └─ 最底层公共对象、异常、工具
│  │
│  ├─ common-redis
│  │  └─ 依赖 common-core
│  │
│  ├─ common-satoken
│  │  └─ 依赖 common-core、common-redis
│  │
│  ├─ common-mybatis
│  │  └─ 依赖 common-core、common-satoken
│  │
│  ├─ common-sse
│  │  └─ 依赖 core、redis、satoken、json
│  │
│  ├─ common-web
│  │  └─ Web 公共能力
│  │
│  └─ common-chat
│     ├─ 依赖 core
│     ├─ 依赖 sse
│     ├─ 依赖 mybatis
│     └─ 定义聊天、模型、工作流公共接口
│
├─ tst-pharma-modules
│  ├─ system
│  │  └─ 依赖 MyBatis、Web、认证、租户等公共模块
│  │
│  ├─ chat
│  │  ├─ 依赖 common-chat
│  │  ├─ 依赖 common-sse
│  │  ├─ 依赖 common-web
│  │  └─ 实现 IChatService、IChatModelService
│  │
│  ├─ aiflow
│  │  ├─ 依赖 common-chat
│  │  ├─ 使用 IChatService
│  │  └─ 实现 IWorkFlowStarterService
│  │
│  ├─ workflow
│  │  └─ 依赖 MyBatis、Web、租户、认证等公共模块
│  │
│  └─ generator
│     └─ 依赖 MyBatis、Web、文档、日志等公共模块
│
├─ tst-pharma-admin
│  ├─ 引入 system
│  ├─ 引入 chat
│  ├─ 引入 workflow
│  ├─ 引入 aiflow
│  ├─ 引入 generator
│  └─ 启动一个完整 Spring Boot 应用
│
└─ tst-pharma-extend
   ├─ monitor-admin
   └─ snailjob-server

十七、判断"两个模块怎么关联"的固定方法

以后看到两个模块,不要凭目录名猜,可以按下面五步确认。

第一步:看调用模块的 pom.xml

例如想知道 chat 能不能使用 common-sse

text 复制代码
打开 tst-pharma-chat/pom.xml
搜索 tst-pharma-common-sse

如果有依赖,说明编译时可见。

第二步:看 Java import

例如:

java 复制代码
import com.tst.pharma.common.sse.core.SseEmitterManager;

说明代码确实使用了该模块的类。

第三步:看字段注入类型

例如:

java 复制代码
private final IWorkFlowStarterService workFlowStarterService;

说明当前类依赖的是接口。

第四步:查谁实现接口

搜索:

text 复制代码
implements IWorkFlowStarterService

找到:

text 复制代码
aiflow → WorkflowStarter

第五步:确认最终启动模块是否同时引入双方

查看:

text 复制代码
tst-pharma-admin/pom.xml

确认同时依赖:

text 复制代码
chat
aiflow

这样 Spring 运行时才有机会把它们连接起来。


十八、核心结论

结论一

text 复制代码
目录相邻
≠ 有依赖关系

真正的编译依赖看:

text 复制代码
pom.xml 中的 dependency

结论二

text 复制代码
根 POM 中有 module
≠ 该模块被装进主应用

它可能只是一起构建。

结论三

text 复制代码
admin 是主应用组装者

它把:

text 复制代码
system、chat、workflow、aiflow、generator

放进同一个 Spring Boot 应用。

结论四

text 复制代码
common 提供公共能力和公共接口
modules 提供具体业务实现

结论五

模块之间推荐通过:

text 复制代码
公共接口
Spring 依赖注入

进行协作,而不是互相直接依赖内部实现。

结论六

当前主体不是微服务,而是:

text 复制代码
模块化单体
├─ 编译时分模块
├─ 代码职责分模块
└─ 运行时主要在同一个 Spring Boot 进程

最典型的真实关系就是:

text 复制代码
ChatServiceFacade
   │
   │ 实现
   ▼
IChatService(common-chat)
   ▲
   │ 使用
   │
WorkflowUtil(aiflow)

以及反方向:

text 复制代码
WorkflowStarter(aiflow)
   │
   │ 实现
   ▼
IWorkFlowStarterService(common-chat)
   ▲
   │ 使用
   │
ChatServiceFacade(chat)

它们由 admin 同时组装,再由 Spring 在运行时连接。这个例子基本涵盖了整个项目模块化设计的核心。


总结

本文围绕"解释模块为什么能互相使用,以及 Maven、Spring 和公共接口分别承担什么职责",主要分析了:

  1. 父子、聚合和依赖三种 Maven 关系的边界;
  2. 启动模块如何完成业务模块的最终组装;
  3. Spring 如何跨 JAR 扫描 Bean 并按接口注入实现;
  4. 公共接口桥梁如何控制依赖方向并避免循环依赖;

整个过程可以概括为:

text 复制代码
根 POM 聚合 → 子模块声明依赖 → Java 使用公共接口 → Spring 扫描实现类 → admin 统一装配 → 运行时完成调用

Maven 解决"编译时能不能看见",Spring 解决"运行时由谁来实现",公共接口解决"依赖应该朝哪个方向"。三者缺一不可。

当前进度

OK 已经完成

  • 已完成主要模块 POM 依赖方向的静态核对;

  • 已通过接口定义、实现类和启动模块还原聊天模块与工作流模块的协作关系;
    TODO 后续继续

  • 下一篇对比普通 CRUD 分层与 AI 对话编排分层;

  • 继续结合真实请求理解模块协作在运行时如何落到方法调用;
    小鱼点睛

Maven 解决"编译时能不能看见",Spring 解决"运行时由谁来实现",公共接口解决"依赖应该朝哪个方向"。三者缺一不可。

下一篇

下一篇将继续分析"AI 智能客服为什么不能照搬传统三层架构:CRUD 与 AI 编排分层对比",把本文建立的结构认知继续落到具体代码和对象流上。


这篇文章是我在真实项目学习过程中的阶段性记录。不同项目的命名和目录可能不同,但判断职责边界、依赖方向和数据生命周期的方法可以复用。如果内容中还有遗漏,欢迎一起交流。

相关推荐
楠楠子呀1 小时前
独立站聊天转化全链路:从二维码引流到数据复盘
java·javascript·数据仓库·python·c#·自动化·etl
有来技术1 小时前
youlai-boot 实战:MinIO 停止维护,Docker 迁移 RustFS 完整记录
java·后端·docker
吴声子夜歌1 小时前
Java面试——设计模式(二)
java·设计模式·面试
JAVA面经实录9171 小时前
MySQL问题定位与性能优化完整知识体系
java·jvm·数据库·mysql·性能优化
Rain的Java大神之路1 小时前
Docker搭建Redis集群完全指南
java·运维·redis·后端·docker·容器·架构
Raas1002 小时前
大模型网关和API网关区别是什么?MAI Gateway统一AI流量治理
java·大数据·运维·人工智能·gateway·企业级·ai网关
BioRunYiXue2 小时前
RACE技术全攻略:从全长克隆到靶基因验证
java·javascript·网络·人工智能·科技·算法·eclipse
AC赳赳老秦2 小时前
个保法下数据处理:OpenClaw 自动过滤公开数据中的个人信息,保障采集分析合规性
java·python·sqlite·json·php·deepseek·openclaw
rannn_1112 小时前
Java 后端面试题总结:点赞功能怎么实现?
java·后端·面试