Springboot基于AbstractRoutingDataSource实现多数据源的动态切换

为了提高数据库的查询效率,利用数据库主从机制,写走主库,查询走从库。如果只是实现一主一从类似简单的主从模式,可以继承AbstractRoutingDataSource实现读写分离。而不需使用mycat,sharedingJDBC等数据库插件。

分析AbstractRoutingDataSource可知,defaultTargetDataSource,表示默认的数据源;targetDataSources表示配置的所有数据源集合;afterPropertiesSet方法spring bean对象初始化方法,会把targetDataSources和defaultTargetDataSource,设置为resolvedDataSources和resolvedDefaultDataSource。getConnection()获取jdbc的连接,并通过determineTargetDataSource()获取指定的数据源,AbstractRoutingDataSource使用模板类的模式,在父类定义了determineCurrentLookupKey()虚拟方法,获取lookupkey对象;其子类必须实现该方法。源码如下:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

    /**
     *配置的数据源
     */
	@Nullable
	private Map targetDataSources;

    /**
     *默认数据源
     */
	@Nullable
	private Object defaultTargetDataSource;

    ......

     /**
     *spring InitializingBean 实现方法,bean初始化时调用
     */
    @Override
	public void afterPropertiesSet() {
		if (this.targetDataSources == null) {
			throw new IllegalArgumentException("Property 'targetDataSources' is required");
		}
		this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
		this.targetDataSources.forEach((key, value) -> {
			Object lookupKey = resolveSpecifiedLookupKey(key);
			DataSource dataSource = resolveSpecifiedDataSource(value);
			this.resolvedDataSources.put(lookupKey, dataSource);
		});
		if (this.defaultTargetDataSource != null) {
			this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
		}
	}

    /**
     *获取jdbc链接时,调用determineTargetDataSource,获取指定的数据
     */
    @Override
	public Connection getConnection() throws SQLException {
		return determineTargetDataSource().getConnection();
	}


    /**
     *determineCurrentLookupKey方法通过子类自定义实现,获取lookupKey,然后从resolvedDefaultDataSource map对象中获取数据源
     */
    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;
	}

    /**
     *子类必须实现的获取lookupKey的方法
     */
    protected abstract Object determineCurrentLookupKey();
}

创建DataSourceAddressEnum枚举类,定义MASTER与SLAVE,路由名称。代码如下:

public enum DataSourceAddressEnum {

    /**
     * 主数据库
     */
    MASTER,

    /**
     * 从数据库
     */
    SLAVE;
}

创建DataSourceContextHolder,使用ThreadLocal,定义每次操作的类型枚举,代码如下:

public class DataSourceContextHolder {

    private static final ThreadLocal CONTEXT_HOLDER = ThreadLocal.withInitial(() -> DataSourceAddressEnum.MASTER);

    public static void setCurrentDataSource(DataSourceAddressEnum dataSourceAddressEnum) {
        CONTEXT_HOLDER.set(dataSourceAddressEnum);
    }

    public static DataSourceAddressEnum getCurrentDataSource() {
        return CONTEXT_HOLDER.get();
    }

    public static void removeDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

创建RoutingDataSourceWithAddress,继承AbstractRoutingDataSource,实现determineCurrentLookupKey,即实现了可以根据DataSourceAddressEnum枚举类实现数据源的动态路由,代码如下:

public class RoutingDataSourceWithAddress extends AbstractRoutingDataSource {

    /**
     * @param defaultTargetDataSource 默认的 DataSource
     * @param targetDataSources       配置的所有 DataSource
     */
    public RoutingDataSourceWithAddress(DataSource defaultTargetDataSource, Map targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
    }

    /**
     *配置的数据源
     */
    @Override
    protected Object determineCurrentLookupKey() {

        DataSourceAddressEnum routingDataSourceAddressEnum = DataSourceContextHolder.getCurrentDataSource();
        if (log.isDebugEnabled()) {
            log.debug("routing data source address is {}", routingDataSourceAddressEnum.name());
        }
        return routingDataSourceAddressEnum;
    }
}

使用AOP+注解的方式,对指定的方法进行数据源动态切换的控制。创建RoutingDataSource注解,定义需要路由的数据源,创建RoutingDataSourceAOP定义数据源路由的切面操作,代码如下:

/**
 * DataSource路由注解
 **/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RoutingDataSource {

    /**
     * 路由的DataSource地址,默认为MASTER
     */
    DataSourceAddressEnum value() default DataSourceAddressEnum.MASTER;
}

/**
 * RoutingDataSource 的aop拦截
 **/
@Aspect
@Component
@Order(10000)
@Slf4j
public class RoutingDataSourceAOP {

    @Pointcut("@annotation(com.kuqi.mall.demo.conmon.datasource.RoutingDataSource)|| @within(com.kuqi.mall.demo.conmon.datasource.RoutingDataSource)")
    public void routingDataSourcePointcut() {
    }

