尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

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

Spring Boot Pf4j模块化开发设计方案
📅 发布时间:2026/6/19 7:08:49

前言

上一篇文章还是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全家桶引入到模块化基础设施,这点考虑非常重要。最终插件只需要继承封装的插件类即可

image

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

Spring Pf4j上下文

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

private ApplicationContext createApplicationContext() {long startTs = System.currentTimeMillis();// Step 1: Pre-create application contextlog.info("Initializing base context for plugin '{}'", pluginId);long preCreateStart = 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);long handleStart = 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);long postCreateStart = 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);long customStart = 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);return context;}

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

image

Spring控制器动态注册 

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

public class GJPluginRequestMappingHandlerMapping extends RequestMappingHandlerMapping {private static final Logger log = LoggerFactory.getLogger(GJPluginRequestMappingHandlerMapping.class);@Overridepublic void detectHandlerMethods(@NotNull Object controller) {super.detectHandlerMethods(controller);}
}

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

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

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

public Set<Object> getControllerBeans(GJPlugin springBootPlugin) {ApplicationContext applicationContext = springBootPlugin.getApplicationContext();Set<Object> beans = new LinkedHashSet<>();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);}return beans;}

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

image

SpringDoc-OpenApi

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

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

image

image

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

@Configuration
public class SpringDocOpenApiCfg {@BeanMultipleOpenApiWebMvcResource multipleOpenApiResource(List<GroupedOpenApi> groupedOpenApis,ObjectFactory<OpenAPIService> defaultOpenAPIBuilder,AbstractRequestService requestBuilder,GenericResponseService responseBuilder,OperationService operationParser,SpringDocConfigProperties springDocConfigProperties,SpringDocProviders springDocProviders,SpringDocCustomizers springDocCustomizers) {return new MultipleOpenApiWebMvcResource(groupedOpenApis,defaultOpenAPIBuilder, requestBuilder,responseBuilder, operationParser,springDocConfigProperties,springDocProviders,springDocCustomizers);}
}

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

public class GJPluginOpenApiInfo {/*** 获取插件Swagger分组名称(插件ID即为组名)*/public String getGroupName;public String getGroupName() {return getGroupName;}public void setGroupName(String getGroupName) {this.getGroupName = getGroupName;}/*** 获取插件Controller所在包*/private List<String> getControllerPackages;public void setControllerPackages(List<String> getControllerPackages) {this.getControllerPackages = getControllerPackages;}public List<String> getControllerPackages() {return getControllerPackages;}
}
public class GJPluginOpenApiConfig {public static final String PLUGIN_SWAGGER_BEAN_PREFIX = "pluginGroupedOpenApi-";public static void registerPluginOpenApiBeans(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(new String[0])).build();springBootPlugin.registerBeanToMainContext(beanName, groupedOpenApi);}
}

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

image

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

image

image

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

image

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

image

image

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

image

image

总结

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

你所看到的并非事物本身,而是经过诠释后所赋予的意义

相关新闻

  • 如何在45分钟内掌握Taichi物理引擎:从新手到专家的完整路径
  • 5分钟零代码上手:AI网页自动化工具实战全解析
  • ComfyUI-LTXVideo视频水印添加终极指南:从版权保护到批量处理

最新新闻

  • 2026年6月大型污水处理厂便携式污泥浓度计十大品牌排名:基于市政水务实测数据的技术量化与选型深度分析 - 仪表品牌榜
  • Loop:重新定义macOS窗口管理的优雅之道
  • 10个高效使用Tag Editor的技巧:批量编辑、脚本处理和自动重命名
  • 2026防火软接实力口碑榜 采购商照着选不踩坑价格透明 - mypinpai
  • compose-for-agents核心组件解析:从Docker容器到MCP工具集的完整架构
  • 深入解析Playwright Java中Browser类:从核心原理到实战应用

日新闻

  • 5分钟掌握Python进化算法:Geatpy高性能优化工具完全指南
  • Microchip 24AA044 EEPROM选型与应用全指南:从参数解析到实战编程
  • 华为的鸿蒙到底有多牛?为什么称作遥遥领先?

周新闻

  • 3步解锁iOS设备:applera1n激活锁绕过完全指南
  • 39 2026 人工智能证书终极盘点,普通人选 AI 证书可以从这些方向入手
  • Redis 暴露公网有多危险?从端口检查到补救步骤

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号