TempGo
发布于 2025-11-17 / 6 阅读
0
0

代码量减半!SpringBoot 4.0 的 BeanRegistrar 到底有多香?

先说个真实遇到的问题。在多租户 SaaS 项目中,每个租户可以选择自己的消息推送方式:有的租户用邮件,有的用钉钉,有的用企业微信。

关键是:租户信息存在数据库里,系统启动时才知道有哪些租户,每个租户用什么渠道。

这就麻烦了。用@Bean注解?写不了。你总不能提前知道会有多少个租户,也不知道每个租户用什么配置。

@ConditionalOnProperty?更不行。这玩意儿只能判断配置文件,数据库里的数据它不知道。

这种场景,必须用程序化 Bean 注册

// 伪代码示意
List<Tenant> tenants = loadFromDatabase();  // 从数据库读租户配置
for (Tenant tenant : tenants) {
    if (tenant.getMessageType().equals("email")) {
        register(tenant.getId() + "MessageService", new EmailService(tenant));
    } else if (tenant.getMessageType().equals("dingtalk")) {
        register(tenant.getId() + "MessageService", new DingtalkService(tenant));
    }
}

启动时从数据库读配置,循环注册 Bean。这才是动态 Bean 注册的真实场景。

类似的场景还有很多:

  • • 多数据源:配置中心返回 10 个数据源配置,得循环注册 10 个 DataSource

  • • 插件化系统:扫描 classpath,发现插件 jar 就动态注册对应的 Handler

  • • 灰度发布:调用远程配置服务,根据返回的策略注册不同的实现

共同特点:Bean 的数量、类型、配置,都是运行时才知道的

这时候就得用程序化 Bean 注册——写代码来动态决定注册哪些 Bean。

Spring 以前提供的方案是ImportBeanDefinitionRegistrar。这个名字,光是敲出来就要 26 个字母。

用过的人都知道有多痛苦:实现接口、操作 BeanDefinition、用字符串写类名……代码量大,可读性差,稍不留神还敲错。(如下图 PIGCLOUD里面动态注册网关swagger bean的拼接)

PIG 项目中动态 bean 的实现

Spring Framework 7 终于出手了,带来了新接口:BeanRegistrar

我第一眼看到这 API 就觉得:"早该这样了。"

今天我们来聊聊这个新特性的设计理念和实际用法。顺便对比一下新旧两种写法,你就知道差距有多大了。


BeanRegistrar 是什么?

先看接口定义:

public interface BeanRegistrar {
    void register(BeanRegistry registry, Environment environment);
}

就这么简单。一个方法,两个参数。完事。

  • • BeanRegistry:注册 Bean 用的

  • • Environment:读配置用的

对比一下老的 ImportBeanDefinitionRegistrar

public interface ImportBeanDefinitionRegistrar {
    void registerBeanDefinitions(
        AnnotationMetadata importingClassMetadata,
        BeanDefinitionRegistry registry
    );
}

参数名就够你记的了。更要命的是,想拿 Environment?不好意思,自己实现EnvironmentAware接口去。

新接口的设计思路很清晰:把程序化 Bean 注册这件事做简单


BeanRegistry API 深度解析

新 API 最大的亮点是流式写法

我们看几个对比,你就明白差距有多大了。

2.1 基础注册

新方式(BeanRegistrar):

registry.registerBean("pigUserService", PigUserService.class);

一行。搞定。

旧方式(ImportBeanDefinitionRegistrar):

BeanDefinitionBuilder builder = BeanDefinitionBuilder
    .genericBeanDefinition("com.pig4cloud.pigx.admin.service.impl.PigUserService");
registry.registerBeanDefinition("pigUserService", builder.getBeanDefinition());

类名得用字符串。敲错了?编译通过,运行炸。

2.2 带配置的注册

新方式

registry.registerBean("pigOrderService", PigOrderService.class, spec -> spec
    .prototype()           // 原型作用域,每次获取都是新实例
    .lazyInit()           // 延迟初始化,用到才创建
    .description("PIG 订单服务")  // 自定义描述
);

