实现SpringBoot动态数据源(1)关系型数据源

文章详细介绍了在SpringBoot2.x中实现动态数据源的步骤,包括数据源注册、创建动态数据源类、数据源切换器、数据源路由配置、事务管理器配置等,并展示了如何在业务层中定义主数据源、初始化数据源、管理数据源以及如何切换数据源。此外,还提供了数据源管理服务和数据源切换的具体实现。
摘要由CSDN通过智能技术生成

1. SpringBoot 2.x 的动态数据源

在Spring Boot 2.x中实现动态数据源的原理是通过动态注册和切换DataSource bean来实现。

1.1 数据源的注册

数据源的注册:在应用启动时,通过配置文件或者代码,创建并注册默认的数据源bean(通常是DataSource的实现类,如HikariDataSource)。这个默认数据源将用于应用的初始数据访问。

1.2 动态数据源类

动态数据源类:创建一个动态数据源类,它继承自AbstractRoutingDataSource。在这个类中,重写determineCurrentLookupKey()方法,根据当前的数据源标识(如线程上下文中的标识)返回要使用的数据源的名称。

1.3 数据源切换器

数据源切换器:创建一个数据源切换器,它是一个工具类,用于在运行时切换数据源。可以使用ThreadLocal或者@Scope注解来实现数据源的线程隔离,将当前线程使用的数据源标识存储在ThreadLocal中。

1.4 数据源路由配置

数据源路由配置:在Spring配置类中,将动态数据源类和数据源切换器配置为Bean,并使用@Primary注解将动态数据源类设置为默认数据源。

1.5 动态数据源切换

动态数据源切换:在需要切换数据源的地方,通过数据源切换器设置当前线程使用的数据源标识。在应用访问数据库之前,动态数据源类会调用determineCurrentLookupKey()方法获取当前数据源的名称,然后根据名称获取对应的数据源bean。

1.6 事务管理器配置

事务管理器配置:为了确保在事务中正确使用动态数据源,需要配置事务管理器。事务管理器需要注入动态数据源作为其数据源。

通过以上的实现方式,当需要切换数据源时,只需在适当的位置调用数据源切换器设置当前线程使用的数据源标识,动态数据源类会根据标识选择相应的数据源。这样就实现了在运行时动态切换数据源的功能。

需要注意的是,在切换数据源后,需要手动重新加载持久层框架的相关配置(如MyBatis的SqlSessionFactory),以确保它们能够正确地使用新的数据源。

2.业务层实现原理

2.1 定义主数据源配置

在配置文件中定义了主数据源的配置信息,包括驱动类、连接URL、用户名和密码等。

#关系型数据库默认动态数据源配置
dynamic:
  datasource:
    sourceId: 2000
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/dynamicdata?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
    username: root
    password: root
    mapperPath: classpath*:mapper/*.xml
    mybatisPlus: true
    plusVersion: 3.5.2

主数据源配置类:

@ConfigurationProperties(prefix = "dynamic.datasource")
@Component
@Data
public class DynamicDataSourceProperties {
	
	private String sourceId;

	/**
	 * Fully qualified name of the JDBC driver. Auto-detected based on the URL by default.
	 */
	private String driverClassName;

	/**
	 * JDBC URL of the database.
	 */
	private String url;

	/**
	 * Login username of the database.
	 */
	private String username;

	/**
	 * Login password of the database.
	 */
	private String password;
	
	private String mapperPath;

	/**
	 * data source type
	 */
	private String dataSourceType;
	
	private boolean ext;

	private boolean mybatisPlus;

}

2.2 注册动态数据源及mybatis/mybatis plus

(1)配置mybatis或mybatis-plus

当前案例的数据源来自DB,所以除了主数据源配置外,还需要提供dao的mybatis或mybatis-plus配置。

spring:
  profiles:
    active: data
  application:
    name: dynamic

#mybatis-plus配置 
mybatis-plus:
  #配置Mapper映射文件
  mapper-locations: classpath*:mapper/*.xml
  global-config:
    banner: false
    #设置逻辑删除(mybatis-plus的 删除 会成为逻辑删除 查询 方法自动过滤删除数据)
    db-config:
      #删除值
      logic-delete-value: 0
      #未删除值
      logic-not-delete-value: 1
  configuration:
    #下划线驼峰命名
    map-underscore-to-camel-case: true
    #开启日志打印
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

server:
  port: 8181

这里把主启动类的配置也一起列出来。

(2)动态数据源的配置类

DynamicDataSourceConfig类中,使用DynamicRoutingDataSource作为数据源,并设置默认数据源为主数据源。

/**
 * @author wsp
 * @create 2023-04-24 22:51
 */
