SpringBoot动态数据源配置

主要的思路:配置多个数据源加到动态数据源对象中,根据实际的情况动态的切换到相应的数据源。


架构流程图:


SpringBoot动态数据源配置_第1张图片

执行的步骤:建立数据源->数据源加到动态数据源对象->动态数据源的配置->动态切换


1、建立数据源

这一步比较简单,根据连接池(例如:HikariCP、c3p0)建立相关的数据源,本例中使用HikariCP。

@Configuration// 配置数据源
@RefreshScope
public class DataSourceConfigure implements Ordered, EnvironmentAware {

    private final Logger logger = LoggerFactory.getLogger(DataSourceConfigure.class);

    private static final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";

    private Environment environment;

    private Class dataSourceClass;

    public DataSourceConfigure() {
        Class dataSourceClass = null;
        try {
            dataSourceClass = Class.forName(DATASOURCE_TYPE_DEFAULT);
        } catch (ClassNotFoundException e) {
            logger.error("不存在类:" + DATASOURCE_TYPE_DEFAULT);
            e.printStackTrace();
        }
        this.dataSourceClass = dataSourceClass;
    }

    @Bean
    @RefreshScope
    @Primary
    public DataSource xxx() {
        logger.info("注册数据源:xxx");
        return (DataSource) Binder.get(environment).bind( "custom.datasource.xxx", this.dataSourceClass).orElse(null);
    }

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

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

如上面的代码段所示,根据自己的实际内容建立相应的数据源。


2、数据源加到动态数据源对象

这一步把相关的数据源增加到动态数据源对象中,直接上代码。

@Configuration
@EnableAutoConfiguration
public class DatabaseConfiguration {
    @Autowired
    private ApplicationContext appContext;

    private static final String DATASOURCE_PREFIX = "custom.datasource";

    @Bean
    public AbstractRoutingDataSource routingDataSource() {
        RoutingDataSource proxy = new RoutingDataSource();
        Map targetDataSources = new HashMap();
        //获取到所有数据源的名称.
        Map> map = Binder.get(appContext.getEnvironment()).bind(DATASOURCE_PREFIX, Map.class).orElse(null);
        /**
         * 获取每个数据源的属性
         */
        map.forEach((key, value) -> {
            DataSource dataSource = (DataSource) appContext.getBean(key);
            targetDataSources.put(key, dataSource);
            if (value.get("isPrimary") != null && value.get("isPrimary").equals(1)) {
                proxy.setDefaultTargetDataSource(dataSource);
            }
        });
        proxy.setTargetDataSources(targetDataSources);
        return proxy;

    }

}

3、动态数据源的配置(根据实际的使用,配置相关的配置,本例中使用的是Spring-data-jpa。)

下面的代码只是简单的配置一下JPA配置类,使得支持jpa的使用,需要根据实际情况修改。

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef = "entityManagerFactoryDynamic",
        transactionManagerRef = "transactionManagerDynamic",
        basePackages = {"com.xxx.gbimclientdynamic.repository"},//设置Repository所在位置
        repositoryFactoryBeanClass = BaseRepositoryFactoryBean.class
)
public class DynamicConfig {

    @Autowired
    @Qualifier("routingDataSource") //配置中定义的名字
    private DataSource routingDataSource;

    @Bean(name = "entityManagerFactoryDynamic")
    @Primary
    public EntityManagerFactory entityManagerFactoryDynamic() {
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendorAdapter);
        factory.setPackagesToScan("com.xxx.gbimclientdynamic.entity");
        factory.setDataSource(routingDataSource);//数据源
        factory.setPersistenceUnitName("dynamicPersistenceUnit");
        Properties properties = new Properties();
        properties.put("hibernate.show_sql", true);
        properties.put("hibernate.dialect","org.hibernate.dialect.SQLServer2012Dialect");
        factory.setJpaProperties(properties);
        factory.afterPropertiesSet();//在完成了其它所有相关的配置加载以及属性设置后,才初始化
        return factory.getObject();
    }

    @Bean(name = "transactionManagerDynamic")
    @Primary
    PlatformTransactionManager transactionManagerDynamic() {
        return new JpaTransactionManager(entityManagerFactoryDynamic());
    }
}

4、动态切换

这一步的这个例子的重点,这里会说得稍微详细一些。
首先要考虑的就是怎么可以实现切换呢?Spring提供了AbstractRoutingDataSource来实现这样的功能,AbstractRoutingDataSource的功能是在其中可以根据key值动态切换到具体的数据源。
AbstractRoutingDataSource的具体的实现,可以看一下具体的实现代码。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
  ......
  ......
}

由代码中看到AbstractRoutingDataSource 是继承于 AbstractDataSource ,而AbstractDataSource是DataSource 的一个实现类,所以这里主要是要看一下获得连接的方法,即如下:

    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineTargetDataSource().getConnection(username, password);
    }

