SpringBoot3整合Mybatis-Plus,自定义动态数据源starter

文章目录

  • 前言
  • 正文
    • 一、项目总览
    • 二、核心代码展示
      • 2.1 自定义AbstractRoutingDataSource
      • 2.2 动态数据源DynamicDataSource
      • 2.3 动态数据源自动配置
      • 2.4 动态数据源上下文DynamicDataSourceContextHolder
      • 2.5 动态数据源修改注解定义
      • 2.6 修改切面DynamicDataSourceAspect
      • 2.7 动态数据源工具类
    • 三、start模块&调试
      • 3.1 Mybatis-Plus配置
      • 3.2 application.yml配置
      • 3.3 启动类
      • 3.4 调试
      • 3.5 数据库sql
    • 四、调试结果
      • 4.1 启动项目
      • 4.2 执行请求

前言

本文旨在SpringBoot3整合Mybatis-Plus,实现动态数据源切换。
不使用Mybatis-Plus本身的依赖。自己动手造轮子。

之前有写过一个SpringBoot切换动态数据源的文章:

  • https://blog.csdn.net/FBB360JAVA/article/details/124610140

本次使用了Java17,SpringBoot3.0.2 ,Mybatis-Spring 3版本。并且自定义starter,提供自定义注解,使用切面实现切换数据源。
本文中对应的代码仓库如下:

  • https://gitee.com/fengsoshuai/dynamic-datasource-mp-starter-demo

其中,代码分支master,是多数据源,提供静态切换方法,注解方式切换。
代码分支dev,是动态多数据源,在master的基础上,提供运行时,新增或修改,或删除数据源。

以上,只考虑单机模式(如果是分布式项目,建议使用中间件,如redis等维护数据源信息;或者创建独立项目专门提供数据源必要信息的接口)

正文

一、项目总览

在这里插入图片描述

本次使用聚合maven项目,内部包含两个模块:

  • dynamic-datasource-mp-starter:自定义starter,实现数据源的基本功能,包含切换数据源等。
  • start:启动&测试模块,整合mybatis-plus ,提供代码配置,以及提供测试接口

二、核心代码展示

注意,本节代码展示,只粘贴了dev分支的代码!

2.1 自定义AbstractRoutingDataSource

如果不需要动态新增或修改数据源,则不需要自定义这个类。直接使用spring-jdbc中的该类即可。

package org.feng.datasource.util;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.jdbc.datasource.lookup.DataSourceLookup;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    @Nullable
    private Map<Object, Object> targetDataSources;
    @Nullable
    private Object defaultTargetDataSource;
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;
    @Nullable
    private DataSource resolvedDefaultDataSource;

    public AbstractRoutingDataSource() {
    }

    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

    public void setTargetDataSourcesAndRefresh(String newMerchant, DataSource newDataSource) {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }

        this.targetDataSources.put(newMerchant, newDataSource);

        if (this.resolvedDataSources == null) {
            this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
        }

        Object value = this.targetDataSources.get(newMerchant);
        Object lookupKey = this.resolveSpecifiedLookupKey(newMerchant);
        DataSource dataSource = this.resolveSpecifiedDataSource(value);
        this.resolvedDataSources.put(lookupKey, dataSource);
    }

    public void removeDataSourcesByMerchant(String newMerchant) {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.targetDataSources.remove(newMerchant);
        this.resolvedDataSources.remove(newMerchant);
    }

    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        this.defaultTargetDataSource = defaultTargetDataSource;
    }

    public void setLenientFallback(boolean lenientFallback) {
        this.lenientFallback = lenientFallback;
    }

    public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) {
        this.dataSourceLookup = dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup();
    }

    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        } else {
            this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
            this.targetDataSources.forEach((key, value) -> {
                Object lookupKey = this.resolveSpecifiedLookupKey(key);
                DataSource dataSource = this.resolveSpecifiedDataSource(value);
                this.resolvedDataSources.put(lookupKey, dataSource);
            });
            if (this.defaultTargetDataSource != null) {
                this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }

        }
    }

    protected Object resolveSpecifiedLookupKey(Object lookupKey) {
        return lookupKey;
    }

    protected DataSource resolveSpecifiedDataSource(Object dataSourceObject) throws IllegalArgumentException {
        if (dataSourceObject instanceof DataSource dataSource) {
            return dataSource;
        } else if (dataSourceObject instanceof String dataSourceName) {
            return this.dataSourceLookup.getDataSource(dataSourceName);
        } else {
            throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSourceObject);
        }
    }

    public Map<Object, DataSource> getResolvedDataSources() {
        Assert.state(this.resolvedDataSources != null, "DataSources not resolved yet - call afterPropertiesSet");
        return Collections.unmodifiableMap(this.resolvedDataSources);
    }

    @Nullable
    public DataSource getResolvedDefaultDataSource() {
        return this.resolvedDefaultDataSource;
    }

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

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

    public <T> T unwrap(Class<T> iface) throws SQLException {
        return iface.isInstance(this) ? (T) this : this.determineTargetDataSource().unwrap(iface);
    }

    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return iface.isInstance(this) || this.determineTargetDataSource().isWrapperFor(iface);
    }

    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.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 + "]");
        } else {
            return dataSource;
        }
    }

    @Nullable
    protected abstract Object determineCurrentLookupKey();
}