链式调用,配置一目了然。

旧方式

BeanDefinitionBuilder builder = BeanDefinitionBuilder
    .genericBeanDefinition("com.pig4cloud.pigx.mall.service.impl.PigOrderService");
builder.setScope("prototype");           // 要记住是"prototype"字符串
builder.setLazyInit(true);               // setter 方式
builder.setDescription("PIG 订单服务");    // 又是 setter

registry.registerBeanDefinition("pigOrderService", builder.getBeanDefinition());

7 行才搞定。还得记一堆 setter 方法名。

2.3 自定义创建逻辑(带依赖注入)

新方式

registry.registerBean("pigGoodsService", PigGoodsService.class, spec -> spec
    .supplier(context -> {
        // context 可以获取其他已注册的 Bean
        PigUserService userService = context.bean(PigUserService.class);
        return new PigGoodsService(userService);
    })
);

简洁清晰,依赖关系一眼看出来。

旧方式

BeanDefinitionBuilder builder = BeanDefinitionBuilder
    .genericBeanDefinition("com.pig4cloud.pigx.mall.service.impl.PigGoodsService");

// 需要手动设置构造函数参数或属性引用
builder.addConstructorArgReference("pigUserService");  // 还得知道 Bean 的名字
// 或者用更复杂的方式
builder.setFactoryMethod("createInstance");
builder.addPropertyReference("userService", "pigUserService");

registry.registerBeanDefinition("pigGoodsService", builder.getBeanDefinition());

一堆 API 要查文档。还容易搞混构造函数注入和属性注入。

2.4 条件注册

新方式

// Environment 直接就在参数里
if (environment.matchesProfiles("production")) {
    registry.registerBean(PigCacheService.class);
}

// 支持任意复杂的逻辑
if (environment.getProperty("pig.cache.enabled", Boolean.class, false)) {
    String cacheType = environment.getProperty("pig.cache.type", "redis");
    if ("redis".equals(cacheType)) {
        registry.registerBean(PigRedisCacheService.class);
    } else {
        registry.registerBean(PigLocalCacheService.class);
    }
}

想怎么判断就怎么判断。if、for、switch 随便用。

旧方式

// 先得实现 EnvironmentAware 接口
private Environment environment;

@Override
public void setEnvironment(Environment environment) {
    this.environment = environment;
}

// 然后在 registerBeanDefinitions 方法里
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    if (environment.matchesProfiles("production")) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder
            .genericBeanDefinition("com.pig4cloud.pigx.common.cache.PigCacheService");
        registry.registerBeanDefinition("pigCacheService", builder.getBeanDefinition());
    }
}

多实现一个接口,还得维护 Environment 的状态。麻烦。

看完这 4 个对比,你应该感受到差距了。新 API 不只是"好看一点",是真正让代码简单了


实战案例

回到前言提到的场景:根据配置动态选择消息推送方式。

我们用一个简化版本来演示:系统根据配置文件选择邮件或短信服务。虽然简单,但已经足够说明 BeanRegistrar 的用法。

3.1 定义服务接口


/**
 * 消息服务接口
 *
 * @author lengleng
 */
public interface PigMessageService {

    /**
     * 发送消息
     * @param content 消息内容
     * @return 发送结果
     */
    String sendMessage(String content);

    /**
     * 获取服务类型
     * @return 服务类型标识
     */
    String getServiceType();
}

3.2 实现类

/**
 * 邮件消息服务
 *
 * @author lengleng
 */
public class PigEmailMessageService implements PigMessageService {

    @Override
    public String sendMessage(String content) {
        // 实际项目这里会调用邮件发送 SDK
        return "邮件发送成功,时间:" + LocalDateTime.now() + ",内容:" + content;
    }

    @Override
    public String getServiceType() {
        return "EMAIL";
    }
}
/**
 * 短信消息服务
 *
 * @author lengleng
 */