@Configuration
public class DynamicDataSourceConfig {
    @Autowired
    private DynamicDataSourceProperties properties;

    public static String defaultDbKey;

    /**
     * 动态数据源
     */
    @Bean
    public DynamicDataSource dynamicDataSource() {
        DynamicDataSource dataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        dataSource.setTargetDataSources(targetDataSources);
        return dataSource;
    }

    /**
     * 会话工厂
     */
    @Bean
    @Primary
    public FactoryBean sqlSessionFactoryBean() throws IOException {
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        if(!properties.isMybatisPlus()) {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dynamicDataSource());
            org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
            configuration.setMapUnderscoreToCamelCase(true);
            sqlSessionFactoryBean.setConfiguration(configuration);
            sqlSessionFactoryBean.setMapperLocations(resolver.getResources(properties.getMapperPath())); //"classpath*:/repository/*.xml"
            return sqlSessionFactoryBean;
        } else {
            MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
            factoryBean.setDataSource(dynamicDataSource());
            factoryBean.setMapperLocations(resolver.getResources(properties.getMapperPath()));
            GlobalConfig config = new GlobalConfig();
            config.setBanner(false);
            factoryBean.setGlobalConfig(config);
            /*在加载MybatisSqlSessionFactoryBean时,把相应的插件加载进去*/
            factoryBean.setPlugins(paginationInterceptor());
            return factoryBean;
        }

    }

    /*这里不变,还是用来配置一些外部的插件*/
    @Bean
    public MybatisPlusInterceptor paginationInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 指定数据库方言为 H2
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

    /**
     * 事务管理器
     */
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }

    @Bean
    public Object initDefaultDataSource() {
        DynamicDataSourceService.createDataSource(properties);
        defaultDbKey = properties.getSourceId();
        return new Object();
    }
}
  • dynamicDataSource()方法创建了一个DynamicDataSource对象作为动态数据源,并设置一个空的目标数据源集合。

  • sqlSessionFactoryBean()方法用于配置会话工厂,根据properties中的配置判断是否使用MyBatis Plus。如果不使用MyBatis Plus,则创建一个SqlSessionFactoryBean对象(即Mybatis),设置动态数据源作为数据源,配置MyBatis的相关属性,并指定Mapper文件的路径。如果使用MyBatis Plus,则创建一个MybatisSqlSessionFactoryBean对象,同样设置动态数据源作为数据源,并加载Mapper文件的路径。此外,还设置了MyBatis Plus的全局配置和插件。          (可以使用Mybatis Plus 或 Mybatis)

  • paginationInterceptor()方法用于创建一个MybatisPlusInterceptor对象,并添加了一个PaginationInnerInterceptor作为插件,指定数据库方言为MySQL。

  • transactionManager()方法创建了一个DataSourceTransactionManager对象作为事务管理器,设置动态数据源作为数据源。

  • initDefaultDataSource()方法在容器初始化时调用,通过DynamicDataSourceService创建默认的数据源,并将其设置为默认数据源。

这些配置通过使用@Bean注解将相应的对象注册到Spring容器中。整体上,该配置类完成了动态数据源的创建和管理,并配置了会话工厂、事务管理器等相关组件,以支持在应用中使用动态数据源进行数据访问和事务管理。

2.3 初始化主数据源

创建了一个DynamicDataSourceProvider类,该类负责提供数据源,并在初始化阶段调用provide方法创建数据源。

/**
 * @author wsp
 * @create 2023-04-24 22:53
 */
@Component
public class DynamicDataSourceProvider {

    @Autowired
    private DynamicDataSourceProperties dynamicDataSourceProperties;

    @Autowired
    private DynamicDataSourceConfig dynamicDataSourceConfig;
    //
    private List<DataSourceInfo> dataSources;

    @Resource
    private DataSourceInfoMapper dataSourceInfoMapper;

    private DataSource buildDataSource(DynamicDataSourceProperties prop) {
        DataSourceBuilder<?> builder = DataSourceBuilder.create();
        builder.driverClassName(prop.getDriverClassName());
        builder.username(prop.getUsername());
        builder.password(prop.getPassword());
        builder.url(prop.getUrl());
        return builder.build();
    }