2.2 动态数据源DynamicDataSource

package org.feng.datasource;

import org.feng.datasource.util.AbstractRoutingDataSource;

/**
 * 动态数据源
 *
 * @author feng
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }
}

2.3 动态数据源自动配置

package org.feng.datasource.config;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.feng.datasource.DynamicDataSource;
import org.feng.datasource.constant.DataSourceConstant;
import org.feng.datasource.entity.DataSourceProperties;
import org.feng.datasource.entity.SpringDataSourceProperties;
import org.feng.datasource.util.DataSourceUtil;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 动态数据源自动配置
 *
 * @author feng
 */
@Data
@Slf4j
@AutoConfiguration
@ConfigurationPropertiesScan({"org.feng.datasource.entity"})
public class DynamicDataSourceAutoConfiguration {

    @Resource
    private SpringDataSourceProperties springDataSourceProperties;


    @Primary
    @Bean
    public DataSource dynamicDataSource() {
        Map<Object, Object> dataSourceMap = new HashMap<>(16);

        Map<String, DataSourceProperties> dataSourcePropertiesMap = springDataSourceProperties.getConfigMap();
        dataSourcePropertiesMap.forEach((merchant, properties) -> dataSourceMap.put(merchant, DataSourceUtil.dataSource(properties)));

        // 设置动态数据源
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(dataSourceMap);

        // 设置默认数据源
        dynamicDataSource.setDefaultTargetDataSource(dataSourceMap.get(DataSourceConstant.MASTER));
        return dynamicDataSource;
    }


    @PostConstruct
    private void init() {
        Map<String, DataSourceProperties> configMap = springDataSourceProperties.getConfigMap();

        configMap.forEach((k, v) -> {
            log.info("merchantKey = {}, config = {}", k, v);
        });
    }
}

2.4 动态数据源上下文DynamicDataSourceContextHolder

package org.feng.datasource;

import lombok.extern.slf4j.Slf4j;
import org.feng.datasource.constant.DataSourceConstant;

import java.util.Optional;

/**
 * 动态数据源上下文保持类
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年05月05日 15时19分
 */
@Slf4j
public class DynamicDataSourceContextHolder {

    /**
     * 动态数据源的上下文
     */
    private static final ThreadLocal<String> DATASOURCE_CONTEXT_MERCHANT_HOLDER = new InheritableThreadLocal<>();

    /**
     * 切换数据源
     *
     * @param merchant 租户Key
     */
    public static void setDataSourceKey(String merchant) {
        log.info("切换数据源,merchant 为 {}", merchant);
        DATASOURCE_CONTEXT_MERCHANT_HOLDER.set(merchant);
    }

