先说个真实遇到的问题。在多租户 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的拼接)
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. 要实现
EnvironmentAware接口才能获取配置——多写一个接口2. 操作
BeanDefinitionBuilder——代码繁琐3. 类名用字符串——敲错了编译不报错,运行才炸
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