    @Around("routingDataSourcePointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        RoutingDataSource routerDataSource = method.getAnnotation(RoutingDataSource.class);
        // 如果没有设置则默认为 MASTER
        DataSourceAddressEnum dataSourceAddressEnum = Objects.isNull(routerDataSource) ? DataSourceAddressEnum.MASTER : routerDataSource.value();
        DataSourceContextHolder.setCurrentDataSource(dataSourceAddressEnum);
        try {
            return joinPoint.proceed();
        } finally {
            DataSourceContextHolder.removeDataSource();
        }
    }
}

创建基于Springboot的自动配置类RoutingDataSourceAutoConfiguration,只要配置了master和slave的属性文件,和mybatis属性文件,就可以自动启动配置。RoutingDataSourceAutoConfiguration源码如下:

@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class, DruidDataSource.class})
@EnableConfigurationProperties(MybatisProperties.class)
public class RoutingDataSourceAutoConfiguration {

    /**
     * 配置master数据源
     */
    @Bean(name = "masterDataSource", initMethod = "init", destroyMethod = "close")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.druid.master")
    public DataSource masterDataSource() {
        DataSource dataSource = DataSourceBuilder.create(this.getClass().getClassLoader())
                .type(com.alibaba.druid.pool.DruidDataSource.class).build();
        return dataSource;
    }

    /**
     * 配置slave数据源
     */
    @Bean(name = "slaveDataSource", initMethod = "init", destroyMethod = "close")
    @ConfigurationProperties(prefix = "spring.datasource.druid.slave")
    public DataSource slaveDataSource() {
        DataSource dataSource = DataSourceBuilder.create(this.getClass().getClassLoader())
                .type(com.alibaba.druid.pool.DruidDataSource.class).build();
        return dataSource;
    }

    /**
     * 初始化路由DataSource
     */
    @Bean(name = "routingDataSourceWithAddress")
    public DataSource dataSource(
            @Autowired @Qualifier("masterDataSource") DataSource masterDataSource,
            @Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource) {

        DataSource defaultTargetDataSource;
        Map targetDataSources = ImmutableMap.of(
                DataSourceAddressEnum.MASTER, defaultTargetDataSource = masterDataSource,
                DataSourceAddressEnum.SLAVE, slaveDataSource);
        return new RoutingDataSourceWithAddress(defaultTargetDataSource, targetDataSources);
    }

    /**
     * 使用SqlSessionFactoryBean配置MyBatis的SqlSessionFactory
     **/
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(
            @Autowired @Qualifier("routingDataSourceWithAddress") DataSource routingDataSourceWithAddress,
            @Autowired MybatisProperties mybatisProperties,
            @Autowired ResourceLoader resourceLoader) throws Exception {

        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(routingDataSourceWithAddress);
        // 设置configuration
        org.apache.ibatis.session.Configuration configuration = mybatisProperties.getConfiguration();
        factory.setConfiguration(configuration);
        // 设置SqlSessionFactory属性
        String configLocation;
        if (StringUtils.isNotBlank(configLocation = mybatisProperties.getConfigLocation())) {
            factory.setConfigLocation(resourceLoader.getResource(configLocation));
        }
        Resource[] resolveMapperLocations;
        if (ArrayUtils.isNotEmpty(resolveMapperLocations = mybatisProperties.resolveMapperLocations())) {
            factory.setMapperLocations(resolveMapperLocations);
        }
        String typeHandlersPackage;
        if (StringUtils.isNotBlank(typeHandlersPackage = mybatisProperties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(typeHandlersPackage);
        }
        String typeAliasesPackage;
        if (StringUtils.isNotBlank(typeAliasesPackage = mybatisProperties.getTypeAliasesPackage())) {
            factory.setTypeAliasesPackage(typeAliasesPackage);
        }
        return factory.getObject();
    }

    /**
     * 使用routingDataSourceWithAddress配置数据库事务
     */
    @Bean
    @ConditionalOnMissingBean
    public DataSourceTransactionManager dataSourceTransactionManager(
            @Autowired @Qualifier("routingDataSourceWithAddress") DataSource routingDataSourceWithAddress) {
        return new DataSourceTransactionManager(routingDataSourceWithAddress);
    }

    /**
     * 编程式事务
     */
    @Bean
    public TransactionTemplate transactionTemplate(
            @Autowired @Qualifier("dataSourceTransactionManager") PlatformTransactionManager platformTransactionManager) {
        return new TransactionTemplate(platformTransactionManager);
    }

    @Bean
    @ConditionalOnMissingBean(RoutingDataSourceAOP.class)
    public RoutingDataSourceAOP rRoutingDataSourceAOP() {
        return new RoutingDataSourceAOP();
    }
}

数据源使用DruidDataSource,详细的配置文件,master和slave数据源,可以配置不同的数据库用来测试,实际开发中,配置为满足数据库主从复制的配置,配置代码如下:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      master:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/kuqi_mall?autoReconnectForPools=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: root
      slave:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/kuqi_mall?autoReconnectForPools=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: root

在springboot的启动类中,屏蔽DruidDataSourceAutoConfigure自动配置类,就能启动。启动类代码,以及操作示例代码如下:

/**
 * springboot启动类exclude DruidDataSourceAutoConfigure
 */
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
@MapperScan(basePackages = {"com.kuqi.mall.demo.dao"})
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

    
     /**
     * 注解定义动态数据源操作
     */
    @Override
    @RoutingDataSource(DataSourceAddressEnum.SLAVE)
    public CouponBo getFromSlave(Long id) {
        return get(id);
    }

完整示例代码,可以参考链接spring-boot-mall项目https://github.com/alldays/spring-boot-mall

你可能感兴趣的:(#,java插件,java,多数据源,springboot,读写分离)