    /**
     * 获取当前数据源名称
     *
     * @return 当前数据源名称
     */
    public static String getDataSourceKey() {
        return Optional.ofNullable(DATASOURCE_CONTEXT_MERCHANT_HOLDER.get())
                .orElse(DataSourceConstant.MASTER);
    }

    /**
     * 删除当前数据源名称
     */
    public static void removeDataSourceKey() {
        DATASOURCE_CONTEXT_MERCHANT_HOLDER.remove();
    }
}

2.5 动态数据源修改注解定义

package org.feng.datasource.annotation;

import org.feng.datasource.constant.DataSourceConstant;

import java.lang.annotation.*;

/**
 * 改变数据源注解
 *
 * @author feng
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ChangeDataSource {
    String merchant() default DataSourceConstant.MASTER;
}

2.6 修改切面DynamicDataSourceAspect

package org.feng.datasource.aop;

import jakarta.annotation.PostConstruct;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.feng.datasource.DynamicDataSourceContextHolder;
import org.feng.datasource.annotation.ChangeDataSource;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 动态数据源切面
 *
 * @author feng
 */
@Aspect
@Component
@Order(1)
@Slf4j
public class DynamicDataSourceAspect {

    @SneakyThrows
    @Before("@annotation(org.feng.datasource.annotation.ChangeDataSource)")
    public void changeDataSource(JoinPoint joinPoint) {
        log.info("开始切换数据源...");

        // 获取方法名,参数
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        Class<?>[] paramsTypes = new Class[args.length];
        if(args.length > 0) {
            for (int i = 0; i < args.length; i++) {
                paramsTypes[i] = args[i].getClass();
            }
        }

        // 获取注解
        Class<?> aClass = joinPoint.getTarget().getClass();
        Method currentMethod = aClass.getDeclaredMethod(methodName, paramsTypes);
        ChangeDataSource changeDataSource = currentMethod.getDeclaredAnnotation(ChangeDataSource.class);
        // 获取租户
        String merchant = changeDataSource.merchant();
        log.info("当前数据源租户切换为:{}", merchant);

        // 切换数据源
        DynamicDataSourceContextHolder.setDataSourceKey(merchant);
    }


    @After("@annotation(org.feng.datasource.annotation.ChangeDataSource)")
    public void changeDataSourceOver() {
        log.info("释放数据源...");
        DynamicDataSourceContextHolder.removeDataSourceKey();
    }

    @PostConstruct
    private void init() {
        log.info("注册动态数据源切换注解");
    }
}

2.7 动态数据源工具类

提供动态新增数据源,删除数据源的方法。
因为本身是维护map,所以同时支持修改(根据merchant来新增或修改或删除)

package org.feng.datasource.util;

import lombok.extern.slf4j.Slf4j;
import org.feng.datasource.DynamicDataSource;
import org.feng.datasource.entity.DataSourceProperties;
import org.springframework.beans.BeansException;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;

/**
 * 数据源工具
 *
 * @author feng
 */
@Slf4j
@Component
public class DataSourceUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;


    /**
     * 设置数据源,用于动态新增,修改数据源
     *
     * @param newMerchant          租户
     * @param dataSourceProperties 数据源属性对象
     */
    public static void setNewDynamicDataSource(String newMerchant, DataSourceProperties dataSourceProperties) {
        log.info("merchantKey = {}, config = {}", newMerchant, dataSourceProperties);

        DynamicDataSource dataSource = applicationContext.getBean(DynamicDataSource.class);

        dataSource.setTargetDataSourcesAndRefresh(newMerchant, dataSource(dataSourceProperties));
    }

    /**
     * 移除数据源
     *
     * @param newMerchant 租户
     */
    public static void removeDynamicDataSource(String newMerchant) {
        log.info("正在移除数据源 merchantKey = {}", newMerchant);

        DynamicDataSource dataSource = applicationContext.getBean(DynamicDataSource.class);

        dataSource.removeDataSourcesByMerchant(newMerchant);
    }


    @Override
    public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
        DataSourceUtil.applicationContext = applicationContext;
    }


    /**
     * 构建数据源
     *
     * @param dataSourceProperties 数据源属性配置
     * @return 数据源
     */
    public static DataSource dataSource(DataSourceProperties dataSourceProperties) {
        return DataSourceBuilder.create()
                .driverClassName(dataSourceProperties.getDriverClassName())
                .url(dataSourceProperties.getJdbcUrl())
                .username(dataSourceProperties.getUsername())
                .password(dataSourceProperties.getPassword())
                .build();
    }
}

