前言
像插件化的开发,在PHP、JS等系统很常见,但在Java领域非常少,成熟的框架我知道的就是OSGI,记得以前银行项目就是用的这个,比较老的技术了。
在2017年的时候自己也尝试过在博客系统中加入插件功能,达到动态扩展的目的,但只能够实现一些简单功能。
现在发现了一个优秀的框架-"Spring-brick",功能、文档完善。
项目地址:
https://gitee.com/starblues/springboot-plugin-framework-parent
文档地址:
介绍
"Spring-brick",是一个可以动态扩展系统的框架,最早在2019年开始开发,该框架可以在SpringBoot项目上开发插件功能,开发插件就像开发独立应用一样,根据网站的介绍,使用该框架可以实现如下需求:
- 在插件中,您可以当成一个微型的SpringBoot项目来开发,简单易用。
- 在插件中扩展出系统各种功能点,用于系统灵活扩展,再也不用使用分支来交付不同需求的项目了。
- 在插件中可以集成各种框架及其各种spring-boot-xxx-starter。
- 在插件中可以定义独立依赖包了,再也不用在主程序中定义依赖包了。
- 可以完美解决插件包与插件包、插件包与主程序因为同一框架的不同版本冲突问题了。各个插件可以定义同一依赖的不同版本框架。
- 无需重启主程序,可以自由实现插件包的动态安装部署,来动态扩展系统的功能。
- 插件也可以不依赖主程序独立集成微服务模块。
项目更新蛮活跃的:
文档比较丰富:
快速入门
示例地址:
https://gitee.com/starblues/springboot-plugin-framework-example
作者提供了一个项目示例,我们来跑一下看看,跑之前看下文档的快速入门:
1、将sql/plugin-test-example.sql文件导入到mysql数据库 2、修改各个模块对数据库连接配置信息,插件目录地址 3、打包插件: mvn clean package 4、进入主程序example-main 启动 Application
运行日志:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.5.6) [Thread-1] c.g.s.l.launcher.SpringMainBootstrap : Starting SpringMainBootstrap using Java 1.8.0_311 on MacBookAir.local with PID 21618 (/Users/chenjujun/java-projects/springboot-plugin-framework-parent/spring-brick-loader/target/classes started by apple in /Users/chenjujun/java-projects/springboot-plugin-framework-example/example-main) [Thread-1] c.g.s.l.launcher.SpringMainBootstrap : The following profiles are active: dev [Thread-1] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) [Thread-1] o.apache.catalina.core.StandardService : Starting service [Tomcat] [Thread-1] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.54] [Thread-1] o.a.c.c.C.[Tomcat].[localhost].[/]: Initializing Spring embedded WebApplicationContext [Thread-1] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1571 ms Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter. Parsed mapper file: 'file [/Users/chenjujun/java-projects/springboot-plugin-framework-example/example-main/target/classes/mapper/MainUserMapper.xml]' _ _ |_ _ _|_. ___ _ | _ | | |\/|_)(_| | |_\ |_)||_|_\ / | 3.4.1 [Thread-1] pertySourcedRequestMappingHandlerMapping : Mapped URL path [/v2/api-docs] onto method [springfox.documentation.swagger2.web.Swagger2ControllerWebMvc#getDocumentation(String, HttpServletRequest)] [Thread-1] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration) [Thread-1] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' [Thread-1] d.s.w.p.DocumentationPluginsBootstrapper : Documentation plugins bootstrapped [Thread-1] d.s.w.p.DocumentationPluginsBootstrapper : Found 1 custom documentation plugin(s) [Thread-1] s.d.s.w.s.ApiListingReferenceScanner : Scanning for api listing references [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getBeanUsingGET_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getBeanByInterfaceUsingGET_1 [Thread-1] c.g.s.l.launcher.SpringMainBootstrap : Started SpringMainBootstrap in 4.107 seconds (JVM running for 6.445) [Thread-1] c.g.s.i.operator.DefaultPluginOperator : 插件加载环境: dev [Thread-1] c.g.s.i.operator.DefaultPluginOperator : 开始加载插件, 插件根路径为: /Users/chenjujun/java-projects/springboot-plugin-framework-example/example-plugins-basic [Thread-1] c.g.s.s.web.PluginStaticResourceConfig : 插件静态资源访问前缀配置为: /static-plugin/{pluginId} [Thread-1] c.g.s.core.PluginLauncherManager : 插件[example-basic-2@1.0.0-SNAPSHOT]加载成功 [Thread-1] c.g.s.example.listener.MyPluginListener : 插件[example-basic-2]加载成功. [Thread-1] c.g.s.core.PluginLauncherManager : 插件[example-basic-1@1.0.0]加载成功 [Thread-1] c.g.s.example.listener.MyPluginListener : 插件[example-basic-1]加载成功. [Thread-1] c.g.s.e.l.MyPluginInitializerListener : 初始化之前 [Thread-1] c.g.s.b.p.web.PluginControllerProcessor : 插件[example-basic-2]注册接口: {GET [/plugins/example-basic-2/hello/config]} [Thread-1] c.g.s.b.p.web.PluginControllerProcessor : 插件[example-basic-2]注册接口: {GET [/plugins/example-basic-2/hello]} [Thread-1] c.g.s.core.PluginLauncherManager : 插件[example-basic-2@1.0.0-SNAPSHOT]启动成功 [Thread-1] c.g.s.example.listener.MyPluginListener : 插件[example-basic-2]启动成功 [Thread-1] c.g.s.b.p.web.PluginControllerProcessor : 插件[example-basic-1]注册接口: {GET [/plugins/example-basic-1/caller/test-param-map]} [Thread-1] c.g.s.b.p.web.PluginControllerProcessor : 插件[example-basic-1]注册接口: {GET [/plugins/example-basic-1/caller/test]} [Thread-1] c.g.s.b.p.web.PluginControllerProcessor : 插件[example-basic-1]注册接口: {GET [/plugins/example-basic-1/caller/test-param-list]} [Thread-1] c.g.s.b.p.web.PluginControllerProcessor : 插件[example-basic-1]注册接口: {GET [/plugins/example-basic-1/caller/test-param]} [Thread-1] c.g.s.b.p.web.PluginControllerProcessor : 插件[example-basic-1]注册接口: {GET [/plugins/example-basic-1/hello/name]} [Thread-1] c.g.s.b.p.web.PluginControllerProcessor : 插件[example-basic-1]注册接口: {GET [/plugins/example-basic-1/hello]} [Thread-1] c.g.s.b.p.web.PluginControllerProcessor : 插件[example-basic-1]注册接口: {GET [/plugins/example-basic-1/hello/config]} [Thread-1] c.g.s.b.p.web.PluginControllerProcessor : 插件[example-basic-1]注册接口: {GET [/plugins/example-basic-1/main/main-user]} [Thread-1] c.g.s.b.p.web.PluginControllerProcessor : 插件[example-basic-1]注册接口: {GET [/plugins/example-basic-1/thy]} [Thread-1] c.g.s.s.w.PluginStaticResourceResolver : 插件[example-basic-1@1.0.0]配置的静态资源: classpath[[static/]], file[[]] [Thread-1] c.g.s.core.PluginLauncherManager : 插件[example-basic-1@1.0.0]启动成功 [Thread-1] c.g.s.example.listener.MyPluginListener : 插件[example-basic-1]启动成功 [Thread-1] d.s.w.p.DocumentationPluginsBootstrapper : Documentation plugins bootstrapped [Thread-1] d.s.w.p.DocumentationPluginsBootstrapper : Found 1 custom documentation plugin(s) [Thread-1] s.d.s.w.s.ApiListingReferenceScanner : Scanning for api listing references [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getExtractImplUsingGET_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getExtractByInterClassUsingGET_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getExtractByInterClassOfMainUsingGET_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: helloUsingGET_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: helloUsingGET_2 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getConfigUsingGET_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: helloUsingGET_3 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: parseUsingPOST_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: backupPluginUsingPOST_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: installUsingPOST_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: startUsingPOST_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: stopUsingPOST_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: uninstallUsingPOST_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: uploadUsingPOST_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: verifyUsingPOST_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getPluginInfoUsingGET_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getBeanByInterfaceUsingGET_2 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getObjectWithAnnotationUsingGET_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getBeanNameUsingGET_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getBeanUsingGET_2 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getBeanUsingGET_3 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getBeanByInterfaceErrorUsingGET_1 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getBeanByInterfaceUsingGET_3 [Thread-1] .d.s.w.r.o.CachingOperationNameGenerator : Generating unique operation named: getObjectWithAnnotationByPluginIdUsingGET_1 [Thread-1] c.g.s.e.l.MyPluginInitializerListener : 初始化完成 [Thread-1] c.g.s.i.operator.DefaultPluginOperator : 插件初始化完成
这是已经加载完成了。
试试怎么动态部署。
动态部署需要使用prod模式启动,毕竟是要模拟线上环境。
插件目录没有插件的时候是这样的:
[Thread-1] c.g.s.i.operator.DefaultPluginOperator : 插件加载环境: prod [Thread-1] c.g.s.i.operator.DefaultPluginOperator : 开始加载插件, 插件根路径为: ../plugins [Thread-1] c.g.s.s.web.PluginStaticResourceConfig : 插件静态资源访问前缀配置为: /static-plugin/{pluginId} [Thread-1] c.g.s.core.PluginLauncherManager : 以下路径未发现插件: ../plugins 请检查路径是否合适. 请检查配置[plugin.runMode]是否合适. 请检查插件是否合法.
调用下插件上传安装接口:
返回成功了:
看下日志,已经安装成功了:
后面就可以跟调用插件的HTTP接口了。
调用流程
为了能够更好的理解,我整理了一个调用流程图
总结
该框架由于主程序跟插件使用的独立ClassLoader,对于一些bean的共享需要通过配置来实现,比较麻烦,需要仔细看开发文档,一不小心就会插件启动不了报错,不过代码写的很好,是学习的好项目。