springboot动态数据源使用

    技术2023-07-04  78

    springboot动态数据源使用

    动态数据源配置,通过数据库存储数据源连接信息,动态使用不同的数据库进行数据处理。使用自定义注解标识使用默认数据库。

    Application.yml

    <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus</artifactId> <version>3.1.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.1.5</version> </dependency> </dependencies>

    DatasourceConfiguration:

    配置文件:

    /** * @author kuangsha * @date 2020/6/29 9:25 */ @Configuration public class DatasourceConfiguration { /** * 注册bean 拿到动态数据源 * 每次数据库操作都会执行DynamicDatasource 中重写的getConnection()方法 * @return */ @Bean public DataSource dataSource() { DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create(); dataSourceBuilder.type(DynamicDatasource.class); return dataSourceBuilder.build(); } }

    DynamicDatasource类:

    使用数据源之前先getConnection连接数据源,首先获取当前使用的数据源id,DatasourceHolder中通过数据源id获取数据源信息,如果没有数据源就初始化数据源连接

    /** * @author kuangsha * @date 2020/6/29 9:27 */ @Slf4j public class DynamicDatasource extends HikariDataSource { @Override public Connection getConnection() throws SQLException { // 获取当前数据源 id Long id = DatasourceConfigContextHolder.getCurrentDatasourceConfig(); // 根据当前id获取数据源 HikariDataSource dataSource = DatasourceHolder.INSTANCE.getDatasource(id); if (null == dataSource) { // 初始化数据源 dataSource = initDatasource(id); } return dataSource.getConnection(); } /** * 初始化数据源 * * @param id * @return */ private HikariDataSource initDatasource(Long id) { HikariDataSource dataSource = new HikariDataSource(); // 判断是否是默认数据源 if (DatasourceHolder.DEFAULT_ID.equals(id)) { // 默认数据源根据 application.yml 配置的生成 DataSourceProperties properties = SpringUtil.getBean(DataSourceProperties.class); dataSource.setJdbcUrl(properties.getUrl()); dataSource.setUsername(properties.getUsername()); dataSource.setPassword(properties.getPassword()); dataSource.setDriverClassName(properties.getDriverClassName()); } else { DatasourceConfig datasourceConfig = DatasourceConfigCache.INSTANCE.getConfig(id); if (null == datasourceConfig) { throw new RuntimeException("没有此数据源"); } dataSource.setJdbcUrl(datasourceConfig.buildJdbcUrl()); dataSource.setUsername(datasourceConfig.getUsername()); dataSource.setPassword(datasourceConfig.getPassword()); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); } // 将创建的数据源添加数据源管理中 绑定当前线程 DatasourceHolder.INSTANCE.addDatasource(id, dataSource); return dataSource; } }

    DatasourceConfigContextHolder类:

    管理数据源id 当前使用数据源的id记录和修改

    /** * 数据源管理 * * @author kuangsha * @date 2020/6/29 9:38 */ public class DatasourceConfigContextHolder { // 线程之间可见数据源id private static final ThreadLocal<Long> DATASOURCE_HOLDER = ThreadLocal.withInitial(() -> DatasourceHolder.DEFAULT_ID); /** * 获取当前数据源id * * @return */ public static Long getCurrentDatasourceConfig() { return DATASOURCE_HOLDER.get(); } /** * 设置默认数据源 */ public static void setDefaultDatasource() { DATASOURCE_HOLDER.remove(); setCurrentDatasourceConfig(DatasourceHolder.DEFAULT_ID); } /** * 设置当前数据源配置 * * @param id */ public static void setCurrentDatasourceConfig(Long id) { DATASOURCE_HOLDER.set(id); } }

    DatasourceHolder类:

    数据源id和数据源对应管理,可以获取数据源id对应的数据源 DataSource,可以添加和移除已有的数据源对应

    /** * @author kuangsha * @date 2020/6/29 9:39 */ public enum DatasourceHolder { /** * 当前实例 */ INSTANCE; /** * 默认数据源的id */ public static final Long DEFAULT_ID = -1L; /** * 管理动态数据源列表。 */ private static final Map<Long, DatasourceManager> DATASOURCE_CACHE = new ConcurrentHashMap<>(); /** * 根据数据源id获取数据源 * * @param id * @return */ public synchronized HikariDataSource getDatasource(Long id) { if (DATASOURCE_CACHE.containsKey(id)) { DatasourceManager datasourceManager = DATASOURCE_CACHE.get(id); datasourceManager.refreshTime(); return datasourceManager.getDataSource(); } return null; } /** * 数据源添加到数据源管理器中,绑定当前线程 * * @param id * @param dataSource */ public void addDatasource(Long id, HikariDataSource dataSource) { DatasourceManager datasourceManager = new DatasourceManager(dataSource); DATASOURCE_CACHE.put(id, datasourceManager); } }

    DatasourceManager类:

    数据源类 保存datasource和刷新时间,每使用一次就刷新最近使用时间

    /** * 数据源管理类 * * @author kuangsha * @date 2020/6/29 9:44 */ public class DatasourceManager { /** * 上一次使用时间 */ private LocalDateTime lastUseTime; /** * 数据源 */ @Getter private HikariDataSource dataSource; public DatasourceManager(HikariDataSource dataSource) { this.dataSource = dataSource; this.lastUseTime = LocalDateTime.now(); } /** * 刷新上次使用时间 */ public void refreshTime() { this.lastUseTime = LocalDateTime.now(); } }

    DatasourceConfigCache类:

    数据库中配置的数据源读取存放,可以新增数据源配置和删除数据源配置,数据源id做key

    /** * 数据源实体管理 * 数据库中配置的数据源,通过数据源id和数据源配置对应 * @author kuangsha * @date 2020/6/29 9:57 */ public enum DatasourceConfigCache { /** * 当前实例 */ INSTANCE; /** * 管理动态数据源列表。 */ private static final Map<Long, DatasourceConfig> CONFIG_CACHE = new ConcurrentHashMap<>(); /** * 获取数据源配置 * * @param id * @return */ public DatasourceConfig getConfig(Long id) { if (CONFIG_CACHE.containsKey(id)) { return CONFIG_CACHE.get(id); } return null; } /** * 添加数据源列表 * * @param id * @param datasourceConfig */ public void addConfig(Long id, DatasourceConfig datasourceConfig) { CONFIG_CACHE.put(id, datasourceConfig); } }

    SpringUtil类:

    获取bean,这儿使用在获取DataSourceProperties的yml配置文件生成

    /** * @author kuangsha * @date 2020/6/29 9:51 */ @Service @Slf4j @Lazy(false) public class SpringUtil implements ApplicationContextAware, DisposableBean { private static ApplicationContext applicationContext = null; /** * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. * * @param requiredType * @return */ public static <T> T getBean(Class<T> requiredType) { return applicationContext.getBean(requiredType); } /** * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. * * @param beanName * @return */ public static <T> T getBean(String beanName) { return (T) applicationContext.getBean(beanName); } /** * 实现DisposableBean接口, 在Context关闭时清理静态变量. * * @throws Exception */ @Override public void destroy() throws Exception { SpringUtil.clearHolder(); } /** * 清空SpringContextHolder中的ApplicationContext为Null. */ private static void clearHolder() { if (log.isDebugEnabled()) { log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext); } applicationContext = null; } /** * 实现ApplicationContextAware接口, 注入Context到静态变量中. * * @param applicationContext * @throws BeansException */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringUtil.applicationContext = applicationContext; } }

    MybatisConfiguration类:

    指定mapper位置,定义sqlSessionFactory,通过数据源创建SqlSessionFactory

    /** * * @author kuangsha * @date 2020/6/29 10:46 */ @Configuration // sqlSessionFactoryRef 指定sqlSessionFactory对象名 @MapperScan(basePackages = "cn.shuhan.dynamic.datasource.mapper", sqlSessionFactoryRef = "sqlSessionFactory") public class MybatisConfiguration { /** * 创建会话工厂。 * * @param dataSource 数据源 * @return 会话工厂 */ @Bean(name = "sqlSessionFactory") @SneakyThrows // lombok public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean.getObject(); } }

    Mapper

    /** * 定义MyMapper * @author kuangsha * @date 2020/6/29 10:28 */ @RegisterMapper public interface MyMapper<T> extends Mapper<T>, MySqlMapper<T> { } /** * @author kuangsha * @date 2020/6/29 10:15 */ @Mapper public interface DatasourceConfigMapper extends MyMapper<DatasourceConfig> { } /** * @author kuangsha * @date 2020/6/29 10:15 */ @Mapper public interface UserMapper extends MyMapper<User> { }

    DynamicDatasourceApplication启动类:

    /** * @author kuangsha * @date 2020/6/29 9:21 */ @SpringBootApplication @RequiredArgsConstructor(onConstructor_ = @Autowired) public class DynamicDatasourceApplication implements CommandLineRunner { private final DatasourceConfigMapper configMapper; public static void main(String[] args) { SpringApplication.run(DynamicDatasourceApplication.class, args); } /** * 初始化数据源和默认数据源 * @param args * @throws Exception */ @Override public void run(String... args) throws Exception { // 设置默认数据源 DatasourceConfigContextHolder.setDefaultDatasource(); // 获取所有配置 List<DatasourceConfig> datasourceConfigs = configMapper.selectAll(); System.out.println("加载其余数据源配置列表: " + datasourceConfigs); // 将数据库配置加入缓存 datasourceConfigs.forEach(e -> DatasourceConfigCache.INSTANCE.addConfig(e.getId(), e)); } }

    UserController类:

    /** * @author kuangsha * @date 2020/6/29 14:34 */ @RestController @RequiredArgsConstructor(onConstructor_ = @Autowired) public class UserController { private final UserMapper userMapper; @GetMapping("/user") public List<User> list() { return userMapper.selectAll(); } @GetMapping("/user1") @DefaultDatasource // 忽略数据源 直接使用默认数据源 public List<User> list1() { return userMapper.selectAll(); } }

    DefaultDatasource自定义注解类:

    /** * 用户只能使用默认数据源设置 * * @author kuangsha * @date 2020/6/29 14:46 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DefaultDatasource { }

    DefaultDatasourceAspect类:

    自定义注解功能实现 ;

    Before:如果有注解就使用默认数据源。没有注解就拿到请求头中的Datasource-Config-Id属性,如果不为空,就将数据源通过拿到的Datasource-Config-Id 去设置对应数据源,如果为空就使用默认数据源;

    After: 方法之后将数据源切换回默认数据源

    1.@Pointcut(“execution(public * cn.shuhan.dynamic.datasource.controller..(…))”) 注解切面位置

    2.@Before(“datasourcePointcut()”) 方法执行前操作

    3.@After(“datasourcePointcut()”) 方法执行后操作

    /** * @author kuangsha * @date 2020/6/29 14:48 */ @Aspect @Component @RequiredArgsConstructor(onConstructor_ = @Autowired) public class DefaultDatasourceAspect { private final Logger logger = LoggerFactory.getLogger(DefaultDatasourceAspect.class); // @Pointcut("@annotation(cn.shuhan.dynamic.datasource.annotation.DefaultDatasource)") // 切注解 // public void datasourcePointcut() { // } @Pointcut("execution(public * cn.shuhan.dynamic.datasource.controller.*.*(..))") // 切位置 public void datasourcePointcut() { } @Before("datasourcePointcut()") public void doBefore(JoinPoint joinPoint) { logger.info("**************************************1111111111111"); Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); // 去除有注解的方法 有注解只能 用默认数据源 DefaultDatasource annotation = method.getAnnotation(DefaultDatasource.class); if (null != annotation) { // 设置为默认数据源 DatasourceConfigContextHolder.setDefaultDatasource(); } else { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes; HttpServletRequest request = attributes.getRequest(); String header = request.getHeader("Datasource-Config-Id"); if (StringUtil.isNotEmpty(header)) { long id = Long.parseLong(header); DatasourceConfigContextHolder.setCurrentDatasourceConfig(id); } else { DatasourceConfigContextHolder.setDefaultDatasource(); } } } /** * 设置回默认数据源 */ @After("datasourcePointcut()") public void doAfter() { logger.info("******************************22222222222"); DatasourceConfigContextHolder.setDefaultDatasource(); } }

    数据sql

    CREATE TABLE IF NOT EXISTS `datasource_config` ( `id` bigint(13) NOT NULL AUTO_INCREMENT COMMENT '主键', `host` varchar(255) NOT NULL COMMENT '数据库地址', `port` int(6) NOT NULL COMMENT '数据库端口', `username` varchar(100) NOT NULL COMMENT '数据库用户名', `password` varchar(100) NOT NULL COMMENT '数据库密码', `database` varchar(100) DEFAULT 0 COMMENT '数据库名称', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='数据源配置表'; INSERT INTO `datasource_config`(`id`, `host`, `port`, `username`, `password`, `database`) VALUES (1, '127.0.01', 3306, 'root', 'root', 'test'); INSERT INTO `datasource_config`(`id`, `host`, `port`, `username`, `password`, `database`) VALUES (2, '192.168.239.4', 3306, 'dmcp', 'Dmcp321!', 'test'); CREATE TABLE IF NOT EXISTS `test_user` ( `id` bigint(13) NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(255) NOT NULL COMMENT '姓名', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='用户表'; -- 默认数据库插入如下 SQL INSERT INTO `test_user`(`id`, `name`) values (1, '默认数据库用户1'); INSERT INTO `test_user`(`id`, `name`) values (2, '默认数据库用户2'); -- 测试库1插入如下SQL INSERT INTO `test_user`(`id`, `name`) values (1, '测试库1用户1'); INSERT INTO `test_user`(`id`, `name`) values (2, '测试库1用户2'); -- 测试库2插入如下SQL INSERT INTO `test_user`(`id`, `name`) values (1, '测试库2用户1'); INSERT INTO `test_user`(`id`, `name`) values (2, '测试库2用户2');

    结果:

    在调用有@DefaultDatasource 注解方法 使用默认数据库

    调用没有@DefaultDatasource注解方法,获取请求头中Datasource-Config-Id 属性,通过Datasource-Config-Id值去获取动态数据源

    Processed: 0.010, SQL: 9