news 2026/4/15 15:28:05

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/4/12 8:05:10

Thinkphp_Laravel框架开发的vue在线问卷调查系统痕迹

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

作者头像 李华
网站建设 2026/4/9 16:56:43

YOLOv11模型训练新选择:PyTorch+GPU云环境部署指南

YOLOv11模型训练新选择&#xff1a;PyTorchGPU云环境部署指南 在智能安防、自动驾驶和工业质检等场景中&#xff0c;实时目标检测的需求正以前所未有的速度增长。面对复杂多变的视觉任务&#xff0c;开发者不仅需要更高效的模型架构&#xff0c;还必须解决训练过程中的算力瓶颈…

作者头像 李华
网站建设 2026/4/12 22:18:02

Vue.js 过渡 动画

Vue.js 过渡 & 动画 在Vue.js中,过渡和动画是提升用户体验和界面动态效果的重要功能。本文将详细介绍Vue.js中的过渡和动画系统,包括其基本概念、使用方法以及一些高级技巧。 基本概念 过渡 过渡是Vue.js提供的一种在元素插入或删除时自动添加动画效果的方式。它允许…

作者头像 李华
网站建设 2026/4/3 0:07:15

leetcode 1351. 统计有序矩阵中的负数 简单

给你一个 m * n 的矩阵 grid&#xff0c;矩阵中的元素无论是按行还是按列&#xff0c;都以非严格递减顺序排列。 请你统计并返回 grid 中 负数 的数目。示例 1&#xff1a;输入&#xff1a;grid [[4,3,2,-1],[3,2,1,-1],[1,1,-1,-2],[-1,-1,-2,-3]] 输出&#xff1a;8 解释&am…

作者头像 李华
网站建设 2026/4/12 8:29:46

生成何以智能?——基于六十四卦状态空间的原理认知新范式

作者&#xff1a;周林东 摘要&#xff1a;当前人工智能范式在可解释性与泛化能力上面临根本挑战&#xff0c;其根源在于基于“静态实体”的本体论预设。本文主张&#xff0c;智能的突破有赖于转向以“动态生成”为核心的新范式。为此&#xff0c;我们从融贯中国古典生成思想与…

作者头像 李华
网站建设 2026/4/7 17:28:34

Ruby 语法概览

Ruby 语法概览 引言 Ruby 是一种广泛使用的编程语言,以其简洁和优雅著称。它被设计为一种通用语言,适用于多种编程任务,从Web开发到脚本编写,再到数据科学。本文旨在提供一个全面的Ruby语法概览,帮助读者快速了解并掌握Ruby的基础知识。 标准库 Ruby的标准库非常丰富,…

作者头像 李华