核心是主程序仅依赖稳定小接口,运行时通过插件加载器获取接口实例,完全不知具体实现;接口只定义“做什么”,参数用不可变dto,返回统一result,禁用instanceof和类名硬编码,插件由宿主统一纳管生命周期并注入上下文。

核心是让主程序只依赖接口,运行时才决定用哪个插件实现——不是“支持插件”,而是“根本不知道插件是谁”。
定义小而稳的接口契约
接口只说“做什么”,不说“怎么做”。比如日志插件,就只暴露两个方法:
- log(Level level, String msg):接收日志内容
- flush():触发落盘或发送
不放配置加载、线程池创建、HTTP客户端初始化等细节。参数用不可变 DTO 或 List
接口一旦发布,尽量不动。要扩展功能,优先加 default 方法(JDK 8+),或定义子接口如 AsyncLogger extends Logger。
主流程彻底脱离具体类型
业务代码里不能出现任何插件类名、instanceof 判断或强转:
- ✅ 正确写法:Logger logger = pluginLoader.get("elk-logger");
- ❌ 错误写法:ElkLogger logger = new ElkLogger(); 或 if (plugin instanceof DbLogger) { ... }
工厂或加载器的返回值只能是接口类型,比如 PluginLoader.load("auth") 返回 Plugin,而不是 Class>、String 类名,更不是具体实现类。
插件标识用逻辑名(如 "file-logger"),别用全限定类名(如 "com.example.FileLogger"),避免编译期硬编码。
插件由宿主统一纳管生命周期
插件不是 new 出来的,是被宿主加载、初始化、执行、销毁的:
- 按需加载:通过配置(YAML 中 enabled: true)或事件触发,不启动就全量加载
- 标准三阶段:宿主依次调用 plugin.initialize(context) → plugin.execute(input) → plugin.shutdown()
- 失败隔离:某个插件 initialize() 报错,只警告并跳过,不影响其他插件和主流程
插件内部不自己开线程、不启定时器、不查 Spring 上下文。它需要的能力(如 ConfigReader、MetricsReporter)由宿主在创建时注入 PluginContext,上下文本身也按接口隔离,只给它真正需要的。
动态加载需自定义类加载机制
要支持外部 jar 热插拔,得绕过默认双亲委派:
- 用 URLClassLoader 加载插件 jar,构造时指定父加载器为宿主的 ClassLoader(确保接口类由宿主提供)
- 重写 loadClass():优先从插件路径找类;仅对 java.、javax. 等系统包才委派给父加载器
- 这样可避免宿主依赖覆盖插件同名版本(比如宿主用 Guava 31,插件用 Guava 29)
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