三、start模块&调试

3.1 Mybatis-Plus配置

package org.feng.start.config;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
import lombok.SneakyThrows;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

import javax.sql.DataSource;

/**
 * mybatis-plus配置
 *
 * @author feng
 */
@Configuration
public class MybatisPlusConfig {

    @Bean
    @Primary
    @SneakyThrows
    public SqlSessionFactory sqlSessionFactory(@Autowired DataSource dataSource) {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        MybatisConfiguration configuration = new MybatisConfiguration();
        sqlSessionFactoryBean.setConfiguration(configuration);
        // 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
        configuration.setMapUnderscoreToCamelCase(true);
        // 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段
        configuration.setCallSettersOnNulls(true);
        // 日志
        configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class);
        // 实体包
        sqlSessionFactoryBean.setTypeAliasesPackage("org.feng.start.entity");
        // mapper.xml位置
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resourceResolver.getResources("classpath*:mapper/**Mapper.xml");
        sqlSessionFactoryBean.setMapperLocations(resources);

        // 设置数据源
        sqlSessionFactoryBean.setDataSource(dataSource);

        // 设置GlobalConfig
        GlobalConfigUtils.setGlobalConfig(configuration, globalConfig());

        // 返回SqlSessionFactory
        return sqlSessionFactoryBean.getObject();
    }

    private GlobalConfig globalConfig() {
        GlobalConfig globalConfig = new GlobalConfig();
        GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
        dbConfig.setIdType(IdType.AUTO);
        globalConfig.setDbConfig(dbConfig);

        return globalConfig;
    }
}

3.2 application.yml配置

spring:
  datasource:
    config-map:
      master:
        driver-class-name: "com.mysql.cj.jdbc.Driver"
        jdbc-url: "jdbc:mysql://192.168.110.68:3306/db_master?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"
        username: "root"
        password: "root123456"
      slave1:
        driver-class-name: "com.mysql.cj.jdbc.Driver"
        jdbc-url: "jdbc:mysql://192.168.110.68:3306/db_slave1?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"
        username: "root"
        password: "root123456"
#      slave2:
#        driver-class-name: "com.mysql.cj.jdbc.Driver"
#        jdbc-url: "jdbc:mysql://192.168.110.68:3306/db_slave2?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"
#        username: "root"
#        password: "root123456"


server:
  port: 80
  servlet:
    context-path: /dynamic_datasource

3.3 启动类

package org.feng.start;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@MapperScan(basePackages = {"org.feng.start.mapper"})
@EnableAspectJAutoProxy(exposeProxy = true)
@EnableTransactionManagement
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class, scanBasePackages = {"org.feng.datasource", "org.feng.start"})
public class StartApplication {

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

}

3.4 调试

定义控制器,提供接口。

package org.feng.start.controller;

import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.feng.datasource.DynamicDataSourceContextHolder;
import org.feng.datasource.annotation.ChangeDataSource;
import org.feng.datasource.entity.DataSourceProperties;
import org.feng.datasource.util.DataSourceUtil;
import org.feng.start.entity.Student;
import org.feng.start.service.IStudentService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * 学生控制器-测试切换数据源
 *
 * @author feng
 */
