Spring AI插件化架构:构建可扩展的AI能力平台
Spring AI插件化架构:构建可扩展的AI能力平台
一、开篇故事:紧耦合的代价
2025年10月,某集团公司的技术总监老陈面对一个棘手问题。
集团旗下有5条业务线,每条业务线都在使用AI能力,但情况各不相同:
- 电商业务线:需要商品描述生成、评论分析
- 金融业务线:需要风险评估、合规检查(用通义千问私有化部署)
- 医疗业务线:需要病历摘要(严格的数据隐私,只能用私有模型)
- 教育业务线:需要题目生成、批改作文(GPT-4效果更好)
- HR业务线:需要简历筛选、面试评估
AI平台团队负责维护统一的AI能力平台,代码结构是这样的:
// 噩梦般的AI服务类,5000+行
@Service
public class AiCapabilityService {
// 电商能力
public String generateProductDescription(Product product) { ... }
public List<String> analyzeReviews(List<String> reviews) { ... }
// 金融能力(私有化模型,特殊配置)
public RiskAssessment assessRisk(FinancialData data) { ... }
public ComplianceResult checkCompliance(String document) { ... }
// 医疗能力(另一套私有模型)
public String summarizeMedicalRecord(String record) { ... }
// 教育能力(GPT-4)
public List<Question> generateQuestions(String topic) { ... }
public EssayFeedback gradeEssay(String essay) { ... }
// HR能力
public ResumeScore scoreResume(Resume resume) { ... }
public InterviewQuestions generateInterviewQuestions(String jd) { ... }
// ... 还有200多个方法
}老陈的团队遇到了3个严重问题:
- 金融业务线要更换私有模型:需要修改核心类,影响所有业务线的部署
- 教育业务线要新增"智能辅导"能力:需要修改同一个5000行的类,其他业务线被迫升级
- 医疗业务线要回滚某个AI功能:整个平台要回滚,其他业务线躺枪
老陈找到了某大厂架构师老赵,老赵只说了一句话:
"你需要插件化架构。每个业务线的AI能力是一个插件,平台负责加载和调度,互不影响。"
二、插件化架构的价值:开闭原则在AI系统的应用
2.1 插件化架构对比传统架构
2.2 插件化带来的收益
| 维度 | 紧耦合架构 | 插件化架构 |
|---|---|---|
| 新增能力 | 修改核心类+全量测试 | 新增插件包 |
| 升级某业务线能力 | 全平台发布 | 热更新单个插件 |
| 回滚某能力 | 全平台回滚 | 卸载插件/回退版本 |
| 不同业务线用不同模型 | 复杂的条件分支 | 插件内独立配置 |
| 第三方扩展 | 源码修改 | 实现插件接口+部署 |
三、完整项目配置
3.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
</parent>
<groupId>com.laozhang.ai</groupId>
<artifactId>ai-plugin-platform</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>platform-core</module>
<module>plugin-api</module>
<module>plugin-ecommerce</module>
<module>plugin-finance</module>
<module>plugin-loader</module>
</modules>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.0-M6</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>3.2 platform-core/pom.xml
<project>
<parent>
<groupId>com.laozhang.ai</groupId>
<artifactId>ai-plugin-platform</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>platform-core</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.laozhang.ai</groupId>
<artifactId>plugin-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.laozhang.ai</groupId>
<artifactId>plugin-loader</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>3.3 application.yml
server:
port: 8080
spring:
application:
name: ai-plugin-platform
ai:
openai:
api-key: ${DASHSCOPE_API_KEY}
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
chat:
options:
model: qwen-plus
data:
redis:
host: localhost
port: 6379
# 插件系统配置
plugin:
# 插件目录(JAR包放置位置)
directory: /opt/ai-platform/plugins
# 热加载扫描间隔(秒)
hot-reload:
enabled: true
scan-interval-seconds: 30
# 插件隔离级别
isolation:
level: CLASSLOADER # CLASSLOADER | PROCESS | NONE
# 允许的插件来源
trusted-sources:
- "com.laozhang.ai.plugin.*"
- "com.yourcompany.plugin.*"
# 业务线到插件的路由映射
plugin-routing:
ecommerce: ecommerce-ai-plugin
finance: finance-ai-plugin
medical: medical-ai-plugin
education: education-ai-plugin
hr: hr-ai-plugin四、AI能力插件接口设计
4.1 统一的Plugin接口
package com.laozhang.ai.plugin.api;
import java.util.Map;
/**
* AI能力插件统一接口
* 所有AI能力插件必须实现此接口
*
* 生命周期:UNLOADED → LOADING → LOADED → ACTIVE → DISABLED → UNLOADED
*/
public interface AiCapabilityPlugin {
// ===================== 插件元信息 =====================
/**
* 插件唯一ID(全局唯一,如 "ecommerce-ai-plugin")
*/
String getPluginId();
/**
* 插件名称(人类可读,如 "电商AI能力插件")
*/
String getPluginName();
/**
* 插件版本(语义化版本,如 "1.2.0")
*/
String getVersion();
/**
* 插件描述
*/
String getDescription();
/**
* 插件作者
*/
String getAuthor();
/**
* 此插件支持的能力类型列表(如 ["PRODUCT_DESCRIPTION", "REVIEW_ANALYSIS"])
*/
java.util.List<String> getSupportedCapabilities();
// ===================== 生命周期 =====================
/**
* 插件初始化(加载时调用一次)
* 用于初始化AI模型连接、加载资源等
*
* @param context 插件上下文(包含配置、Spring ApplicationContext等)
*/
void initialize(PluginContext context);
/**
* 插件销毁(卸载时调用)
* 释放资源
*/
void destroy();
/**
* 获取插件当前状态
*/
PluginStatus getStatus();
// ===================== 核心能力执行 =====================
/**
* 执行AI能力
*
* @param capability 能力类型(与getSupportedCapabilities对应)
* @param input 输入参数
* @param context 执行上下文(用户信息、会话等)
* @return 执行结果
*/
PluginResponse execute(String capability, Map<String, Object> input,
ExecutionContext context);
/**
* 流式执行AI能力
*/
default reactor.core.publisher.Flux<String> stream(String capability,
Map<String, Object> input,
ExecutionContext context) {
// 默认实现:将同步结果包装成Flux
PluginResponse response = execute(capability, input, context);
return reactor.core.publisher.Flux.just(response.getContent());
}
/**
* 健康检查
*/
default boolean isHealthy() {
return getStatus() == PluginStatus.ACTIVE;
}
// ===================== 枚举 =====================
enum PluginStatus {
UNLOADED, // 未加载
LOADING, // 加载中
LOADED, // 已加载(未激活)
ACTIVE, // 激活可用
DISABLED, // 手动禁用
ERROR // 错误状态
}
}4.2 插件上下文
package com.laozhang.ai.plugin.api;
import lombok.*;
import org.springframework.ai.chat.client.ChatClient;
import java.util.Map;
import java.util.Properties;
/**
* 插件上下文
* 平台向插件提供的运行时环境
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PluginContext {
/** 插件专属配置 */
private Properties pluginConfig;
/** 平台级配置 */
private Properties platformConfig;
/** 平台提供的ChatClient(插件可以使用,也可以自己创建) */
private ChatClient defaultChatClient;
/** 插件数据目录(每个插件独立的文件存储空间) */
private java.io.File dataDirectory;
/** 插件日志目录 */
private java.io.File logDirectory;
/** 平台共享的缓存服务 */
private org.springframework.data.redis.core.StringRedisTemplate redisTemplate;
/** 插件间通信接口 */
private PluginCommunicationBus communicationBus;
}4.3 执行上下文和响应
package com.laozhang.ai.plugin.api;
import lombok.*;
import java.util.Map;
/**
* 执行上下文
* 每次调用携带的运行时信息
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ExecutionContext {
private String userId;
private String sessionId;
private String tenantId; // 租户/业务线标识
private String requestId; // 请求追踪ID
private Map<String, Object> attributes; // 自定义扩展属性
}
/**
* 插件执行响应
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PluginResponse {
public enum Status { SUCCESS, FAILURE, PARTIAL }
private Status status;
private String content;
private Map<String, Object> metadata;
private String errorMessage;
private int inputTokens;
private int outputTokens;
private long elapsedMs;
public static PluginResponse success(String content) {
return PluginResponse.builder()
.status(Status.SUCCESS)
.content(content)
.build();
}
public static PluginResponse failure(String errorMessage) {
return PluginResponse.builder()
.status(Status.FAILURE)
.errorMessage(errorMessage)
.build();
}
}五、SPI机制:Java ServiceLoader在AI插件中的应用
5.1 SPI配置文件
在插件JAR包中,配置SPI声明文件:
// 文件路径:src/main/resources/META-INF/services/com.laozhang.ai.plugin.api.AiCapabilityPlugin
// 文件内容:插件实现类的全限定名
com.laozhang.ai.plugin.ecommerce.EcommerceAiPlugin5.2 SPI插件发现器
package com.laozhang.ai.plugin.loader;
import com.laozhang.ai.plugin.api.AiCapabilityPlugin;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
/**
* 基于Java SPI的插件发现器
* 扫描插件目录,通过ServiceLoader发现插件
*/
@Slf4j
public class SpiPluginDiscovery {
private final String pluginDirectory;
private final Map<String, URLClassLoader> pluginClassLoaders = new HashMap<>();
public SpiPluginDiscovery(String pluginDirectory) {
this.pluginDirectory = pluginDirectory;
}
/**
* 扫描并加载所有插件
*/
public List<AiCapabilityPlugin> discoverPlugins() {
List<AiCapabilityPlugin> plugins = new ArrayList<>();
File dir = new File(pluginDirectory);
if (!dir.exists() || !dir.isDirectory()) {
log.warn("[PLUGIN] Plugin directory not found: {}", pluginDirectory);
return plugins;
}
File[] jarFiles = dir.listFiles(f -> f.getName().endsWith(".jar"));
if (jarFiles == null) return plugins;
for (File jar : jarFiles) {
try {
List<AiCapabilityPlugin> jarPlugins = loadPluginsFromJar(jar);
plugins.addAll(jarPlugins);
log.info("[PLUGIN] Loaded {} plugins from {}", jarPlugins.size(), jar.getName());
} catch (Exception e) {
log.error("[PLUGIN] Failed to load plugin from {}: {}", jar.getName(), e.getMessage());
}
}
return plugins;
}
/**
* 从JAR文件加载插件
* 使用独立的URLClassLoader实现类隔离
*/
private List<AiCapabilityPlugin> loadPluginsFromJar(File jar) throws Exception {
// 关键:为每个插件JAR创建独立的ClassLoader
URL[] urls = {jar.toURI().toURL()};
URLClassLoader pluginClassLoader = new URLClassLoader(
urls,
this.getClass().getClassLoader() // 父ClassLoader:平台ClassLoader
);
// 通过SPI发现插件实现
ServiceLoader<AiCapabilityPlugin> serviceLoader =
ServiceLoader.load(AiCapabilityPlugin.class, pluginClassLoader);
List<AiCapabilityPlugin> plugins = new ArrayList<>();
for (AiCapabilityPlugin plugin : serviceLoader) {
plugins.add(plugin);
// 记录插件到ClassLoader的映射(用于卸载)
pluginClassLoaders.put(plugin.getPluginId(), pluginClassLoader);
}
return plugins;
}
/**
* 卸载插件(释放ClassLoader)
*/
public void unloadPlugin(String pluginId) throws Exception {
URLClassLoader classLoader = pluginClassLoaders.remove(pluginId);
if (classLoader != null) {
classLoader.close();
log.info("[PLUGIN] Plugin ClassLoader closed: {}", pluginId);
}
}
}六、插件注册中心:插件的发现和管理
package com.laozhang.ai.plugin.registry;
import com.laozhang.ai.plugin.api.AiCapabilityPlugin;
import com.laozhang.ai.plugin.api.PluginContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* AI插件注册中心
*
* 职责:
* 1. 插件的注册与注销
* 2. 插件的生命周期管理
* 3. 按能力类型路由到对应插件
* 4. 插件健康状态监控
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class PluginRegistry {
private final PluginContext pluginContext;
private final PluginLoader pluginLoader;
/** 插件存储:pluginId → Plugin */
private final Map<String, AiCapabilityPlugin> plugins = new ConcurrentHashMap<>();
/** 能力到插件的映射:capability → pluginId */
private final Map<String, String> capabilityIndex = new ConcurrentHashMap<>();
/** 读写锁(注册/注销时写锁,查询时读锁) */
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
/** 健康检查线程池 */
private final ScheduledExecutorService healthCheckExecutor =
Executors.newSingleThreadScheduledExecutor(
r -> new Thread(r, "plugin-health-check")
);
@PostConstruct
public void init() {
// 启动时加载所有插件
loadAllPlugins();
// 定期健康检查
healthCheckExecutor.scheduleAtFixedRate(
this::performHealthCheck, 30, 30, TimeUnit.SECONDS
);
log.info("[PLUGIN-REGISTRY] Initialized with {} plugins", plugins.size());
}
/**
* 注册插件
*/
public void registerPlugin(AiCapabilityPlugin plugin) {
rwLock.writeLock().lock();
try {
String pluginId = plugin.getPluginId();
// 检查版本冲突
if (plugins.containsKey(pluginId)) {
AiCapabilityPlugin existing = plugins.get(pluginId);
log.warn("[PLUGIN-REGISTRY] Plugin {} already registered (version {}), " +
"overwriting with version {}",
pluginId, existing.getVersion(), plugin.getVersion());
// 先销毁旧插件
safeDestroy(existing);
}
// 初始化插件
try {
plugin.initialize(pluginContext);
} catch (Exception e) {
log.error("[PLUGIN-REGISTRY] Plugin initialization failed: {}", pluginId, e);
return;
}
// 注册插件
plugins.put(pluginId, plugin);
// 建立能力索引
for (String capability : plugin.getSupportedCapabilities()) {
capabilityIndex.put(capability, pluginId);
log.debug("[PLUGIN-REGISTRY] Capability registered: {} → {}", capability, pluginId);
}
log.info("[PLUGIN-REGISTRY] Plugin registered: {} v{} (capabilities: {})",
pluginId, plugin.getVersion(), plugin.getSupportedCapabilities());
} finally {
rwLock.writeLock().unlock();
}
}
/**
* 注销插件
*/
public void unregisterPlugin(String pluginId) {
rwLock.writeLock().lock();
try {
AiCapabilityPlugin plugin = plugins.remove(pluginId);
if (plugin == null) {
log.warn("[PLUGIN-REGISTRY] Plugin not found: {}", pluginId);
return;
}
// 移除能力索引
plugin.getSupportedCapabilities()
.forEach(capabilityIndex::remove);
// 销毁插件
safeDestroy(plugin);
log.info("[PLUGIN-REGISTRY] Plugin unregistered: {}", pluginId);
} finally {
rwLock.writeLock().unlock();
}
}
/**
* 按能力类型获取插件
*/
public Optional<AiCapabilityPlugin> getPluginByCapability(String capability) {
rwLock.readLock().lock();
try {
String pluginId = capabilityIndex.get(capability);
if (pluginId == null) return Optional.empty();
AiCapabilityPlugin plugin = plugins.get(pluginId);
if (plugin == null || !plugin.isHealthy()) return Optional.empty();
return Optional.of(plugin);
} finally {
rwLock.readLock().unlock();
}
}
/**
* 获取所有已注册插件信息
*/
public List<PluginInfo> getAllPluginInfos() {
rwLock.readLock().lock();
try {
return plugins.values().stream()
.map(p -> PluginInfo.builder()
.pluginId(p.getPluginId())
.pluginName(p.getPluginName())
.version(p.getVersion())
.status(p.getStatus())
.capabilities(p.getSupportedCapabilities())
.healthy(p.isHealthy())
.build())
.toList();
} finally {
rwLock.readLock().unlock();
}
}
private void loadAllPlugins() {
List<AiCapabilityPlugin> discoveredPlugins = pluginLoader.loadAll();
discoveredPlugins.forEach(this::registerPlugin);
}
private void performHealthCheck() {
plugins.forEach((id, plugin) -> {
try {
if (!plugin.isHealthy()) {
log.warn("[PLUGIN-REGISTRY] Plugin health check failed: {}", id);
}
} catch (Exception e) {
log.error("[PLUGIN-REGISTRY] Health check error for {}: {}", id, e.getMessage());
}
});
}
private void safeDestroy(AiCapabilityPlugin plugin) {
try {
plugin.destroy();
} catch (Exception e) {
log.error("[PLUGIN-REGISTRY] Error destroying plugin {}: {}",
plugin.getPluginId(), e.getMessage());
}
}
@PreDestroy
public void shutdown() {
healthCheckExecutor.shutdown();
plugins.values().forEach(this::safeDestroy);
log.info("[PLUGIN-REGISTRY] All plugins destroyed");
}
@lombok.Data
@lombok.Builder
public static class PluginInfo {
private String pluginId;
private String pluginName;
private String version;
private AiCapabilityPlugin.PluginStatus status;
private List<String> capabilities;
private boolean healthy;
}
}七、动态加载:运行时加载/卸载AI插件
package com.laozhang.ai.plugin.loader;
import com.laozhang.ai.plugin.api.AiCapabilityPlugin;
import com.laozhang.ai.plugin.registry.PluginRegistry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 插件热加载器
*
* 功能:
* 1. 定期扫描插件目录
* 2. 自动加载新增插件
* 3. 自动卸载已删除插件
* 4. 支持通过API主动触发热加载
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class PluginHotLoader {
private final PluginRegistry pluginRegistry;
private final SpiPluginDiscovery pluginDiscovery;
@Value("${plugin.directory:/opt/ai-platform/plugins}")
private String pluginDirectory;
@Value("${plugin.hot-reload.enabled:true}")
private boolean hotReloadEnabled;
/** 已加载的插件JAR文件记录:filename → lastModified */
private final Map<String, Long> loadedJars = new ConcurrentHashMap<>();
/**
* 定期扫描插件目录(30秒一次)
*/
@Scheduled(fixedDelayString = "${plugin.hot-reload.scan-interval-seconds:30}000")
public void scanAndReload() {
if (!hotReloadEnabled) return;
File dir = new File(pluginDirectory);
if (!dir.exists()) return;
File[] jarFiles = dir.listFiles(f -> f.getName().endsWith(".jar"));
if (jarFiles == null) return;
Set<String> currentJars = new HashSet<>();
for (File jar : jarFiles) {
String filename = jar.getName();
currentJars.add(filename);
Long lastModified = loadedJars.get(filename);
if (lastModified == null) {
// 新插件
log.info("[HOT-RELOAD] New plugin detected: {}", filename);
loadPlugin(jar);
} else if (lastModified != jar.lastModified()) {
// 插件已更新
log.info("[HOT-RELOAD] Plugin updated: {}", filename);
reloadPlugin(jar);
}
}
// 检查已删除的插件
Set<String> removedJars = new HashSet<>(loadedJars.keySet());
removedJars.removeAll(currentJars);
removedJars.forEach(this::unloadPlugin);
}
/**
* 手动触发插件加载(通过REST API调用)
*/
public boolean manualLoadPlugin(String jarFileName) {
File jar = new File(pluginDirectory, jarFileName);
if (!jar.exists()) {
log.error("[HOT-RELOAD] Plugin file not found: {}", jarFileName);
return false;
}
return loadPlugin(jar);
}
/**
* 手动触发插件卸载
*/
public boolean manualUnloadPlugin(String pluginId) {
try {
pluginRegistry.unregisterPlugin(pluginId);
pluginDiscovery.unloadPlugin(pluginId);
log.info("[HOT-RELOAD] Plugin manually unloaded: {}", pluginId);
return true;
} catch (Exception e) {
log.error("[HOT-RELOAD] Failed to unload plugin: {}", pluginId, e);
return false;
}
}
private boolean loadPlugin(File jar) {
try {
List<AiCapabilityPlugin> plugins = pluginDiscovery.discoverPlugins();
plugins.forEach(pluginRegistry::registerPlugin);
loadedJars.put(jar.getName(), jar.lastModified());
return true;
} catch (Exception e) {
log.error("[HOT-RELOAD] Failed to load plugin {}: {}", jar.getName(), e.getMessage());
return false;
}
}
private void reloadPlugin(File jar) {
// 先卸载,再加载
// 简化实现:实际需要找到该JAR中的插件ID
loadPlugin(jar);
}
private void unloadPlugin(String jarFileName) {
log.info("[HOT-RELOAD] Plugin JAR removed, unloading: {}", jarFileName);
loadedJars.remove(jarFileName);
// 实际需要找到该JAR对应的pluginId并卸载
}
}八、电商插件完整实现示例
package com.laozhang.ai.plugin.ecommerce;
import com.laozhang.ai.plugin.api.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import java.util.*;
/**
* 电商AI能力插件
*
* 支持能力:
* - PRODUCT_DESCRIPTION:商品描述生成
* - REVIEW_ANALYSIS:评论情感分析
* - TITLE_OPTIMIZATION:标题优化
*/
@Slf4j
public class EcommerceAiPlugin implements AiCapabilityPlugin {
private ChatClient chatClient;
private PluginStatus status = PluginStatus.UNLOADED;
private PluginContext pluginContext;
private static final String PLUGIN_ID = "ecommerce-ai-plugin";
private static final String PLUGIN_VERSION = "1.2.0";
@Override
public String getPluginId() { return PLUGIN_ID; }
@Override
public String getPluginName() { return "电商AI能力插件"; }
@Override
public String getVersion() { return PLUGIN_VERSION; }
@Override
public String getDescription() { return "提供商品描述生成、评论分析、标题优化等电商AI能力"; }
@Override
public String getAuthor() { return "老张AI团队"; }
@Override
public List<String> getSupportedCapabilities() {
return List.of("PRODUCT_DESCRIPTION", "REVIEW_ANALYSIS", "TITLE_OPTIMIZATION");
}
@Override
public void initialize(PluginContext context) {
this.status = PluginStatus.LOADING;
this.pluginContext = context;
try {
// 插件可以使用平台提供的ChatClient
// 也可以根据自己的配置创建专属ChatClient
this.chatClient = context.getDefaultChatClient();
// 加载插件特有资源(如提示词模板)
loadPromptTemplates(context);
this.status = PluginStatus.ACTIVE;
log.info("[ECOMMERCE-PLUGIN] Initialized successfully, version={}", PLUGIN_VERSION);
} catch (Exception e) {
this.status = PluginStatus.ERROR;
log.error("[ECOMMERCE-PLUGIN] Initialization failed", e);
throw new RuntimeException("Plugin initialization failed", e);
}
}
@Override
public PluginResponse execute(String capability, Map<String, Object> input,
ExecutionContext context) {
if (status != PluginStatus.ACTIVE) {
return PluginResponse.failure("Plugin not active: " + status);
}
long start = System.currentTimeMillis();
try {
String result = switch (capability) {
case "PRODUCT_DESCRIPTION" -> generateProductDescription(input);
case "REVIEW_ANALYSIS" -> analyzeReviews(input);
case "TITLE_OPTIMIZATION" -> optimizeTitle(input);
default -> throw new IllegalArgumentException(
"Unsupported capability: " + capability);
};
return PluginResponse.builder()
.status(PluginResponse.Status.SUCCESS)
.content(result)
.elapsedMs(System.currentTimeMillis() - start)
.build();
} catch (Exception e) {
log.error("[ECOMMERCE-PLUGIN] Execute failed: capability={}, error={}",
capability, e.getMessage());
return PluginResponse.failure(e.getMessage());
}
}
private String generateProductDescription(Map<String, Object> input) {
String productName = (String) input.getOrDefault("productName", "");
String category = (String) input.getOrDefault("category", "");
String features = (String) input.getOrDefault("features", "");
String targetAudience = (String) input.getOrDefault("targetAudience", "年轻消费者");
String prompt = String.format("""
请为以下商品生成一段吸引人的产品描述:
商品名称:%s
品类:%s
核心特点:%s
目标受众:%s
要求:
1. 200-300字
2. 突出核心卖点
3. 语言生动有吸引力
4. 适合在电商平台展示
""", productName, category, features, targetAudience);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
private String analyzeReviews(Map<String, Object> input) {
@SuppressWarnings("unchecked")
List<String> reviews = (List<String>) input.getOrDefault("reviews", List.of());
String reviewsText = String.join("\n", reviews);
String prompt = String.format("""
请分析以下商品评论,给出情感倾向分析:
%s
请输出JSON格式:
{
"overall_sentiment": "正面/负面/中性",
"positive_points": ["优点1", "优点2"],
"negative_points": ["缺点1", "缺点2"],
"improvement_suggestions": ["建议1", "建议2"],
"score": 0.0
}
""", reviewsText);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
private String optimizeTitle(Map<String, Object> input) {
String originalTitle = (String) input.getOrDefault("originalTitle", "");
String category = (String) input.getOrDefault("category", "");
String prompt = String.format("""
请优化以下商品标题,使其更适合电商搜索和转化:
原标题:%s
品类:%s
要求:
1. 不超过30个字
2. 包含核心关键词
3. 突出差异化卖点
4. 给出3个候选标题
""", originalTitle, category);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
private void loadPromptTemplates(PluginContext context) {
// 从插件数据目录加载自定义提示词模板
// 实际项目中可以支持运营人员通过UI配置提示词
}
@Override
public void destroy() {
this.status = PluginStatus.UNLOADED;
log.info("[ECOMMERCE-PLUGIN] Destroyed");
}
@Override
public PluginStatus getStatus() {
return status;
}
}九、平台核心服务:插件调度与路由
package com.laozhang.ai.service;
import com.laozhang.ai.plugin.api.*;
import com.laozhang.ai.plugin.registry.PluginRegistry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.UUID;
/**
* AI能力平台核心服务
* 负责将业务请求路由到对应的插件
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class AiCapabilityPlatformService {
private final PluginRegistry pluginRegistry;
/**
* 执行AI能力
*
* @param capability 能力类型(如 "PRODUCT_DESCRIPTION")
* @param input 输入参数
* @param userId 用户ID
* @param tenantId 业务线标识
* @return 执行结果
*/
public PluginResponse executeCapability(String capability,
Map<String, Object> input,
String userId,
String tenantId) {
// 1. 查找支持该能力的插件
AiCapabilityPlugin plugin = pluginRegistry.getPluginByCapability(capability)
.orElseThrow(() -> new UnsupportedOperationException(
"No active plugin found for capability: " + capability));
log.info("[PLATFORM] Routing capability={} to plugin={} for tenant={}",
capability, plugin.getPluginId(), tenantId);
// 2. 构建执行上下文
ExecutionContext context = ExecutionContext.builder()
.userId(userId)
.tenantId(tenantId)
.requestId(UUID.randomUUID().toString())
.build();
// 3. 执行插件
PluginResponse response = plugin.execute(capability, input, context);
// 4. 记录执行日志
log.info("[PLATFORM] Capability executed: capability={}, status={}, elapsed={}ms",
capability, response.getStatus(), response.getElapsedMs());
return response;
}
}REST API控制器
package com.laozhang.ai.controller;
import com.laozhang.ai.plugin.api.PluginResponse;
import com.laozhang.ai.plugin.loader.PluginHotLoader;
import com.laozhang.ai.plugin.registry.PluginRegistry;
import com.laozhang.ai.service.AiCapabilityPlatformService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* AI插件平台REST API
*/
@RestController
@RequestMapping("/api/v1/ai-platform")
@RequiredArgsConstructor
public class AiPlatformController {
private final AiCapabilityPlatformService platformService;
private final PluginRegistry pluginRegistry;
private final PluginHotLoader hotLoader;
/**
* 执行AI能力
*/
@PostMapping("/capabilities/{capability}/execute")
public ResponseEntity<PluginResponse> execute(
@PathVariable String capability,
@RequestBody Map<String, Object> input,
@RequestHeader("X-User-Id") String userId,
@RequestHeader("X-Tenant-Id") String tenantId) {
PluginResponse response = platformService.executeCapability(
capability, input, userId, tenantId
);
return ResponseEntity.ok(response);
}
/**
* 查看所有已注册插件
*/
@GetMapping("/plugins")
public ResponseEntity<List<PluginRegistry.PluginInfo>> listPlugins() {
return ResponseEntity.ok(pluginRegistry.getAllPluginInfos());
}
/**
* 热加载插件
*/
@PostMapping("/plugins/load")
public ResponseEntity<String> loadPlugin(@RequestParam String jarFileName) {
boolean success = hotLoader.manualLoadPlugin(jarFileName);
return success ?
ResponseEntity.ok("Plugin loaded successfully") :
ResponseEntity.internalServerError().body("Failed to load plugin");
}
/**
* 卸载插件
*/
@DeleteMapping("/plugins/{pluginId}")
public ResponseEntity<String> unloadPlugin(@PathVariable String pluginId) {
boolean success = hotLoader.manualUnloadPlugin(pluginId);
return success ?
ResponseEntity.ok("Plugin unloaded successfully") :
ResponseEntity.internalServerError().body("Failed to unload plugin");
}
}十、插件配置独立管理
package com.laozhang.ai.plugin.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Properties;
/**
* 插件配置管理服务
*
* 支持:
* 1. 文件配置(静态)
* 2. Redis配置(动态,支持热更新)
* 3. 数据库配置(持久化)
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class PluginConfigService {
private final StringRedisTemplate redisTemplate;
private static final String CONFIG_KEY_PREFIX = "ai:plugin:config:";
/**
* 获取插件配置
* 优先级:Redis动态配置 > 文件配置 > 默认值
*/
public Properties getPluginConfig(String pluginId) {
Properties config = new Properties();
// 1. 加载文件配置
try {
var stream = getClass().getResourceAsStream(
"/plugin-configs/" + pluginId + ".properties"
);
if (stream != null) {
config.load(stream);
}
} catch (Exception e) {
log.warn("[PLUGIN-CONFIG] No file config for plugin: {}", pluginId);
}
// 2. 加载Redis动态配置(覆盖文件配置)
String redisKey = CONFIG_KEY_PREFIX + pluginId;
var dynamicConfigs = redisTemplate.opsForHash().entries(redisKey);
dynamicConfigs.forEach((k, v) -> config.setProperty(k.toString(), v.toString()));
return config;
}
/**
* 动态更新插件配置(触发热更新)
*/
public void updateConfig(String pluginId, String key, String value) {
String redisKey = CONFIG_KEY_PREFIX + pluginId;
redisTemplate.opsForHash().put(redisKey, key, value);
log.info("[PLUGIN-CONFIG] Config updated: plugin={}, key={}", pluginId, key);
// 发布配置变更事件(触发插件重载配置)
redisTemplate.convertAndSend(
"plugin:config:changed",
pluginId + ":" + key + "=" + value
);
}
}十一、性能数据与企业实践
11.1 插件化架构性能影响
在某集团AI平台(日调用量200万次)的压测数据:
| 场景 | 单体架构 | 插件化架构(含路由开销) |
|---|---|---|
| P50延迟 | 1150ms | 1165ms(+15ms) |
| P99延迟 | 3200ms | 3230ms(+30ms) |
| 吞吐量 | 420 RPS | 415 RPS |
| 内存占用 | 512MB | 580MB(+68MB,多5个ClassLoader) |
结论:插件化带来的性能损耗约1.3%,可接受范围内。
11.2 运维指标
| 指标 | 单体架构 | 插件化架构 |
|---|---|---|
| 单个能力发布耗时 | 25分钟(全量发布) | 2分钟(热更新) |
| 某插件故障影响面 | 全平台不可用 | 仅该插件能力不可用 |
| 不同业务线使用不同模型 | 代码条件分支 | 插件内独立配置 |
| 第三方扩展接入时间 | 2周(源码审核) | 2天(实现接口+部署) |
十二、FAQ
Q1:插件ClassLoader隔离会不会导致类型不兼容问题?
A:会,这是ClassLoader隔离的经典问题。解决方案是将共享的接口类(plugin-api模块)放到父ClassLoader中,插件内的实现类放到子ClassLoader。这样平台可以通过接口调用插件,不会有类型不兼容问题。本文的设计中,AiCapabilityPlugin接口由平台ClassLoader加载,实现类由插件ClassLoader加载,符合双亲委派原则。
Q2:插件之间能相互调用吗?
A:通过PluginCommunicationBus可以实现。建议通过pluginId + capability的形式跨插件调用,而不是直接持有对方插件的引用(会导致强耦合和ClassLoader问题)。
Q3:如何防止恶意插件破坏平台?
A:几个措施:1)代码签名验证(只接受可信来源的JAR);2)安全管理器限制插件的文件/网络访问权限;3)资源配额(CPU/内存限制);4)插件审核机制。生产环境中,建议走CI/CD流水线统一构建和部署插件。
Q4:插件热加载时正在处理的请求怎么办?
A:需要实现优雅关闭。卸载插件前,先将其状态设置为DISABLED,拒绝新请求路由到此插件,等待正在处理的请求完成(带超时)后再真正卸载。可以用CompletableFuture配合引用计数实现。
Q5:多节点部署时,热加载如何同步?
A:通过Redis Pub/Sub或MQ广播插件变更事件,所有节点监听并同步执行加载/卸载操作。注意要处理网络分区场景下的状态不一致问题。
结尾
老陈按照插件化架构重构了AI平台。5个业务线变成了5个独立插件,平台核心代码精简到800行。金融业务线更换私有模型只需要发布一个插件JAR,2分钟热更新完成,其他业务线毫无感知。
这就是开闭原则(OCP)在AI系统中的最佳实践:对扩展开放,对修改关闭。
