news 2026/10/15 20:03:21

Spring Boot Pf4j模块化开发设计方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot Pf4j模块化开发设计方案

前言

上一篇文章还是2年前,一是工作太忙,二是人也变得懒散,好多新东西仅止于脑海里面的印象,未能深入,不成体系,最近主要花了些时间实现Java版本的模块化,同时也要重点兼顾小伙伴们从.NET Core移植模块的成本,所以需要全盘考虑的东西会更加实际,好在有些Java底子加上AI的出现,实现的过程相对会容易一些,最近对AGI提起兴趣,接下来应该会重点学习这方面的应用开发再来和大家分享,好了,话不多说,接下来的系列文章会讲讲Java版本的模块化,和大家一起探讨探讨,或许有更好的一些建议,我能学习到更多。

Spring Pf4j实现效果

我们选择【https://github.com/pf4j/pf4j】作为Java模块化的基础设施,虽然官方作者提供了pf4j-spring的版本基础使用,但能力太弱(主要作者对spring boot好像不是非常熟悉,并没有任何贬低意思,在相关issue作者也做出了表明),尤其是我们还要考虑.NET Core模块的移植,所以不能完全开箱即用,所以我对其进行二次封装。二次封装为Spring版本,注意这里我说的是封装为Spring,而不是SpringBoot,因为SpringBoot是Web应用,而Spring提供了SpringBoot的基础能力,所以我们只需要引入Spring基础包即可,万万不可将SpringBoot全家桶引入到模块化基础设施,这点考虑非常重要。最终插件只需要继承封装的插件类即可

插件开发者可重写beforeApplicationContextRefresh和afterApplicationContextReady,熟悉.NET Core开发的伙伴们应该能猜到等同于ConfigureServices和Configure方法,在before方法里可自定义手动注册相关bean(当然常见的component和bean等注解会自动注册),而after则是上下文刷新完成后可做业务上的初始化工作

Spring Pf4j上下文

每个插件有独立的上下文,所以在启动插件时需创建插件上下文,完成创建插件上下文分为4个步骤,一是初始化上下文,二是提供上述抽象开发者可重写的手动注册,三是刷新插件上下文,四是上述插件利用上下文进行相关业务初始化操作