@Slf4j
@RequestMapping("/student")
@RestController
public class StudentController {

    @Resource
    private IStudentService studentService;

    @GetMapping("/list/{merchant}")
    public List<Student> list(@PathVariable String merchant) {
        try {
            // 切换数据源
            DynamicDataSourceContextHolder.setDataSourceKey(merchant);

            // 查库
            List<Student> resultList = studentService.list();

            log.info("查询结果:{}", resultList);

            return resultList;
        } finally {
            // 清除当前数据源
            DynamicDataSourceContextHolder.removeDataSourceKey();
        }
    }


    @GetMapping("/listStu/master")
    public List<Student> listStu() {
        // 查库
        List<Student> resultList = studentService.list();

        log.info("查询结果:{}", resultList);

        return resultList;
    }

    @ChangeDataSource(merchant = "slave1")
    @GetMapping("/listStu/slave1")
    public List<Student> listStu1() {
        // 查库
        List<Student> resultList = studentService.list();

        log.info("查询结果:{}", resultList);

        return resultList;
    }

    @ChangeDataSource(merchant = "slave2")
    @GetMapping("/listStu/slave2")
    public List<Student> listStu2() {
        // 查库
        List<Student> resultList = studentService.list();

        log.info("查询结果:{}", resultList);

        return resultList;
    }

    @GetMapping("/listStu/newDataSource")
    public List<Student> newDataSource() {
        String merchant = "slave2";
        DataSourceProperties dataSourceProperties = newSlave2();
        DataSourceUtil.setNewDynamicDataSource(merchant, dataSourceProperties);

        // 切换数据源
        DynamicDataSourceContextHolder.setDataSourceKey(merchant);
        // 查库
        List<Student> resultList = studentService.list();

        log.info("查询结果:{}", resultList);

        DynamicDataSourceContextHolder.removeDataSourceKey();
        return resultList;
    }

    private DataSourceProperties newSlave2() {
        DataSourceProperties dataSourceProperties = new DataSourceProperties();
        dataSourceProperties.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSourceProperties.setJdbcUrl("jdbc:mysql://192.168.110.68:3306/db_slave2?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8");
        dataSourceProperties.setUsername("root");
        dataSourceProperties.setPassword("root123456");
        return dataSourceProperties;
    }
}

3.5 数据库sql

按照自己的需要,创建多个数据库,并创建好自己使用的数据表即可。
本文中测试使用的如下:

