sbp
sbp copied to clipboard
关于spring+p4j实现的讨论
具体不爽的点:
- 如果想使用spring实现一套插件,明明只需要使用spring context即可,为什么要费这么大劲要用springboot来实现,new annotation application context应该更简单
- SpringBootstrap类里列举了这么多auto configuration需要排除,真的 看到这个类我心都凉了半截。有没有一种可能,就是,咱只要用简单的spring context就不需要操心这些
- 明明p4j提供了明确的classloader加载顺序规范(
ClassLoadingStrategy
),我实现的时候也没有用这套。但是SpringBootPluginClassLoader这个类实现的真丑,明明内部loadClass可以按约定先平台、后插件jar,居然还提供pluginFirstClasses让调用者关心细节。SpringBootstrap也没有指定父容器的classLoader
新项目已经被我重新实现了,仍然是spring+p4j,如果想看我也可以放出源码
以及 PluginListableBeanFactory 未传递插件平台的parentBeanFactory,导致插件的feign无法拿到插件平台的load balance
@xinkunZ 大哥能参考下你的代码吗?我也需要做类似这样的需求。。。头大了
@hnzhrh 思路:用annotation application context作为插件的spring context,将插件平台的spring context作为插件的parent,使得插件可以优先从parent中获取bean。 以及,考虑自定义插件的application context的classloader,在加载class时优先从平台(即启动插件的线程的上下文classloader)加载class,找不到再从插件jar( PluginClassLoader )加载class
- 插件start逻辑
applicationContext = new AnnotationConfigApplicationContext();
final String home = StringUtils.substringAfterLast(homePackage, ".");
applicationContext.setBeanNameGenerator(
new PluginBeanNameGenerator(StringUtils.equals(home, "plugin") ? null : home));
ConfigurationPropertiesBindingPostProcessor.register(applicationContext);
applicationContext.setEnvironment(prepareEnvironment());
applicationContext.setParent(this.getMainApplicationContext());
applicationContext.setClassLoader(wrapper.getPluginClassLoader());
applicationContext.scan(homePackage);
applicationContext.refresh();
- 自定义的classloader
public static class MyClassLoader extends PluginClassLoader {
private final ClassLoader current;
MyClassLoader(final PluginManager pluginManager, final PluginDescriptor pluginDescriptor,
final ClassLoader parent) {
super(pluginManager, pluginDescriptor, parent);
this.current = parent;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
Class<?> aClass = current.loadClass(name);
if (aClass != null) {
return aClass;
}
} catch (ClassNotFoundException e) {
return super.loadClass(name);
}
throw new ClassNotFoundException(name);
}
}
抛开情绪性的内容, 你实际上提出了两个很好的问题:
-
为什么基于SpringBoot封装, 而不是直接基于Spring封装一套插件框架, 为什么要费么那大劲去介入Spring Boot的启动过程? 首先基于Spring的pf4j封装, pf4j作者decebals已经提供了pf4j-spring. 同时他也回答过, 他不是一位Java后台开发人员, 对Spring Boot没有开发经验, 无意提供基于Spring Boot的pf4j封装, 所以这才有了sbp的诞生. Spring Boot跟spring-core的区别. 其中最重要最有实现意义的一点, 在于通过properties完成各种中间件组件的集成配置. 比如Datasource, Jpa, Jackson等等几百个库, 上千项配置. sbp这样设计可以实现每个插件的配置独立或共享, 每个插件可以被独立启动以进行自动化测试. 如果应用场景足够简单, 使用spring-core+手工配置即可解决, 那确实无需使用到Spring Boot, 更无需用到sbp.
-
为什么要去折腾ClassLoader控制类加载的顺序? 当初是为了适应Jpa的插件化, 这在文档中有详细解释. 在最新版本的sbp中, Jps(Hibernate)的插件化集成方式改变了, 因此也不再需要用到这个配置项.
你提到的0.1.5版本是2020年旧版本了. sbp在2023年初为了支持spirng boot 3.0和webflux, 进行了一波大重构. 你也可以了解一下.
最后我想说的是. sbp从一开始就有明确的实现目标, 也有比较完整的单元测试覆盖率保证其提供符合预期的功能. 如果在同一实现目标的前提下, 有更好的实现方式, 也欢迎随时提出问题和改进建议. 目前sbp这个项目我利用个人空闲时间用爱发电已经维持了5年时间, 已在多个真实项目中使用验证过, 但目前的状态还是一个比较抽象的底层框架, 使用起来确实仍有不低的门槛, 目前比较成熟的应用场景是提供api服务, 并利用插件化的能力向不同的部署目标提供差异化交付. 我一直希望能够推出像其它"Spring微服务全家桶"那样开箱即用的模板工程, 但迟迟没有找到一个比较完美的前端插件化方案配合. 希望有时间有能力的朋友能加入一起玩.
好了我冷静完了 说一下我对插件的设计思路
我以为,IDEA有自己的插件、eclipse有自己的插件,因此,我们设计的插件系统应该是针对一个指定的服务(插件平台),而为这个服务开发的插件才可以被这个服务加载,插件大部分的依赖、spring bean都应该由平台提供。
常见用例: 平台提供一个interface class,而对此平台开发的插件,可以实现此接口,注册成bean,平台循环所有的插件的context,以上述interface类名拿到所有实现,已达到扩充平台端能力的目的。
那么,因此: 如果插件是springboot,那么引申而来的autoconfig、容器端口等问题过于复杂,并且这样做的意义不明: 既然插件已经是springboot,那为什么不单独启动(docker、jar)
因此,我设计的插件系统的目标:
- 插件系统有两个依赖: 平台api和插件api。平台api是给springboot工程使用,插件api是给插件使用(包含基于p4j的自定义spring plugin父类)
- 插件能够继承平台的spring context(反之见第三点)、可以拿到平台的所有bean;平台可以拿到某插件id的contexct、也可以拿到所有插件的context、各插件的context独立且bean不会冲突
- 允许插件注册bean到平台的context。典型场景:controller。即,允许插件扩充controller给平台,以实现新的reset接口
- 插件所有的能力项基本来自平台,因为context继承的缘故,插件甚至可以无缝使用包括但不限于:平台定义好的数据库、service、mq等等
那么,因此: 如果插件是springboot,那么引申而来的autoconfig、容器端口等问题过于复杂,并且这样做的意义不明: 既然插件已经是springboot,那为什么不单独启动(docker、jar)
正是因为AutoConfiguration复杂, 所以才要让SpringBoot帮我们去做好这些乱七八糟的鸟事. 就拿你提的方案里面第3点举例, 如果不用SpringBoot去配置Controller, 那么就要自己配置aop, 配置security, 配置TransactionManager, 配置ConversionService等等各种看不见但是离不开的东西, 这些工作量都不少的.
基于spring框架的插件化, 其实问题不在于用spring还是spring boot, 还是要根据项目情况和个人习惯去做选择.
插件化的难点其实是在于对各个现在中间件库的插件化改造. 目前主要的中间件库实现基本上都是只考虑在启动时完成配置就用到死, 没留下多少能在运行时更改配置的接口, 很多时间只能通过反射hack掉或整个context重新加载.
org.laxture.sbp.spring.boot.SpringBootstrap
但凡把这个文件打开一眼 我就得三天睡不着觉
如果实现了我说的第3点(插件能够继承平台的spring context),那么插件天然就能得到平台springboot创建出的TransactionManager、ConversionService。插件无需关心创建流程,直接注入即可。 对于插件本身,它的context是直接new出来的AnnotationApplicationContext。插件不需要是fat jar
建议你看看Intellij对插件平台与插件的classpath、classloader定义
对所有想用现成的spring插件框架的人说一句: 没有谁能帮你解决一个复杂的spring二创功能,有能力就自己理清思路手搓一个,没有能力就想想自己真的需要这种插件功能吗是否可以换个方式实现(比如父子独立服务)