getConnection()方法中的determineTargetDataSource()明显就是确定数据源的方法,所以我们继续看一下determineTargetDataSource()这个方法。

    /**
     * Retrieve the current target DataSource. Determines the
     * {@link #determineCurrentLookupKey() current lookup key}, performs
     * a lookup in the {@link #setTargetDataSources targetDataSources} map,
     * falls back to the specified
     * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
     * @see #determineCurrentLookupKey()
     */
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }

determineTargetDataSource()中,determineCurrentLookupKey();是获取数据源dataSource的key值的,在AbstractRoutingDataSource类中,determineCurrentLookupKey()是一个抽象方法,就该子类就必须重新该方法。接着根据determineCurrentLookupKey()获得的key值,在this.resolvedDataSources中获得dataSource。如果不存在,就根据默认设置默认的数据源。
所以,根据源码,继承AbstractRoutingDataSource类,并重写其中的determineCurrentLookupKey()方法,就可以实现数据源的切换。

上述内容基本说明了如何切的问题,接下来需要考虑在何时切,并且应该如何实现的问题。
其实这里对简单的就是利用AOP了,AOP的具体内容可以自己去网上找一下,这里就不作详细的介绍了。就是利用AOP在调用需要调用数据库的方法前就设定好需要的数据源(利用上述的determineCurrentLookupKey())。

所以,动态切换的大体思路是这样的:
利用AOP,调用需要调用数据库的方法前利用determineCurrentLookupKey()设定好需要的数据源。

完整代码如下:

//动态数据源存放对象,保存key值,用于动态切换使用
public class DbContextHolder {

    //线程本地环境,ThreadLocal 是用于线性安全的
    private static final ThreadLocal dataSources = new ThreadLocal();


    //设置数据源
    public static void setDataSource(String customerType) {
        dataSources.set(customerType);
    }

    //获取数据源
    public static String getDataSource() {
        return dataSources.get();
    }

    //清除数据源
    public static void clearDataSource() {
        dataSources.remove();
    }

    
}

//用于动态切换,就是上面说的源码的这部分
public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DbContextHolder .getDataSource();
    }

}


//定义一个注解
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DbName {
    String value() default "";
}

//切面
@Aspect
@Component
public class DataSourceAspect {

    private Logger logger = LoggerFactory.getLogger(getClass().getName());


    //作用于service层 ||@args(com.xxx.connection.aop.RoutingDbName)
    //这个匹配能匹配到类,方法,无法匹配到参数
    @Pointcut("execution(public * com.xxx..*.service.impl..*.*(..))&&(@target(com.xxx.connection.aop.DbName)||@annotation(com.xxx.connection.aop.DbName))||execution(public * com.xxx..*.service.impl..*.*(.., @DbName(*), ..))")
    public void routingDsPointCut() {
    }


    //方法切面,不指定注解的值时,第一个参数作为dbName,指定注解的值,可指定数据源,优先级:类->方法->参数
    @Around("routingDsPointCut()")
    public Object proceeRouting(ProceedingJoinPoint joinpoint) throws Throwable {
        logger.info("set connection on service");
        Object result = null;
        MethodSignature signature = (MethodSignature) joinpoint.getSignature();
        Object[] args = joinpoint.getArgs();
        DbName dbName = null;
        String dbStr = "";
        //获取切面方法
        Method method = signature.getMethod();
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        Class[] parameterTypes = method.getParameterTypes();
        //获取参数注解
        for(int i =0;i0){
                DbContextHolder.setDataSource((String)args[0]);
                logger.info("使用第一个参数的值,当前使用"+(String)args[0]+"数据源!");
            }else { //没有指定数据源
                logger.info("注解的值不能为空!使用默认数据源!");
            }
        }else{
            logger.info("service不切换数据源!");
        }
        result = joinpoint.proceed();//执行前后
        logger.info("数据源切换完毕!");
        return result;
    }


   
}



5、拓展

上述的四步已经基本的说明了一个简单的动态数据源切换了,但是上面提交的数据源是已经确定了,如果数据源数据源还不确定的情况下,能不能动态的生成数据源动态的切换呢?答案是肯定的。这就是本部分要说的内容。
其实实现的思路也很简单:在切换前看看有没有需要切换的数据源,如果没有,就根据相关的配置生成。所以,内容基本和上述的四步很类似,唯一不一样的就是继承AbstractRoutingDataSource类重新的determineCurrentLookupKey()方法。直接上代码:

public class RoutingDataSource extends AbstractRoutingDataSource {
    /**
     * 默认值数据源类型,如需要别的数据源需要修改
     */
    private final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";