CREATE TABLE `tb_student` (
  `student_name` varchar(100) DEFAULT NULL,
  `id` bigint unsigned NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

四、调试结果

4.1 启动项目

在这里插入图片描述
启动项目,可以看到注册了两个数据源。

4.2 执行请求

  • Get 请求 :http://localhost:80/dynamic_datasource/student/listStu/master
    响应:
[
  {
    "id": 1,
    "studentName": "master_student2312s"
  }
]
  • Get 请求 :http://localhost:80/dynamic_datasource/student/listStu/slave1
    响应:
[
  {
    "id": 1,
    "studentName": "slave1_studew12321"
  }
]
  • Get 请求 :http://localhost:80/dynamic_datasource/student/listStu/slave2
    因为此时还没有slave2,所以默认请求的是master的数据源,查询结果如下:
[
  {
    "id": 1,
    "studentName": "master_student2312s"
  }
]
  • Get 请求 :http://localhost:80/dynamic_datasource/student/listStu/newDataSource
    新增数据源,代码中写的是新增slave2的数据源
    日志如下:
    在这里插入图片描述

查询结果如下:

[
  {
    "id": 1,
    "studentName": "slave2_213dqwa"
  }
]
  • Get 请求 :http://localhost:80/dynamic_datasource/student/listStu/slave2
    此时已经新增了slave2数据源,因此能够切换到slave2中,查询结果如下:
[
  {
    "id": 1,
    "studentName": "slave2_213dqwa"
  }
]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/381791.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

嵌入式系统的前景:未来智能汽车

&#xff08;本文为简单介绍&#xff0c;个人的观点仅供参考&#xff09; 智能汽车时代已经来临!未来十年,我们的汽车将变得越来越智能化。各大汽车公司在研发自动驾驶技术,目标是实现真正的无人驾驶。要实现这一目标,嵌入式系统将发挥关键作用。 简单来说,嵌入式系统就是在汽…

【Linux】指令提权-sudo

Hello everybody&#xff0c;新年快乐&#xff01;哈哈&#xff01;今天打算给大家讲讲指令提权的相关知识&#xff0c;虽然内容不多&#xff0c;但有时却很有用。在我们学习过权限&#xff0c;vim后就可以学习指令提权啦&#xff0c;没看过的宝子们建议先去看一看我之前的文章…

旅游|基于Springboot的旅游管理系统设计与实现(源码+数据库+文档)

旅游管理系统目录 目录 基于Springboot的旅游管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、用户管理 2、景点分类管理 3、景点信息管理 4、酒店信息管理 5、景点信息 6、游记分享管理 四、数据库设计 1、实体ER图 2、具体的表设计如下所示&#xf…

工业级加固平板丨亿道三防平板电脑丨安卓工业平板丨改善车队管理

在现代物流和运输行业中&#xff0c;车队管理是一个复杂而重要的任务。为了更好地管理车队&#xff0c;提高工作效率和减少成本&#xff0c;许多企业正在采用新技术和工具。其中&#xff0c;三防平板电脑作为一种功能强大且适应恶劣环境的设备&#xff0c;已经在车队管理中得到…

【九章斩题录】Leetcode:判定是否互为字符重排(C/C++)

面试题 01.02. 判定是否互为字符重排 ✅ 模板&#xff1a;C class Solution { public:bool CheckPermutation(string s1, string s2) {} }; 「 法一 」排序 &#x1f4a1; 思路&#xff1a;看到题目中说 "重新排列后能否变成另一个字符串"&#xff0c;等等……重新…

第三百一十七回

文章目录 1. 概念介绍2. 实现方法2.1 hintText2.2 labelText2.3 controller 3. 示例代码4. 内容总结 我们在上一章回中介绍了"如何在输入框中处理光标"相关的内容&#xff0c;本章回中将介绍如何添加输入框默认值.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1.…

[VulnHub靶机渗透] Misdirection: 1

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【python】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏…

Linux操作系统基础(八):Linux的vi/vim编辑器

文章目录 Linux的vi/vim编辑器 一、vi/vim编辑器介绍 二、打开文件 三、VIM编辑器的三种模式(重点) 四、命令模式相关命令 五、底行模式相关命令 Linux的vi/vim编辑器 一、vi/vim编辑器介绍 vi是visual interface的简称, 是Linux中最经典的文本编辑器 vi的核心设计思想…

专业130+总分410+苏州大学837信号系统与数字逻辑考研经验电子信息与通信,真题,大纲,参考书

今年考研总分410&#xff0c;专业837信号系统与数字逻辑130&#xff0c;整体每门相对比较均衡&#xff0c;没有明显的短板&#xff0c;顺利上岸苏大&#xff0c;总结一下自己这大半年的复习经历&#xff0c;希望可以对大家有所帮助&#xff0c;也算是对自己考研做个总结。 专业…

6 scala-面向对象编程基础

Scala 跟 Java 一样&#xff0c;是一门面向对象编程的语言&#xff0c;有类和对象的概念。 1 类与对象 与 Java 一样&#xff0c;Scala 也是通过关键字 class 来定义类&#xff0c;使用关键字 new 创建对象。 要运行我们编写的代码&#xff0c;同样像 Java 一样&#xff0c;…

烟火可禁却难禁,灵境难及终将及

现实痛点 2024年1月30日&#xff0c;贵阳市发生了一件令人痛心的事&#xff0c;有人在小区内放烟花导致失火&#xff0c;一男子具备足够的消防安全知识&#xff0c;知道如何使用消防栓却因设施不合格接不上消防栓&#xff0c;接上了又没水&#xff0c;消防员来也束手无策&…

#Z2322. 买保险

一.题目 二.思路 1.暴力 训练的时候&#xff0c;初看这道题&#xff0c;这不就打个暴力吗&#xff1f; 2.暴力代码 #include<bits/stdc.h> #define int long long using namespace std; int n,m,fa,x,y,vis[1000001],ans; vector<int> vec[1000001]; void dfs(i…

VitePress-14- 配置-titleTemplate 的作用详解

作用描述 1、titleTemplate 是标题的后缀&#xff1b;2、可以自定义标题的后缀&#xff1b;3、可以自定义整个的标题以及后缀&#xff0c;语法如下&#xff1a; titleTemplate: :title 链接符号 自己定义的后缀 【:title】&#xff1a;从页面的第一个 <h1> 标题推断出的…

Qt视频播放器项目

一.创建项目 二.设计UI 按钮与名称的对应 打开视频按钮 -> pushButton_Open 播放按钮 -> pushButton_Play 暂停按钮 -> pushButton_Pause 停止按钮 -> pushButton_Stop 音量按钮 -> pushButton_Sound设置图标 在项目目录下创建images文件夹&#xff0c;把图标…

2024 [arXiv] ST-LLM——时空大语言模型用于交通预测

这应该是第一个将LLM用于交通预测&#xff08;时空图预测&#xff09;&#xff0c;这篇由南洋理工大学龙程&#xff08;Cheng Long&#xff09;老师团队与商汤&#xff0c;北大和德国科隆大学&#xff08;Cologne&#xff09;合作完成。且抢先使用了最通用的名字时空大模型名字…

CodeWave学习笔记--博物馆预约管理系统

场馆信息管理页面搭建&#xff08;PC&#xff09; 首先是场馆实体的创建 页面的搭建 在总览界面下创建子界面venueManage界面 现在总览页中实现跳转场馆管理子界面 设计场馆管理界面 效果 访客预约申请页面搭建&#xff08;H5&#xff09; 添加H5端&#xff0c;点击确认即可 …

Bean 的作用域

Bean 的作用域种类 在 Spring 中⽀持 6 种作⽤域&#xff0c;后 4 种在 Spring MVC 环境才⽣效 1. singleton&#xff1a;单例作⽤域 2. prototype&#xff1a;原型作⽤域&#xff08;多例作⽤域&#xff09; 3. request&#xff1a;请求作⽤域 4. session&#xff1a;会话作⽤…

JDK新特性

JDK新特性 函数式接口和Lambda 表达式Stream流操作新日期API操作其他新特性 Lambda 是一个匿名函数&#xff0c;我们可以把 Lambda表达式理解为是一段可以传递的代码&#xff08;将代码 像数据一样进行传递&#xff09;。可以写出更简洁、更 灵活的代码。作为一种更紧凑的代码…

操作系统-【预备学习-1】(Linux 文件目录)

文章目录 相关知识目录结构进入目录补充查看目录创建文件删除文件创建文件夹删除文件夹文件和文件夹拷贝文件和文件夹移动/重命名 任务要求 相关知识 目录结构 Linux 文件系统是树形层次结构&#xff0c;具体如下图所示&#xff0c;最重要的是根目录&#xff08;/&#xff09…

8868体育助力西甲皇家马德里俱乐部 帮助球队把握榜首大战

西甲的皇家马德里足球俱乐部是8868体育合作的俱乐部之一&#xff0c;皇家马德里目前取得18胜3平1负的成绩&#xff0c;排名西甲联赛第一的位置&#xff0c;本赛季主场也有着9胜1平的不败成绩&#xff0c;处于第二的位置&#xff0c;主场的发挥相当突出。目前皇家马德里已经是重…
最新文章