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/15 20:21:49

基于SpringBoot的顺丰仓储管理信息系统的开发与应用

随着物流行业的迅猛发展&#xff0c;高效仓库管理已成为企业提升竞争力的核心要素。在信息技术持续革新的背景下&#xff0c;仓库管理系统作为优化仓储运营的关键工具&#xff0c;发挥着重要作用。顺丰作为物流行业的领军企业&#xff0c;其仓库管理的高效性与精准性备受关注。…

作者头像 李华
网站建设 2026/4/15 13:50:02

Thinkphp_Laravel框架开发的教育平台的设计与实现

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

作者头像 李华
网站建设 2026/4/16 13:03:21

Anaconda Prompt常用命令速查表(PyTorch专用)

Anaconda Prompt常用命令速查表&#xff08;PyTorch专用&#xff09; 在深度学习项目开发中&#xff0c;最让人头疼的往往不是模型结构设计或训练调参&#xff0c;而是环境配置——明明本地跑得好好的代码&#xff0c;换一台机器就报错“CUDA not available”&#xff0c;或者因…

作者头像 李华
网站建设 2026/4/16 12:52:10

无需复杂配置!PyTorch-CUDA基础镜像一键启动GPU训练

无需复杂配置&#xff01;PyTorch-CUDA基础镜像一键启动GPU训练 在深度学习项目中&#xff0c;最让人头疼的往往不是模型设计&#xff0c;而是环境搭建——明明代码写好了&#xff0c;却卡在“CUDA not available”或“版本不兼容”的报错上。你有没有经历过这样的场景&#x…

作者头像 李华
网站建设 2026/4/15 19:25:33

Django Auth:深入理解与最佳实践

Django Auth:深入理解与最佳实践 引言 Django是一个强大的Python Web框架,它提供了一个强大的认证系统,即Django Auth。Django Auth不仅提供了用户认证的基本功能,如用户登录、注销、密码管理等,还支持用户组、权限分配等高级功能。本文将深入探讨Django Auth的原理和使…

作者头像 李华
网站建设 2026/4/15 23:18:38

Git cherry-pick应用场景:将特定修复引入旧版本

Git cherry-pick 应用场景&#xff1a;将特定修复引入旧版本 在现代 AI 工程实践中&#xff0c;一个看似微小的内存泄漏问题&#xff0c;可能让客户环境中的训练任务在数小时后崩溃。而此时你发现&#xff0c;这个 bug 已经在主干分支被修复了——但新功能尚未稳定&#xff0c;…

作者头像 李华