    public List<DataSource> provide() {
        List<DataSource> res = new ArrayList<>();
        String initTenantId = "";
        if (dynamicDataSourceProperties != null) {
            DataSource dataSource = buildDataSource(dynamicDataSourceProperties);
            dynamicDataSourceConfig.initDefaultDataSource();
            res.add(dataSource);
        }

        //获取数据源信息
        dataSources = dataSourceInfoMapper.selectList(new LambdaQueryWrapper<DataSourceInfo>().eq(false, DataSourceInfo :: getDriverClassName, "com.mysql.cj.jdbc.Driver"));
        dataSources.forEach(item -> {
            DynamicDataSourceProperties properties = new DynamicDataSourceProperties();
            properties.setSourceId(item.getSourceId());
            properties.setDriverClassName(item.getDriverClassName());
            properties.setUrl(item.getUrl());
            properties.setUsername(item.getUsername());
            properties.setPassword(item.getPassword());
            DataSource dataSource = buildDataSource(properties);
            res.add(dataSource);
            if (!StringUtils.equals(initTenantId, item.getSourceId())) {
                DynamicDataSourceService.createDataSource(properties);
            }
        });
        return res;
    }

    @PostConstruct
    public void init() {
        provide();
    }

}

DynamicDataSourceProvider类中的provide方法实现了数据源的动态添加和切换逻辑。首先创建主数据源,然后从数据库中获取其他数据源的配置信息。每个数据源都使用DynamicDataSourceServicecreateDataSource方法创建并添加到数据源列表中。

以下是代码的分析:

(1)在类上使用`@Component`注解,将该类标记为一个Spring组件。

(2)通过`@Autowired`注解注入`DynamicDataSourceProperties`和`DynamicDataSourceConfig`对象。

(3)定义`dataSources`成员变量,用于存储数据源信息。

(4)使用`@Resource`注解注入`DataSourceInfoMapper`对象,用于操作数据源信息。

(5)实现`buildDataSource()`方法,根据传入的`DynamicDataSourceProperties`对象构建并返回数据源。

(6)实现`provide()`方法,用于提供数据源列表。

  •    创建一个空的结果列表`res`。
  •    如果`dynamicDataSourceProperties`不为null,说明存在初始数据源信息。
    •   调用`buildDataSource(dynamicDataSourceProperties)`方法根据初始数据源信息创建数据源。
    •   调用`dynamicDataSourceConfig.initDefaultDataSource()`方法初始化默认数据源。
    •   将初始数据源添加到结果列表`res`中。
  • 获取数据源信息:
    •  调用`dataSourceInfoMapper.selectList(...)`方法从数据库中查询数据源信息。
    •  对查询结果进行遍历,对每个数据源信息进行处理。
      • 创建一个新的`DynamicDataSourceProperties`对象,并设置相应的属性。
      • 调用`buildDataSource(properties)`方法根据数据源属性创建数据源。
      • 将数据源添加到结果列表`res`中。
      • 如果当前数据源的`sourceId`与`initTenantId`不相等,调用`DynamicDataSourceService.createDataSource(properties)`方法创建数据源。

(7)在`@PostConstruct`注解的方法`init()`中调用`provide()`方法,初始化数据源提供者,在初始化过程中会自动加载初始数据源和从数据库中加载的数据源。

总体上,该代码实现了动态数据源的提供功能。在初始化过程中,会根据初始数据源信息创建数据源,并将其添加到结果列表中。然后从数据库中查询数据源信息,对每个数据源信息进行处理,创建相应的数据源,并将其添加到结果列表中。如果某个数据源的`sourceId`与`initTenantId`不相等,会调用`DynamicDataSourceService.createDataSource(properties)`方法创建数据源。最终,返回结果列表包含所有的数据源。

2.4 数据源管理


/**
 * @author wsp
 * @create 2023-04-24 22:50
 */
@Component
public class DynamicDataSourceService {
    private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceService.class);

    private static final ConcurrentHashMap<Object, Object> dataSources = new ConcurrentHashMap<>();

    /**
     * 用于初始化数据源时加锁
     */
    private final ConcurrentHashMap<String, Object> dataSourceInitLock = new ConcurrentHashMap<>(100);

    private static final ThreadLocal<String> dbKeys = ThreadLocal.withInitial(() -> null);

    /**
     * 动态激活一个数据源并入缓存
     *
     * @param name       数据源的key
     * @param dataSource 数据源对象
     */
    public static void activeDataSourceAndCache(String name, DataSource dataSource) {
        dataSources.put(name, dataSource);
        DynamicDataSource dynamicDataSource = StaticMethodGetBean.getBean(DynamicDataSource.class);
        dynamicDataSource.setTargetDataSources(dataSources);
        dynamicDataSource.afterPropertiesSet();

        log.info("添加了数据源:{}",name);
    }

    /**
     * 切换数据源
     */
    public static void switchDb(String dbKey) {
        dbKeys.set(dbKey);
    }

    /**
     * 重置数据源
     */
    public static void resetDb() {
        dbKeys.remove();
    }

    /**
     * 获取当前数据源
     */
    public static String currentDb() {
        return dbKeys.get();
    }

    /**
     * 创建数据源
     * @param properties
     * @return
     */
    public static DataSource createDataSource(DynamicDataSourceProperties properties) {
        DataSourceBuilder<?> builder = DataSourceBuilder.create();
        builder.driverClassName(properties.getDriverClassName());
        builder.username(properties.getUsername());
        builder.password(properties.getPassword());
        builder.url(properties.getUrl());

        DataSource dataSource = builder.build();
        String sourceId = properties.getSourceId();
        activeDataSourceAndCache(sourceId, dataSource);
        return dataSource;
    }
}

