第2331篇:Java AI项目的模块化设计——JPMS在AI工程中的应用
第2331篇:Java AI项目的模块化设计——JPMS在AI工程中的应用
适读人群:负责大型Java AI项目架构设计的工程师,或希望提升AI项目代码组织能力的开发者 | 阅读时长:约15分钟 | 核心价值:理解JPMS在AI项目中的实用价值,掌握模块边界划分和依赖管理的工程实践
说实话,JPMS(Java Platform Module System,也叫Project Jigsaw)是Java 9引入后被业界讨论最多、争议最大,但真正用起来的团队并不多的特性之一。
理由很现实:大多数项目用Maven/Gradle的多模块管理就够了,为什么要再搞一层JPMS?Spring Boot本身也对JPMS的支持有限。
但在AI工程场景里,我逐渐发现JPMS有几个点确实有价值,特别是在Prompt模板管控、模型适配器隔离、安全边界划分这几个维度。
这篇文章不是推销JPMS,而是说清楚在AI项目里哪些地方用JPMS值得,哪些地方别碰。
AI项目的模块化诉求
一个中等规模的AI项目,典型的代码组织混乱问题:
- Prompt模板字符串散落在各个Service里,没人知道哪些是生产用的,哪些是测试用的
- 不同的LLM适配器(OpenAI/DeepSeek/本地Ollama)代码混在一起,切换模型要改多处
- AI工具类(Tool/Function)没有统一的可见性控制,任何代码都可以直接实例化
这些问题用Maven多模块可以部分解决,但JPMS提供了编译时的强制约束,让模块边界在代码层面就生效,不是靠约定,而是靠编译器。
一个实际的AI项目模块划分
ai-platform/
├── ai-core/ # 核心抽象层(接口定义)
│ └── module-info.java
├── ai-openai/ # OpenAI适配器
│ └── module-info.java
├── ai-deepseek/ # DeepSeek适配器
│ └── module-info.java
├── ai-rag/ # RAG核心逻辑
│ └── module-info.java
├── ai-tools/ # 工具集合(Function Calling)
│ └── module-info.java
├── ai-security/ # AI安全检查(Prompt注入防护等)
│ └── module-info.java
└── ai-application/ # 应用层(Spring Boot启动)
└── module-info.javaai-core模块:定义接口,不依赖任何实现
// ai-core/src/main/java/module-info.java
module com.example.ai.core {
// 对外暴露的接口包
exports com.example.ai.core.chat;
exports com.example.ai.core.embedding;
exports com.example.ai.core.rag;
exports com.example.ai.core.tool;
// 只依赖JDK标准模块和少量基础库
requires java.base;
requires transitive org.slf4j;
// 不依赖任何Spring,保持核心的可测试性
}// ai-core/src/main/java/com/example/ai/core/chat/AiChatService.java
public interface AiChatService {
String chat(String sessionId, String message);
Flux<String> chatStream(String sessionId, String message);
}
// ai-core/src/main/java/com/example/ai/core/rag/RagService.java
public interface RagService {
String query(String question);
void ingestDocument(String content, Map<String, String> metadata);
}ai-openai模块:OpenAI的实现,只暴露工厂
// ai-openai/src/main/java/module-info.java
module com.example.ai.openai {
// 只暴露配置类,不暴露内部实现
exports com.example.ai.openai.config;
// 需要核心接口
requires com.example.ai.core;
requires spring.ai.openai;
requires spring.context;
// 关键:不暴露实现类,外部无法直接new OpenAiChatServiceImpl()
// 只能通过Spring的DI获取接口实例
}// 实现类只在模块内部可见
// 注意:包名不在exports里,所以外部模块无法访问这个类
class OpenAiChatServiceImpl implements AiChatService {
private final ChatClient chatClient;
OpenAiChatServiceImpl(ChatClient chatClient) {
this.chatClient = chatClient;
}
@Override
public String chat(String sessionId, String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
@Override
public Flux<String> chatStream(String sessionId, String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
}Prompt模板的模块化管控
这是JPMS在AI项目里最实用的场景之一。
把所有Prompt模板集中在一个独立模块里,通过模块边界控制谁可以读取、谁可以修改:
// ai-prompts模块(独立管理所有Prompt)
module com.example.ai.prompts {
// 只暴露加载接口,不暴露模板内容的直接访问
exports com.example.ai.prompts.loader;
requires com.example.ai.core;
requires java.base;
}// com.example.ai.prompts.loader.PromptLoader
public interface PromptLoader {
/**
* 按名称加载Prompt模板
* @param name 模板名称,如 "rag.answer", "code.review"
* @return 模板字符串
*/
String load(String name);
/**
* 按名称和参数渲染Prompt
*/
String render(String name, Map<String, Object> variables);
}// Prompt模板统一存放在模块资源目录
ai-prompts/src/main/resources/prompts/
├── rag/
│ ├── answer.txt # RAG回答模板
│ ├── rewrite-query.txt # 查询改写模板
│ └── grade-document.txt # 文档质量评分模板
├── code/
│ ├── review.txt # 代码审查模板
│ └── explain.txt # 代码解释模板
└── chat/
├── system-default.txt # 默认系统提示
└── system-formal.txt # 正式场景系统提示// 内部实现:从资源文件加载Prompt
class FileBasedPromptLoader implements PromptLoader {
private final Map<String, String> templates = new ConcurrentHashMap<>();
@Override
public String load(String name) {
return templates.computeIfAbsent(name, this::loadFromFile);
}
@Override
public String render(String name, Map<String, Object> variables) {
String template = load(name);
// 简单的变量替换(可以用更复杂的模板引擎)
for (Map.Entry<String, Object> entry : variables.entrySet()) {
template = template.replace("{" + entry.getKey() + "}",
String.valueOf(entry.getValue()));
}
return template;
}
private String loadFromFile(String name) {
String path = "/prompts/" + name.replace('.', '/') + ".txt";
try (InputStream is = getClass().getResourceAsStream(path)) {
if (is == null) {
throw new PromptNotFoundException("Prompt模板不存在: " + name);
}
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new PromptLoadException("加载Prompt失败: " + name, e);
}
}
}JPMS的实际限制:不要高估它
讲完好处,必须说清楚限制,避免浪费时间。
限制1:Spring Boot的JPMS支持并不完整
Spring大量使用反射,JPMS的强封装会拦截很多Spring内部的反射操作。Spring Boot 3.x对JPMS有了更好的支持,但你仍然需要在module-info.java里加上大量opens声明:
module com.example.ai.application {
// Spring需要通过反射访问你的Bean
opens com.example.ai.application.config to spring.core, spring.context;
opens com.example.ai.application.service to spring.core;
opens com.example.ai.application.controller to spring.web;
// Jackson的反序列化需要访问你的DTO
opens com.example.ai.application.dto to com.fasterxml.jackson.databind;
// ... 这个列表会越来越长
}这些opens声明实际上削弱了JPMS的封装保证——如果开了太多,等于白折腾。
限制2:大多数第三方库还没有模块化
Spring AI本身目前不是JPMS模块(没有module-info.java),它以"自动模块"方式运行。自动模块会导出所有包,读取所有其他自动模块,基本等于没有封装。
实用建议:在AI项目里,我的推荐是
- 小项目:不用JPMS,Maven多模块就够
- 中等项目:用JPMS来管理核心接口模块(ai-core),让实现模块继续用Maven多模块
- 大项目/平台:核心接口和Prompt模板用JPMS严格管控,其他模块按需引入JPMS
用最小代价获得最大价值:哪怕只把JPMS用来把Prompt模板和核心接口封装好,就已经比散落各处的字符串常量强得多。
