SpringFramework 如何在运行期动态注册新的数据源?

307 阅读4分钟

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)); 发布事件