public class PigSmsMessageService implements PigMessageService {

    @Override
    public String sendMessage(String content) {
        // 实际项目这里会调用短信网关
        return "短信发送成功,时间:" + LocalDateTime.now() + ",内容:" + content;
    }

    @Override
    public String getServiceType() {
        return "SMS";
    }
}

3.3 旧方式:ImportBeanDefinitionRegistrar

先看以前怎么写的:

/**
 * 消息服务注册器 - 旧方式
 *
 * @author lengleng
 */
public class PigMessageServiceRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    private Environment environment;

    @Override
    public void setEnvironment(Environment environment) {
        // 需要实现 EnvironmentAware 才能拿到 Environment
        this.environment = environment;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        String messageType = environment.getProperty("pig.message.type", "email");

        BeanDefinitionBuilder builder;

        switch (messageType.toLowerCase()) {
            case "email":
                builder = BeanDefinitionBuilder.genericBeanDefinition(
                    "com.pig4cloud.pigx.common.message.impl.PigEmailMessageService"
                );
                break;
            case "sms":
                builder = BeanDefinitionBuilder.genericBeanDefinition(
                    "com.pig4cloud.pigx.common.message.impl.PigSmsMessageService"
                );
                break;
            default:
                throw new IllegalArgumentException("未知的消息类型:" + messageType);
        }

        // 设置 Bean 属性
        builder.setScope("singleton");
        builder.setLazyInit(false);

        // 注册 Bean 定义
        registry.registerBeanDefinition("pigMessageService", builder.getBeanDefinition());
    }
}

数一数问题:

  1. 1. 要实现EnvironmentAware接口才能获取配置——多写一个接口

  2. 2. 操作BeanDefinitionBuilder——代码繁琐

  3. 3. 类名用字符串——敲错了编译不报错,运行才炸

  4. 4. setter 方式设置属性——不够直观

54 行代码。就为了根据配置选择一个实现类。

3.4 新方式:BeanRegistrar

再看新写法:

/**
 * 消息服务注册器 - 新方式
 *
 * @author lengleng
 */
public class PigMessageServiceRegistrar implements BeanRegistrar {

    @Override
    public void register(BeanRegistry registry, Environment environment) {
        String messageType = environment.getProperty("pig.message.type", "email");

        switch (messageType.toLowerCase()) {
            case "email" -> registry.registerBean(
                "pigMessageService",
                PigEmailMessageService.class,
                spec -> spec.description("PIG 邮件消息服务")
            );
            case "sms" -> registry.registerBean(
                "pigMessageService",
                PigSmsMessageService.class,
                spec -> spec.description("PIG 短信消息服务")
            );
            default -> throw new IllegalArgumentException("未知的消息类型:" + messageType);
        }
    }
}

27 行代码。代码量减少一半。

更重要的是:

  • • Environment 直接传进来,不用实现额外接口

  • • 直接用 Class 引用,类型安全,写错了编译就报错

  • • 流式 API,配置清晰

  • • 专注业务逻辑,而不是框架细节

3.5 激活配置

/**
 * 消息服务配置
 *
 * @author lengleng
 */
@Configuration
@Import(PigMessageServiceRegistrar.class)
public class PigMessageAutoConfiguration {
    // 其他@Bean 定义可以共存
}

配置文件:

pig:
  message:
    type: email  # 可选:email、sms

想切换?改一下配置,重启就行。

总结

Spring 这个 20 岁的老江湖,反倒越活越灵了。在 Spring Boot 4 里,新出的 BeanRegistrar 接口把过去“程序化注册 Bean”这件麻烦事,给理顺得干干净净。

它不是什么颠覆级更新,但确实把开发体验往前推了一大步。

以后再碰到需要动态注册 Bean的场景,不妨直接上 BeanRegistrar,让代码更优雅。

本文转载自:https://mp.weixin.qq.com/s/iglyKb6-ZQsKOirW8DDA9g


评论