    /**
     * 别名
     */
    private final static ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
    private Logger logger = LoggerFactory.getLogger(getClass().getName());
    @Override
    protected Object determineCurrentLookupKey() {

        //需要切换的数据库
        String dbName = DbContextHolder.getDataSource();
        if (dbName == null) {
            return null;
        }
        dbName=dbName.toLowerCase();
        try {
            /**
             * 获取AbstractRoutingDataSource的targetDataSources属性,该属性存放数据源属性
             *`
             **/
            Map targetSourceMap = getTargetSource();
            synchronized (this) {
                //判断targetDataSources中是否已经存在要设置的数据源bean
                // 存在的话,则直接返回beanName
                // 不存在的话,则需要建立数据源

                if (!targetSourceMap.keySet().contains(dbName)) {
                    logger.info("数据不存在,建立数据源。");
                    //建立数据源
                    Object dataSource = createDataSource(dbName,EnvironmentUtil.getEnvironment());
                    logger.info("建立数据源成功。");
                    /**
                     * 在创建后的bean,放入到targetDataSources Map中
                     * **/
                    targetSourceMap.put(dbName, dataSource);
                    logger.info("数据源放入到targetDataSources Map中。");
                    //通知spring有bean更新
                    super.afterPropertiesSet();
                    logger.info("通知spring有bean更新");
                }else{
                    logger.info("数据已存在,切换到相应的数据源");
                }

            }

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return DbContextHolder.getDataSource().toLowerCase();
    }

    //获取AbstractRoutingDataSource的targetDataSources属性,该属性存放数据源属性
    @SuppressWarnings("unchecked")
    public Map getTargetSource() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
        Field field = AbstractRoutingDataSource.class.getDeclaredField("targetDataSources");
        field.setAccessible(true);
        return (Map) field.get(this);
    }

    /**
     * 根据数据源信息在spring中创建bean,并返回
     * @param dbName 数据源信息
     * @return 数据源
     * @throws IllegalAccessException
     */
    public Object createDataSource(String dbName,Environment environment) throws Exception {
        Class clazz = Class.forName(DATASOURCE_TYPE_DEFAULT);
        //获取参数
        Map config=getBeanDef(environment,dbName);
        // 通过类型绑定参数并获得实例对象,若数据源修改时,这个需要修改
        DataSource consumerDatasource = bind(clazz, config);
        return consumerDatasource;
    }

    //获取参数
    private Map getBeanDef(Environment environment,String dbName) {
        Map dataSourceMap = (Map)Binder.get(environment).bind("custom.datasource", Map.class).orElse(null);
        if(dataSourceMap != null && !dataSourceMap.isEmpty()) {
            Iterator var4 = dataSourceMap.entrySet().iterator();
            if(var4.hasNext()){
                Map.Entry> entry = (Map.Entry)var4.next();
                String jdbcurl=null!=entry.getValue().get("jdbcurl")?entry.getValue().get("jdbcurl").toString():"";
                jdbcurl=jdbcurl.toLowerCase().replace(entry.getKey().toLowerCase(),dbName);
                entry.getValue().put("jdbcurl",jdbcurl);
                return  entry.getValue();

            }
        }

        return dataSourceMap;
    }
    //绑定数据源
    private  T bind(Class clazz, Map properties) {
        ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
        Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)});
        // 通过类型绑定参数并获得实例对象
        return binder.bind(ConfigurationPropertyName.EMPTY, Bindable.of(clazz)).get();
    }

}

在这里determineCurrentLookupKey()方法与第四步不相同的地方主要是这部分:

   /**
             * 获取AbstractRoutingDataSource的targetDataSources属性,该属性存放数据源属性
             *`
             **/
            Map targetSourceMap = getTargetSource();
            synchronized (this) {
                //判断targetDataSources中是否已经存在要设置的数据源bean
                // 存在的话,则直接返回beanName
                // 不存在的话,则需要建立数据源

                if (!targetSourceMap.keySet().contains(dbName)) {
                    logger.info("数据不存在,建立数据源。");
                    //建立数据源
                    Object dataSource = createDataSource(dbName,EnvironmentUtil.getEnvironment());
                    logger.info("建立数据源成功。");
                    /**
                     * 在创建后的bean,放入到targetDataSources Map中
                     * **/
                    targetSourceMap.put(dbName, dataSource);
                    logger.info("数据源放入到targetDataSources Map中。");
                    //通知spring有bean更新
                    super.afterPropertiesSet();
                    logger.info("通知spring有bean更新");
                }else{
                    logger.info("数据已存在,切换到相应的数据源");
                }

简单的看一下,这部分其实是和刚刚提到的思路完全一致的。首先判断一下有没有需要的数据源(if (!targetSourceMap.keySet().contains(dbName))),如果没有的情况建立数据源(createDataSource(dbName,EnvironmentUtil.getEnvironment())),然后加到AbstractRoutingDataSourcetargetDataSources属性中(targetSourceMap.put(dbName, dataSource);),最后通知spring有bean更新(super.afterPropertiesSet();)。

你可能感兴趣的:(SpringBoot动态数据源配置)