SpringFramework 如何在运行期动态注册新的数据源?
借鉴从 0 开始深入学习 Spring - LinkedBear - 掘金小册 (juejin.cn),我觉得这个很棒!!不是打广告
用例:配置多个数据源
0 Spring在启动的时候注册多个数据源
-
启动时动态初始化数据源:在基于SpringFramework / SpringBoot的应用初始化,也就是IOC容器初始化时,读取并解析配置文件,构造多个数据源,并注册到IOC容器中
- 此时通常情况下 IOC 容器还没有刷新完毕,项目还没有启动完成
通过声明一个标注了
@ConfigurationProperties的类,并用Map接受数据源的参数就可以把数据源的定义信息都获取到@Configuration(proxyBeanMethods = true) public class DataSourcePropertyConfig implements ImportBeanDefinitionRegistrar { private static final HashMap<String, DruidDataSource> dataSourceHashMap = new HashMap< >(3); @Bean @Primary @ConfigurationProperties("spring.datasource.db1") public DataSource db1() { System.out.println("db1 creating ......"); return new DruidDataSource(); } @Bean @ConfigurationProperties("spring.datasource.db2") public DataSource db2() { System.out.println("db2 creating ......"); return new DruidDataSource(); } }
#数据源
spring.datasource.druid.db2.url=jdbc:mysql://localhost:3306/db1?characterEncoding=utf8&useSSL=false
spring.datasource.druid.db2.username=root
spring.datasource.druid.db2.password=root
spring.datasource.druid.db2.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.db21.url=jdbc:mysql://localhost:3306/db2?characterEncoding=utf8&useSSL=false
spring.datasource.druid.db21.username=root
spring.datasource.druid.db21.password=root
spring.datasource.druid.db21.driver-class-name=com.mysql.jdbc.Driver
1 运行时期动态注入Bean
1.1 基于BeanDefinition
IOC 中bean的创建来源是BeanDefinition,一般情况下使用
@Bean,@Component,<bean>标签来注册bean,都是先封装成一个个BeanDefinition,然后才根据BeanDefifnition创建Bean对象.
利用回调函数进行注册,实现**BeanFactoryAware** , **ApplicationContextAware**两个接口;
BeanFactoryAware:是对Bean容器的回调
ApplicationContextAware:是上下文的回调
@RestController
public class DemoController implements BeanFactoryAware, ApplicationContextAware {
private DefaultListableBeanFactory beanFactory;
private ConfigurableApplicationContext ctx;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (DefaultListableBeanFactory) beanFactory;
}
@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
this.ctx = (ConfigurableApplicationContext) ctx;
}
@GetMapping(value = "/getdbs")
public String getResource() {
Map<String, DruidDataSource> beansOfType = beanFactory.getBeansOfType(DruidDataSource.class);
System.out.println(beansOfType);
return beansOfType.toString();
}
// 利用BeanDefinitionBuilder进行构造
@RequestMapping("/register1")
public String registerByBeanDefinition() {
beanFactory.removeBeanDefinition("db3");
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(DruidDataSource.class);
builder.addPropertyValue("driverClassName", "com.mysql.jdbc.Driver");
builder.addPropertyValue("url", "jdbc:mysql://localhost:3306/db3?characterEncoding=utf8");
builder.addPropertyValue("username", "root");
builder.addPropertyValue("password", "root");
builder.setScope(ConfigurableListableBeanFactory.SCOPE_SINGLETON);
this.beanFactory.registerBeanDefinition("db3", builder.getBeanDefinition());
return "success";
}
}
1.2 基于SingletonFactory
可以从IOC知识得到,在构建单例Bean的时候,会使用
DefaultSingletonBeanRegistry去承接单例Bean,因此可以将新的数据源注入到单例Bean里。
// 基于SingletonFactory
@RequestMapping("/register2")
public String registerBySingletonFactory() {
if (beanFactory.containsBeanDefinition("db3"))
beanFactory.removeBeanDefinition("db3");
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/db3?characterEncoding=utf8");
dataSource.setUsername("root");
dataSource.setPassword("root");
System.out.println("db3 创建完成!");
beanFactory.registerSingleton("db3", dataSource);
return "success";
}
1.3 依赖注入怎么办呢?
上面都是通过getBean去获取Bean,但是这个时候我们需要使用依赖注入会有什么情况呢,依然使用上述方法注册db3;
@Service
public class DataSourceService {
@Autowired
Map<String, DataSource> dataSourceMap;
public void printDataSources() {
dataSourceMap.forEach((s, dataSource) -> {
System.out.println(s + " ======== " + dataSource);
});
}
}
在controller层输出一下:
@GetMapping(value = "/getdbs")
public String getResource() {
Map<String, DruidDataSource> beansOfType = beanFactory.getBeansOfType(DruidDataSource.class);
System.out.println(beansOfType);
dataSourceService.printDataSources();
return "hehe";
}
控制台打印:
{db1={
CreateTime:"2021-12-28 22:34:59",
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}, db2={
CreateTime:"2021-12-28 22:34:59",
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}, db3={
CreateTime:"2021-12-28 22:35:08",
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}}
db1 ======== {
CreateTime:"2021-12-28 22:34:59",
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}
db2 ======== {
CreateTime:"2021-12-28 22:34:59",
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}
可以发现在service层注入的依然只有db1和db2,db3虽然注入进去,可以getBean获取,但是service层的依赖注入却没有注入db3;
解决
AutowireCapableBeanFactory 的一个作用是框架集成,它提供了一个 autowireBean 方法,用于给现有的对象进行依赖注入:
// 基于SingletonFactory
@RequestMapping("/register2")
public String registerBySingletonFactory() {
if (beanFactory.containsBeanDefinition("db3"))
beanFactory.removeBeanDefinition("db3");
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/db3?characterEncoding=utf8");
dataSource.setUsername("root");
dataSource.setPassword("root");
System.out.println("db3 创建完成!");
beanFactory.registerSingleton("db3", dataSource);
// 重新刷新 DataSourceService 类中的依赖注入
beanFactory.autowireBean(beanFactory.getBean(DataSourceService.class));
return "success";
}
beanFactory.autowireBean(beanFactory.getBean(DataSourceService.class)); 刷新了DataSourceService中的依赖;
控制台打印:
{db1={
CreateTime:"2021-12-28 22:43:19",
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}, db2={
CreateTime:"2021-12-28 22:43:19",
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}, db3={
CreateTime:"2021-12-28 22:43:28",
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}}
db1 ======== {
CreateTime:"2021-12-28 22:43:19",
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}
db2 ======== {
CreateTime:"2021-12-28 22:43:19",
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}
db3 ======== {
CreateTime:"2021-12-28 22:43:28",
ActiveCount:0,
PoolingCount:0,
CreateCount:0,
DestroyCount:0,
CloseCount:0,
ConnectCount:0,
Connections:[
]
}
优化(注解)
如果有很多这种的Service'依赖注入,那岂不是需要修改很多次
这样可以自定义一个注解,通过这个注解获取到需要重新刷新的Bean
注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefreshBean {
}
controller层:
// 基于SingletonFactory
@RequestMapping("/register2")
public String registerBySingletonFactory() {
if (beanFactory.containsBeanDefinition("db3"))
beanFactory.removeBeanDefinition("db3");
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/db3?characterEncoding=utf8");
dataSource.setUsername("root");
dataSource.setPassword("root");
System.out.println("db3 创建完成!");
beanFactory.registerSingleton("db3", dataSource);
// 重新刷新 DataSourceService 类中的依赖注入
Map<String, Object> beansWithAnnotation = beanFactory.getBeansWithAnnotation(RefreshBean.class);
beansWithAnnotation.forEach((name,bean) -> {
beanFactory.autowireBean(bean);
});
return "success";
}
Map<String, Object> beansWithAnnotation = beanFactory.getBeansWithAnnotation(RefreshBean.class);
beansWithAnnotation.forEach((name,bean) -> {
beanFactory.autowireBean(bean);
});
优化(注解+事件)
ApplicationContext 本身也是一个 ApplicationEventPublisher,这样既可以将刷新依赖注入的方法抽取出来,使用事件来监听刷新;
- 自定义一个事件继承
ApplicationContextEvent
public class DynamicEvent extends ApplicationContextEvent {
public DynamicEvent(ApplicationContext source) {
super(source);
}
}
- 创建一个监听器实现
ApplicationListener接口
@Component
public class DynamicListener implements ApplicationListener<DynamicEvent> {
@Override
public void onApplicationEvent(DynamicEvent dynamicEvent) {
ApplicationContext ctx = dynamicEvent.getApplicationContext();
AutowireCapableBeanFactory beanFactory = ctx.getAutowireCapableBeanFactory();
Map<String, Object> beansMap = ctx.getBeansWithAnnotation(RefreshBean.class);
beansMap.values().forEach(beanFactory::autowireBean);
}
}
在controller层发布事件:
// 基于SingletonFactory
@RequestMapping("/register2")
public String registerBySingletonFactory() {
if (beanFactory.containsBeanDefinition("db3"))
beanFactory.removeBeanDefinition("db3");
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/db3?characterEncoding=utf8");
dataSource.setUsername("root");
dataSource.setPassword("root");
System.out.println("db3 创建完成!");
beanFactory.registerSingleton("db3", dataSource);
ctx.publishEvent(new DynamicEvent(ctx));
return "success";
}
ctx.publishEvent(new DynamicEvent(ctx)); 发布事件