Spring Boot Pf4j模块化开发

Spring Pf4j上下文

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

复制代码
private ApplicationContext createApplicationContext() {

        long startTs = System.currentTimeMillis();

        // Step 1: Pre-create application context
        log.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 refresh
        log.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 logic
        log.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 time
        log.info("Plugin '{}' context fully initialized in {} ms",
                pluginId, System.currentTimeMillis() - startTs);

        return context;
    }

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

Spring控制器动态注册

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

复制代码
public class GJPluginRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    private static final Logger log = LoggerFactory.getLogger(GJPluginRequestMappingHandlerMapping.class);

    @Override
    public 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;
    }

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

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延迟注册,如下为通用方案

复制代码
@Configuration
public class SpringDocOpenApiCfg {

    @Bean
    MultipleOpenApiWebMvcResource 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,代码如下:

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

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

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

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

相关推荐
趁月色小酌***2 小时前
吃透Java核心:从基础语法到并发编程的实战总结
java·开发语言·python
计算机毕设指导62 小时前
基于Django的本地健康宝微信小程序系统【源码文末联系】
java·后端·python·mysql·微信小程序·小程序·django
Ccuno2 小时前
Java中常用的数据结构实现类概念
java·开发语言·深度学习
weixin_440730502 小时前
Java基础学习day02
java·python·学习
曲莫终2 小时前
增强版JSON对比工具类
java·后端·测试工具·json
BD_Marathon2 小时前
Spring——核心概念
java·后端·spring
幽络源小助理2 小时前
SpringBoot+Vue数字科技风险报告管理系统源码 | Java项目免费下载 – 幽络源
java·vue.js·spring boot
ss2732 小时前
线程池配置-七大关键参数
java·开发语言
汉堡包0012 小时前
【网安基础】--Spring/Spring Boot RCE 解析与 Shiro 反序列化漏洞的关联(包括简易加密方式梳理)
学习·安全·spring·信息安全