DynamicDataSourceService类是数据源的管理类,提供了创建数据源、切换数据源和移除数据源等方法。在创建数据源时,使用DataSourceBuilder创建数据源对象,并根据配置信息设置驱动类、连接URL、用户名和密码等属性。

2.5 新数据源创建入口

@RestController
@RequestMapping("/dynamic")
public class DynamicController {

    @Autowired
    private DynamicDataSourceService dynamicDataSourceService;

    @Autowired
    private DataSourceInfoMapper dataSourceInfoMapper;

    /**
     * 创建数据源
     * @param dataSourceInfo
     * @return
     */
    @PostMapping("/create")
    public String create(@RequestBody DataSourceInfo dataSourceInfo) {
        DynamicDataSourceProperties properties = new DynamicDataSourceProperties();
        //数据库新增数据源信息
        dataSourceInfoMapper.insert(dataSourceInfo);
        properties.setSourceId(dataSourceInfo.getSourceId());
        properties.setUrl(dataSourceInfo.getUrl());
        properties.setDriverClassName(dataSourceInfo.getDriverClassName());
        properties.setUsername(dataSourceInfo.getUsername());
        properties.setPassword(dataSourceInfo.getPassword());
        //创建动态数据源
        DataSource dataSource = DynamicDataSourceService.createDataSource(properties);
        return FastjsonUtil.getBeanToJson("ok");
    }

}

DynamicController类是一个RESTful接口,用于接收请求并创建数据源。在create方法中,先将新的数据源信息插入数据库,然后调用DynamicDataSourceServicecreateDataSource方法创建数据源,并返回结果。

DynamicController中通过@Autowired注解注入DynamicDataSourceServiceDataSourceInfoMapper,用于动态数据源的管理和数据库操作。

2.6 实现数据源切换

@NoArgsConstructor
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        String currentDb = DynamicDataSourceService.currentDb();
        if (currentDb == null) {
            return DynamicDataSourceConfig.defaultDbKey;
        }
        return currentDb;
    }
}

数据源切换是通过使用AbstractRoutingDataSource实现的。在DynamicRoutingDataSource类中继承了AbstractRoutingDataSource,重写了determineCurrentLookupKey方法。该方法根据线程本地变量中存储的数据源标识,决定当前使用哪个数据源。

2.7 获取bean对象工具类

@Component
public class StaticMethodGetBean<T> implements ApplicationContextAware {
    
	private static ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    	StaticMethodGetBean.applicationContext = applicationContext;
    }
    @Nullable
    public static <T> T  getBean(Class<T> clazz) {
        return applicationContext != null?applicationContext.getBean(clazz):null;
    }
}

这段代码定义了一个名为 StaticMethodGetBean 的类,它实现了 ApplicationContextAware 接口,并使用了 @Component 注解,将其标记为一个 Spring 组件。

代码定义了一个静态变量 applicationContext,用于保存应用程序的上下文。实现了 setApplicationContext 方法,该方法在应用程序上下文被初始化时被调用,并将应用程序上下文赋值给 applicationContext 变量。

实现了一个静态的 getBean 方法,该方法接收一个 Class<T> 类型的参数,用于获取 Spring 容器中的相应类型的 Bean 实例。

  • 在方法内部,通过判断 applicationContext 是否为 null,如果不为 null,则调用 applicationContext.getBean(clazz) 方法来获取对应的 Bean 实例。

  • 如果 applicationContextnull,则返回 null

通过这个工具类,可以在非 Spring 管理的类中通过静态方法调用 getBean 方法来获取 Spring 容器中的 Bean 实例,从而实现非依赖注入的方式获取 Bean 对象。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值