前言

一般而言,我们都知道,在启动一个spring/springboot项目时,随着ioc容器的加载,会向容器中注册许多我们在程序中已经定义好的bean,我们平时注册bean的方式如下:
xml方式:

<beans>//Bean定义的开始和结束
    <import  resource=“resource1.xml” />//导入其他配置文件Bean的定义
    <import  resource=“resource2.xml” />
    <bean id=“bean1” class=“***”></bean>
    <bean name=“bean2” class=“***”></bean>
    <alias alias=“bean3” name=“bean2” />//alias用于定义Bean的别名
</beans>

配置类方式:

@Bean(name = "storage")
    public DataSourceProxy storageDataSourceProxy(@Qualifier("originStorage") DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

上述两种注册bean的方式,都属于静态的,也就是注册的目标bean都已经写死在我们的程序中,且项目一启动时,就会事先加载和创建这些bean,并注册到ioc容器中。
这也导致我们使用的这些bean,只能使用现有配置的属性和行为,不能做超出这之外的功能,这就出现了局限性。

当然,我们也可以将所有需要的属性配置好,在程序运行中,在读取这些配置动态的修改bean的属性,就可以使得这些bean完成其他操作了,无可厚非,这也是一种方法,但是还是有一定的局限性,假设这些配置有几万条呢。。。。

应用场景和解决问题

平常时,已有的静态bean,已经能满足我们工作的大量需求。但由于这些bean的属性和行为都固定了,在特殊需求下,有一定的局限性。
举个我遇到的工作场景,一个项目下需要连接许多库,这些库可以是mysql、oracle、sqlserver、Db2不同类型的库,每种类型下也有很多的库,用户可以根据自己的需求在页面选择目标库,实现切换,并可以对目标库的数据进行维护。
这里我们可以想到,平常时我们一个项目的数据源连接,直接配置在yml就行了。即便多个数据源,也是可以直接配置在yml就行了,但现在这个场景的库不仅一个,也不仅一个类型,有好几百个库,所有仅靠yml的配置,是无法满足这个需求了,这就可以考虑使用动态bean了。

这里,我们可以知道,动态bean主要解决的问题是:在程序执行过程中,动态的配置bean信息且动态的向容器中注册bean,解决静态注册bean的局限性。(只能意会,难以言传)

了解注册Bean的基本原理

我们先不考虑上述的场景,我们先学习如何注册bean。
我们先来参考一位博主的文章,原文链接:https://blog.csdn.net/liuyueyi25/article/details/83244255
下面的内容,我是直接搬过来了,请往下看。

1. 注册bean的核心实现类

我们的实现方式和上面也没什么区别,依然是借助BeanDefinition来创建Bean定义并注册到BeanFactory中,具体实现的核心代码如下:

public class ManualRegistBeanUtil {

     /**
     * 主动向Spring容器中注册bean
     *
     * @param applicationContext Spring容器
     * @param name               BeanName
     * @param clazz              注册的bean的类性
     * @param args               构造方法的必要参数,顺序和类型要求和clazz中定义的一致
     * @param <T>
     * @return 返回注册到容器中的bean对象
     */
    public static <T> T registerBean(ConfigurableApplicationContext applicationContext, String name, Class<T> clazz,
            Object... args) {
        if(applicationContext.containsBean(name)) {
            Object bean = applicationContext.getBean(name);
            if (bean.getClass().isAssignableFrom(clazz)) {
                return (T) bean;
            } else {
                throw new RuntimeException("BeanName 重复 " + name);
            }
        }


        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
        for (Object arg : args) {
            beanDefinitionBuilder.addConstructorArgValue(arg);
        }
        BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();

        BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getBeanFactory();
        beanFactory.registerBeanDefinition(name, beanDefinition);
        return applicationContext.getBean(name, clazz);
    }
}

上面唯一的方法中,接收四个参数,源码中也有说明,稍微需要注意下的是Spring容器中不允许出现同名的Bean.
(我们看了这位博主写的这个方法,BeanDefinitionBuilder 的方式实现注册,而该方法也没有对目标bean做任何操作,直接注册了,这里与静态注册bean的原理相当)

2. bean的定义

a. 不依赖其他Bean

即不依赖其他的Bean, 单纯的供其他地方使用,这种情况下,主要需要测试的就是别人可以通过什么方式来使用它

@Slf4j
public class ManualBean {

    private int id;

    public ManualBean() {
        Random random = new Random();
        id = random.nextInt(100);
    }

    public String print(String msg) {
        return "[ManualBean] print : " + msg + " id: " + id;
    }
}

b. 依赖其他Bean

和前面一个不同,这个Bean内部需要注入其他的Bean,因此我们主动注册Bean时,能否将依赖的Bean也注入进去呢?

定义一个测试Bean

@Slf4j
public class ManualDIBean {

    private int id;

    @Autowired
    private OriginBean originBean;

    private String name;

    public ManualDIBean(String name) {
        Random random = new Random();
        this.id = random.nextInt(100);
        this.name = name;
    }

    public String print(String msg) {
        String o = originBean.print(" call by ManualDIBean! ");
        return "[ManualDIBean] print: " + msg + " id: " + id + " name: " + name + " originBean print:" + o;
    }
}

(显然,是可以的;因为在容器加载时,originBean就已经被注册到容器了,因此这里自然能注入)

c. 普通Bean依赖手动注册的Bean

这个其实就是使用case了,手动注册的Bean也是被人使用的,那可以怎么使用呢?传统的Autowired可否?

@Slf4j
@Component
public class AnoOriginBean {
    // 希望可以注入 主动注册的Bean
    @Autowired
    private ManualBean manualBean;

    public AnoOriginBean() {
        System.out.println("AnoOriginBean init: " + System.currentTimeMillis());
    }

    public String print() {
        return "[AnoOriginBean] print!!! manualBean == null ? " + (manualBean == null);
    }
}

Bean注册实现
前面定义了两个需要手动注册的bean,所以就需要选择一个合适的地方来处理主动注册的逻辑,我们把这段逻辑放在AutoConfig中,用于测试演示

@Configuration
public class BeanRegisterAutoConf {

    public BeanRegisterAutoConf(ApplicationContext applicationContext) {
        System.out.println("BeanRegisterAutoConf init: " + System.currentTimeMillis());
        registerManualBean((ConfigurableApplicationContext) applicationContext);
    }

    /**
     * 手动注册自定义地bean
     * @param applicationContext
     */
    private void registerManualBean(ConfigurableApplicationContext applicationContext) {
        // 主动注册一个没什么依赖的Bean
        ManualBean manualBean = ManualRegistBeanUtil.registerBean(applicationContext, "manualBean", ManualBean.class);
        manualBean.print("test print manualBean");

        // manualDIBean 内部,依赖由Spring容器创建的OriginBean
        ManualDIBean manualDIBean = ManualRegistBeanUtil.registerBean(applicationContext, "manualDIBean",
                ManualDIBean.class, "依赖OriginBean的自定义Bean");
        manualDIBean.print("test print manualDIBean");
    }
}

(这里使用了一个配置类,调用我们之前定义的注册bean的方法,将我们定义的bean注册到容器里了,因此自然可以使用@Autowired)

小结

参考这篇博文,我们已经大致了解了bean的注册原理,但要解决我们上面的场景,仍旧是不行的,因此我们需要使用动态注册bean,动态注册bean也就是在上述的基础上稍稍加一点逻辑而已。

动态注册bean

场景

回忆一下场景:
一个项目下需要连接许多库,这些库可以是mysql、oracle、sqlserver、Db2不同类型的库,每种类型下也有很多的库,用户可以根据自己的需求在页面选择目标库,实现切换,并可以对目标库的数据进行维护。
这里我们可以想到,平常时我们一个项目的数据源连接,直接配置在yml就行了。即便多个数据源,也是可以直接配置在yml就行了,但现在这个场景的库不仅一个,也不仅一个类型,有好几百个库,所有仅靠yml的配置,是无法满足这个需求了,这就可以考虑使用动态bean了。

分析

已知项目使用的数据源是DruidDataSource,若平常时一般的数据源,我们直接在yml里配置即可,项目启动后,springboot就会给我们创建一个数据源对象于容器中供我们使用。但现今,我们不采用这种方式,因此就需要我们通过动态注册bean的方式,实现动态注册数据源。
(目标清楚了:就是实现动态注册DruidDataSource数据源)
我们项目需要连接的库有很多,那么这些库的连接信息,应该存在哪?在哪取?怎么用?
自然的存在数据库,从库里取,所有需要有一个表存储这些信息,对应也需要一个实体类。
(本项目自然会有一个独立的数据库,负责存储我们项目运行需要的信息,那些数据库连接配置信息,自然也是存在这个库里)
由上述两个分析,我们已经能拿到一个动态的数据源了,但问题来了,拿到数据源后怎么用呢?
也许大家平时都用ssm/ssh等框架,使用起来也很简单,操作数据库也就是定义sql写方法即可。但是大家也许不知道,操作数据库这一过程,并非仅有表面调用方法这么简单,我们先看一下原始spring+mybatis配置数据源的xml:

<!--读入配置文件 -->
    <bean id="propertyConfigurer"  class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
            <property name="locations">
                <value>classpath*:jdbc.properties</value>
            </property>
    </bean>
    
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"  destroy-method="close">  
         <property name="driverClassName" value="${jdbc.driverClassName}" />
         <property name="url" value="${jdbc.url}" />
         <property name="username" value="${jdbc.username}" />
         <property name="password" value="${jdbc.password}" />
    </bean>
 <!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 -->  
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">  
        <property name="dataSource" ref="dataSource" /> 
        <!-- 文件映射器,指定类文件 -->
        <property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>  
        <!-- 自动扫描mapping.xml文件 -->  
        <property name="mapperLocations" value="classpath:com/chinasoft/ssm/mapper/*.xml"></property>  
    </bean>   <!--下面两种方法,一种是自动是扫描对应的service类,一种是通过注入sqlsessionFactory的方式来获取一个SqlsessionTemplate--> 
    <!-- DAO接口所在包名,Spring会自动查找其下的类 -->  
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">     <!--set注入--> 
        <property name="basePackage" value="com.chinasoft.ssm.dao"/>  
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>  
    </bean>   
      <!-- 配置SqlSessionTemplate -->     
       <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">       
         <!-- 通过构造函数注入 -->   
               <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory" />      
               </bean>

从这个xml,我们就可以看到,这个xml文件配置了一个sqlSessionFactory的东西,而我们的调用的所有mapper方法去操作数据库,其实都跳不过它,都需要它的支持,它负责了我们程序与数据库的交互。
(我们动态的注册了数据源bean,自然还要动态的注册sqlSessionFactoryBean,不然怎么使用)

实现

下面只展示的几个关键步骤。
动态注册数据源DruidDataSource和session工厂sqlSessionFactoryBean

 private synchronized void initTargetPool(Database database) {      
        String wDataSourceId = database.getName() + database.getSeq() + database.getMode() + DATASOURCE_NAME;    
        String wSqlSessionFactoryId = database.getName() + database.getSeq() + database.getMode();     
        BeanDefinitionBuilder wBeanDefBuilder = null;    
        wBeanDefBuilder = BeanDefinitionBuilder.genericBeanDefinition(MyDataSource.class);     
        wBeanDefBuilder.getBeanDefinition().setAttribute(ID, wDataSourceId);   
        wBeanDefBuilder.setScope(SINGLETON);   
        wBeanDefBuilder.addPropertyValue(URL, database.getUrl());   
        wBeanDefBuilder.addPropertyValue(USERNAME, database.getUser());     
        wBeanDefBuilder.addPropertyValue(DB_PW, database.getPassword());    
        wBeanDefBuilder.addPropertyValue(DRIVERCLASSNAME, EnumType.DriverClass.getValue(database.getType()));    
        wBeanDefBuilder.addPropertyValue(DEFAULTAUTOCOMMIT, false);       
        wBeanDefBuilder.addPropertyValue(INITIALSIZE, 1);      
        wBeanDefBuilder.addPropertyValue(MINIDLE, 1);     
        wBeanDefBuilder.addPropertyValue(MAXACTIVE, 10);  
        wBeanDefBuilder.addPropertyValue("testWhileIdle", false);  
        wBeanDefBuilder.addPropertyValue("testOnBorrow", true);     
        wBeanDefBuilder.addPropertyValue("maxWait", 30000);
        //        wBeanDefBuilder.addPropertyValue("validationQuery", getCheckSql(database.getType()));
        
        Properties properties = new Properties();   
        switch (database.getType()) {       
            case ORACLE:             
                properties.setProperty("v$session.program", "dbmu");       
                break;       
            case DB2:          
                properties.setProperty("clientProgramName", "dbmu");         
                break; 
            case MYSQL:    
                properties.setProperty("connectionAttributes", "program_name:dbmu");          
                break;        
            case SQLSERVER:         
                properties.setProperty("applicationName", "dbmu");           
                break;        
            default:             
                throw new AppException("DBMU000", "不支持的数据库类型");    
        }       
        wBeanDefBuilder.addPropertyValue("connectProperties", properties);   
        
        //注册数据源   
        SpringUtil.getApplicationContext().registerBeanDefinition(wDataSourceId,wBeanDefBuilder.getRawBeanDefinition());  
        
        Object wDataSourceBean = SpringUtil.getApplicationContext().getBean(wDataSourceId);  
        wBeanDefBuilder = BeanDefinitionBuilder.genericBeanDefinition(SqlSessionFactoryBean.class);     
        wBeanDefBuilder.getBeanDefinition().setAttribute(ID, wSqlSessionFactoryId);    
        wBeanDefBuilder.addPropertyValue(DATASOURCE, wDataSourceBean);      
        try {            
            wBeanDefBuilder.addPropertyValue("mapperLocations",new PathMatchingResourcePatternResolver().getResources(String.format(DAO_PATH, database.getType().toLowerCase())));     
        } catch (Exception e) {    
            throw new AppException("DBMU000", "添加数据库配置文件失败:" + e.getMessage());    
        }
       wBeanDefBuilder.addPropertyValue("plugins", interceptorsProvider.getIfAvailable());        
        
        // 注册SqlSessionFactoryId 
        SpringUtil.getApplicationContext().registerBeanDefinition(wSqlSessionFactoryId,wBeanDefBuilder.getRawBeanDefinition());
    }

创建模型

public class Database {
    private String name = "";
    private Integer seq = 0;
    private String type = "";
    private String description = "";
    private String timezone = "";
    private String team = "";
    private String archCode = "";
    private String connectType = "";
    private String instanceName = "";
    private String databaseManager = "";
    private String appDeveloper = "";  
    private String serverHost = "";
    private String serverIp = "";
    private String serverPort = "";
    private String url = "";
    private String user = "";
    private String password = "";
    private String synchronizeTime = "";
}

自然是贴表走,表的sql就不说了。

使用上述注册的bean操作数据库

    public void singleExecute(Database database, String operateType, String executeSql) {   
        DefaultSqlSessionFactory bean = (DefaultSqlSessionFactory) SpringUtil.getApplicationContext().getBean(database.getName() + database.getSeq() + database.getMode());   
        SqlSession sqlSession = bean.openSession();   
        String sql = "";   
        sqlSession.getMapper(ExecuteMapper.class).executeInsert(sql);
    }

mapper方法及xml

@Mapperpublic interface ExecuteMapper {    
	int executeUpdate(@Param("executeSql") String executeSql);    
	int executeDelete(@Param("executeSql") String executeSql);    
	int executeInsert(@Param("executeSql") String executeSql);    
	List<List<Field>> executeQuery(@Param("querySql") String querySql);
}
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cmb.dbmu.dao.database.ExecuteMapper">    
	<update id="executeUpdate" parameterType="java.util.Map">       
		${executeSql}    
	</update>    
	<delete id="executeDelete" parameterType="java.util.Map" >       
		${executeSql}    
	</delete>    
	<insert id="executeInsert" parameterType="java.util.Map" >       
		${executeSql}    
	</insert>    
	<select id="executeQuery" parameterType="java.util.Map" resultType="object">       
		${querySql}    
	</select>
</mapper>

小结

上述的内容就是动态注册bean和使用了。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