privateApplicationContext createApplicationContext() {longstartTs =System.currentTimeMillis();//Step 1: Pre-create application contextlog.info("Initializing base context for plugin '{}'", pluginId);longpreCreateStart =System.currentTimeMillis(); AnnotationConfigApplicationContext annotationContext=preCreateApplicationContext(); log.info("Initialized base context for plugin '{}' in {} ms", pluginId, System.currentTimeMillis()-preCreateStart);//Step 2: Customize context before refreshlog.info("Customizing context configuration for plugin '{}'", pluginId);longhandleStart =System.currentTimeMillis(); AnnotationConfigApplicationContext context=beforeApplicationContextRefresh(annotationContext); log.info("Customized context configuration for plugin '{}' in {} ms", pluginId, System.currentTimeMillis()-handleStart);if(context ==null) { context=annotationContext; }//Step 3: Refresh the context (load beans, etc.)log.info("Refreshing Spring context for plugin '{}'", pluginId);longpostCreateStart =System.currentTimeMillis(); postCreateApplicationContext(context); log.info("Refreshed Spring context for plugin '{}' in {} ms", pluginId, System.currentTimeMillis()-postCreateStart);//Step 4: Post-refresh custom logiclog.info("Executing post-refresh logic for plugin '{}'", pluginId);longcustomStart =System.currentTimeMillis(); afterApplicationContextReady(context); log.info("Completed post-refresh logic for plugin '{}' in {} ms", pluginId, System.currentTimeMillis()-customStart);//Total timelog.info("Plugin '{}' context fully initialized in {} ms", pluginId, System.currentTimeMillis()-startTs);returncontext; }

整个步骤最重要的属于初始化插件的上下文,这里贴一下伪代码

Spring控制器动态注册

控制器的动态注册必然是等插件上下文刷新完成后去通过插件上下文获取控制器bean,同时基于控制器的请求处理映射为RequestMappingHandlerMapping,所以我们需要实现自定义的请求处理映射,这里我们暂时只需考虑控制器及其方法的动态注册

publicclassGJPluginRequestMappingHandlerMappingextendsRequestMappingHandlerMapping {privatestaticfinalLogger log = LoggerFactory.getLogger(GJPluginRequestMappingHandlerMapping.class); @OverridepublicvoiddetectHandlerMethods(@NotNull Object controller) {super.detectHandlerMethods(controller); } }

我们将上述自定义请求映射处理作为bean注册到主应用,然后在插件上下文创建完成后,获取注册到主应用的自定义请求处理映射,传入插件,伪代码如下:

GJPluginLifecycle registerController() { GJPluginRequestMappingHandlerMapping pluginRequestMappingHandlerMapping= plugin.getMainApplicationContext()
.getBean("pluginRequestMappingHandlerMapping", GJPluginRequestMappingHandlerMapping.class); pluginRequestMappingHandlerMapping.registerControllers(plugin);returnthis; }

插件上下文获取控制器bean,并将插件控制器bean注册到主应用上下文以及控制器方法注册到自定义的请求处理映射中

publicSet<Object>getControllerBeans(GJPlugin springBootPlugin) { ApplicationContext applicationContext=springBootPlugin.getApplicationContext(); Set<Object> beans =newLinkedHashSet<>(); Map<String, Object> controllerBeans = applicationContext.getBeansWithAnnotation(Controller.class); Map<String, Object> restControllerBeans = applicationContext.getBeansWithAnnotation(RestController.class); beans.addAll(controllerBeans.values()); beans.addAll(restControllerBeans.values());if(log.isTraceEnabled()) { List<String> names =beans.stream() .map(b->b.getClass().getSimpleName()) .collect(Collectors.toList()); log.debug("Scanned {} controller beans: {}", beans.size(), names); }returnbeans; }

我们再来遍历插件中所有控制器列表,进行动态注册即可

SpringDoc-OpenApi

上述为整个模块化或者插件化的设计方案,我们首先需要实现的第一个则是Swagger,将所有插件接口列表能够在主应用启动完成后在swagger页面里呈现出来,但我们插件控制器为动态注册,那么这里如何设计呢,我们一步步来。首先是在主应用引入openapi的包

<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> </dependency>

上述只是主应用定义的控制器已被呈现,但要使得动态注册的插件控制器在主应用启动后也能在swagger中呈现出来,我们还需要完成3个步骤,一是在插件基础设施中引入openapi,插件化基础设施尽可能轻量,无需引入springdoc-openapi-starter-webmvc-ui,建议引入springdoc-openapi-starter-common包即可,如此插件只需对控制器等等打上标签,其他应该都用不到。二是插件注册时需要构建插件控制器的GroupedOpenApi(即每个插件对应一个GroupedOpenApi),并将其注册到主应用上下文,三是主应用需要支持动态注册多GroupedOpenApi。我们重点关注步骤2和步骤3,在主应用yml配置文件中对spring-doc的相关配置过于简单此处忽略不讲,为了实现多模块的动态注册,需要使用springdoc-OpenApi的多GroupedOpenApi延迟注册,如下为通用方案

@ConfigurationpublicclassSpringDocOpenApiCfg { @Bean MultipleOpenApiWebMvcResource multipleOpenApiResource(List<GroupedOpenApi>groupedOpenApis, ObjectFactory<OpenAPIService>defaultOpenAPIBuilder, AbstractRequestService requestBuilder, GenericResponseService responseBuilder, OperationService operationParser, SpringDocConfigProperties springDocConfigProperties, SpringDocProviders springDocProviders, SpringDocCustomizers springDocCustomizers) {returnnewMultipleOpenApiWebMvcResource(groupedOpenApis, defaultOpenAPIBuilder, requestBuilder, responseBuilder, operationParser, springDocConfigProperties, springDocProviders, springDocCustomizers); } }

我们封装插件的注册GroupedOpenApi逻辑,如下:

publicclassGJPluginOpenApiInfo {/*** 获取插件Swagger分组名称(插件ID即为组名)*/publicString getGroupName;publicString getGroupName() {returngetGroupName; }publicvoidsetGroupName(String getGroupName) {this.getGroupName =getGroupName; }/*** 获取插件Controller所在包*/privateList<String>getControllerPackages;publicvoidsetControllerPackages(List<String>getControllerPackages) {this.getControllerPackages =getControllerPackages; }publicList<String>getControllerPackages() {returngetControllerPackages; } }
publicclassGJPluginOpenApiConfig {publicstaticfinalString PLUGIN_SWAGGER_BEAN_PREFIX = "pluginGroupedOpenApi-";publicstaticvoidregisterPluginOpenApiBeans(GJPlugin springBootPlugin, GJPluginOpenApiInfo pluginSwaggerInfo) { String groupName=pluginSwaggerInfo.getGroupName(); groupName=groupName.trim().toLowerCase();if(groupName.trim().isEmpty()) {return; } String beanName= PLUGIN_SWAGGER_BEAN_PREFIX +groupName; String finalGroupName=groupName; GroupedOpenApi groupedOpenApi=GroupedOpenApi.builder() .group(finalGroupName.trim()) .displayName(finalGroupName.trim()) .packagesToScan(pluginSwaggerInfo.getControllerPackages().toArray(newString[0])) .build(); springBootPlugin.registerBeanToMainContext(beanName, groupedOpenApi); } }

在上述我们遍历控制器列表动态注册控制器时,此时调用上述封装注册插件的GroupedOpenApi,代码如下:

我们搞一个Demo插件控制器,看能不能在swagger界面中呈现出来

此时我们发现插件GroupedOpenApi有了,但插件接口列表没有呈现,同时主应用的接口列表悄无声息已无,于是乎开始自定义OpenApiResource调试等等系列操作,底层最后在构建计算接口列表等等时有一个方法引起重要关注

上述严格判断插件控制器方法的bean到底是不是属于对应的控制器,于是我们回过头去看我们动态注册控制器的bean和将控制器的方法注册到请求处理映射的逻辑,如下爱再重点标识一下,以免小伙伴们忘记了

未曾注意到这一细节,我们发现了问题,注册控制器到主应用上下文的bean用的控制器名称,而将控制器方法的注册传入的是控制器对象而不是简单的控制器名称,所以获取到的方法控制器bean则是控制器的hash值,而控制器的bean实际是字符串,所以传入方法的控制器也修改为控制器的名称

总结

如上基于pf4j二次封装的整个设计思路,其中还涉及一些细节并未详细展开,细节主要是对pf4j底层实现的深入了解,然后在封装以及安全等等上做出了进一步的打磨,若有需要了解的小伙伴们,可在评论留言,我们可一起碰撞碰撞思路,本文暂到此为止,感谢阅读。

你所看到的并非事物本身,而是经过诠释后所赋予的意义
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 17:43:26

Thinkphp_Laravel框架开发的vue植物园性毒源成分管理系统_y2201

目录 具体实现截图项目开发技术介绍PHP核心代码部分展示系统结论源码获取/同行可拿货,招校园代理 具体实现截图 本系统&#xff08;程序源码数据库调试部署讲解&#xff09;带文档1万字以上 同行可拿货,招校园代理 Thinkphp_Laravel框架开发的vue植物园性毒源成分管理系统_y2…

作者头像 李华
网站建设 2026/10/15 9:01:00

【计算机毕业设计案例】基于springboot的大学生英语学习平台基于Springboot的在线英语阅读平台的设计与实现(程序+文档+讲解+定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/10/15 14:14:47

GitHub Actions自动化测试PyTorch项目,集成CUDA环境

GitHub Actions自动化测试PyTorch项目&#xff0c;集成CUDA环境 在深度学习项目开发中&#xff0c;一个常见的痛点是&#xff1a;如何确保代码在不同环境中都能稳定运行&#xff1f;尤其是当你的模型依赖GPU加速时&#xff0c;本地能跑通的代码到了CI流水线却频频报错——可能是…

作者头像 李华
网站建设 2026/10/15 14:15:00

Java毕设项目推荐-基于SpringBoot的供应链管理系统的设计与实现采购管理、库存管理、物流管理和客户关系管理【附源码+文档,调试定制服务】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/10/15 14:13:06

Abaqus子弹穿钢板模型:探秘CAE文件背后的奥秘

abaqus子弹穿钢板模型&#xff0c;cae文件在工程模拟领域&#xff0c;Abaqus一直是一款备受青睐的强大工具。今天咱就来唠唠Abaqus子弹穿钢板模型及其核心的CAE文件。 一、Abaqus子弹穿钢板模型简述 子弹穿钢板的模拟&#xff0c;能帮助工程师们了解材料在高速冲击下的力学响应…

作者头像 李华
网站建设 2026/10/15 7:26:53

Carsim与Simulink联合仿真实现弯道变道:从路径规划到轨迹跟踪

carsimsimulink联合仿真实现变道 包含路径规划 carsimsimulink联合仿真实现变道 包含路径规划算法mpc轨迹跟踪算法 可选simulink版本和c版本算法 可以适用于弯道道路&#xff0c;弯道车道保持&#xff0c;弯道变道 carsim内规划轨迹可视化 Carsim2020.0 Matlab2017b在自动驾驶领…

作者头像 李华