1. 数据准备
 
create database if not exists `ds1`;
create database if not exists `ds3`;
 
 
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
BEGIN;
INSERT INTO `tb_user` (`id`, `username`) VALUES (1, 'wms');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
 
 
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
BEGIN;
INSERT INTO `tb_user` (`id`, `username`) VALUES (1, 'zhangsan');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
 
2. 版本
 
- SprintBoot:2.7.11
- Mybatis-Plus:3.5.5
- MySQL:8.0.30
3. 引入依赖
 
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring-boot.version>2.7.11</spring-boot.version>
    <lombok.version>1.18.30</lombok.version>
    <mysql.version>8.0.33</mysql.version>
    <mybatis-plus.version>3.5.5</mybatis-plus.version>
</properties>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>
    </dependencies>
</dependencyManagement>
 
4. 编码
 
 
@MapperScan(
        basePackages = "com.wxf.metadata.mapper"
)
@SpringBootApplication(
        scanBasePackages = "com.wxf.metadata"
)
public class MetadataApplication {
    public static void main(String[] args) {
        SpringApplication.run(MetadataApplication.class, args);
    }
}
 
 
spring:
  profiles:
    active: dev
 
 
server:
  port: 8099
  shutdown: graceful
spring:
  application:
    name: metadata-service
  datasource:
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/ds1?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&useIPv6=false
    username: root
    password: root
    driverClassName: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 500
      max-lifetime: 18000000
      minimum-idle: 30
      connection-timeout: 30000
      connection-test-query: SELECT 1
      pool-name: HiKariDataSource
      type: com.zaxxer.hikari.HikariDataSource
      idle-timeout: 180000
      auto-commit: true
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
mybatis-plus:
  mapper-locations: classpath:/mapper/*.xml
  type-aliases-package: com.dcits.metadata.entity
  check-config-location: false 
  global-config:
    db-config:
      id-type: assign_id 
      logic-delete-field: deleted 
      logic-delete-value: 1  
      logic-not-delete-value: 0 
  configuration:
    map-underscore-to-camel-case: true 
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 
logging:
  config: classpath:log4j2-spring.xml
  charset:
    file: UTF-8
 
 
<?xml version="1.0" encoding="UTF-8"?>
<configuration monitorInterval="5">
    
    <Properties>
        
        
        <property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
        
        <property name="FILE_PATH" value="${env:LOG_DIR:-logs/metadata-service}"/>
        <property name="FILE_NAME" value="metadata-service"/>
    </Properties>
    <appenders>
        <console name="Console" target="SYSTEM_OUT">
            
            <PatternLayout pattern="${LOG_PATTERN}"/>
            
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
        </console>
        
        <RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/${FILE_NAME}-info.log"
                     filePattern="${FILE_PATH}/backup/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz">
            
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                
                <TimeBasedTriggeringPolicy interval="1"/>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
            
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>
        
        <RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/${FILE_NAME}-warn.log"
                     filePattern="${FILE_PATH}/backup/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz">
            
            <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                
                <TimeBasedTriggeringPolicy interval="1"/>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
            
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>
        
        <RollingFile name="RollingFileDebug" fileName="${FILE_PATH}/${FILE_NAME}-debug.log"
                     filePattern="${FILE_PATH}/backup/${FILE_NAME}-DEBUG-%d{yyyy-MM-dd}_%i.log.gz">
            
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                
                <TimeBasedTriggeringPolicy interval="1"/>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
            
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>
        
        <RollingFile name="RollingFileError" fileName="${FILE_PATH}/${FILE_NAME}-error.log"
                     filePattern="${FILE_PATH}/backup/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz">
            
            <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                
                <TimeBasedTriggeringPolicy interval="1"/>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
            
            <DefaultRolloverStrategy max="15"/>
        </RollingFile>
    </appenders>
    
    
    <loggers>
        <root level="DEBUG">
            <appender-ref ref="Console"/>
            <appender-ref ref="RollingFileInfo"/>
            <appender-ref ref="RollingFileWarn"/>
            <appender-ref ref="RollingFileError"/>
            <appender-ref ref="RollingFileDebug"/>
        </root>
    </loggers>
</configuration>
 
 
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusPropertiesCustomizer;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        
        
        
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        
        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return mybatisPlusInterceptor;
    }
    public MybatisPlusPropertiesCustomizer mybatisPlusPropertiesCustomizer() {
        return properties -> {
            GlobalConfig globalConfig = properties.getGlobalConfig();
            globalConfig.setBanner(false);
            MybatisConfiguration configuration = new MybatisConfiguration();
            
            configuration.setDefaultEnumTypeHandler(MybatisEnumTypeHandler.class);
            properties.setGlobalConfig(globalConfig);
        };
    }
}
 
 
public class DynamicDatasource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DatasourceContextHolder.getDataSource();
    }
}
 
 
public class DatasourceMapCache {
    private static final Map<Object, Object> DATA_SOURCE_MAP = new ConcurrentHashMap<>(16);
    public static Map<Object, Object> getDataSourceMap() {
        return DATA_SOURCE_MAP;
    }
    public static void refreshDataSource(String datasourceKey, DataSource dataSource) {
        
        DATA_SOURCE_MAP.put(datasourceKey, dataSource);
        
        DynamicDatasource dynamicDatasource = SpringApplicationContext.getBean(DynamicDatasource.class);
        
        dynamicDatasource.setTargetDataSources(DATA_SOURCE_MAP);
        
        dynamicDatasource.afterPropertiesSet();
    }
    public static void removeDataSource(String datasourceKey) {
        DATA_SOURCE_MAP.remove(datasourceKey);
    }
}
 
 
public class DatasourceContextHolder {
    private static final ThreadLocal<String> DATA_SOURCE_THREAD_LOCAL = ThreadLocal.withInitial(() -> "defaultDatasource");
    public static String getDataSource() {
        return DATA_SOURCE_THREAD_LOCAL.get();
    }
    public static void setDataSource(String datasourceKey) {
        DATA_SOURCE_THREAD_LOCAL.set(datasourceKey);
    }
    public static void remove() {
        DATA_SOURCE_THREAD_LOCAL.remove();
    }
}
 
 
import javax.sql.DataSource;
import java.util.Map;
@Configuration
public class DatasourceConfig {
    @Bean
    public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDatasource") DynamicDatasource dynamicDatasource) {
        return new JdbcTemplate(dynamicDatasource);
    }
    
    @Bean(name = "defaultDatasource")
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean("dynamicDatasource")
    public DynamicDatasource dynamicDatasource(@Qualifier("defaultDatasource") DataSource dataSource) {
        DynamicDatasource dynamicDatasource = new DynamicDatasource();
        
        dynamicDatasource.setDefaultTargetDataSource(dataSource);
        Map<Object, Object> dataSourceMap = DatasourceMapCache.getDataSourceMap();
        
        dataSourceMap.put("defaultDatasource", dataSource);
        
        dynamicDatasource.setTargetDataSources(dataSourceMap);
        return dynamicDatasource;
    }
    @Bean
    public MybatisSqlSessionFactoryBean sqlSessionFactory(@Qualifier("dynamicDatasource") DynamicDatasource dynamicDatasource) throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dynamicDatasource);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:mapper/*.xml"));
        return sqlSessionFactoryBean;
    }
    @Bean
    public PlatformTransactionManager platformTransactionManager(@Qualifier("dynamicDatasource") DynamicDatasource dynamicDatasource) {
        return new DataSourceTransactionManager(dynamicDatasource);
    }
}
 
 
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Ds {
    String value() default "datasource";
}
 
 
import com.dcits.metadata.config.datasource.DatasourceContextHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.util.Objects;
@Aspect
@Component
public class DynamicDatasourceAspect {
    
    @Pointcut("@annotation(com.dcits.metadata.config.datasource.aspect.Ds)")
    public void dynamicDatasource() {
    }
    
    @Around("dynamicDatasource()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            
            Class<?> clazz = joinPoint.getTarget().getClass();
            Ds clasDs = clazz.getAnnotation(Ds.class);
            
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Ds methodDs = methodSignature.getMethod().getAnnotation(Ds.class);
            if (Objects.nonNull(methodDs)) {
                
                DatasourceContextHolder.setDataSource(methodDs.value());
            } else {
                DatasourceContextHolder.setDataSource(clasDs.value());
            }
            return joinPoint.proceed();
        } finally {
            DatasourceContextHolder.remove();
        }
    }
}
 
 
@Component
public class SpringApplicationContext implements ApplicationContextAware {
    private static ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringApplicationContext.applicationContext = applicationContext;
    }
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }
    public static Object getBean(String name) {
        return applicationContext.getBean(name);
    }
    public static <T> T getBean(String name, Class<T> requiredType) {
        return applicationContext.getBean(name, requiredType);
    }
    
    public static void publishEvent(Object event) {
        applicationContext.publishEvent(event);
    }
}
 
 
import com.dcits.metadata.config.datasource.DatasourceMapCache;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
public class HikariConfigUtils {
    
    public static DataSource initHikariDatasource(String url, String driver, String username, String password) {
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setJdbcUrl(url);
        hikariConfig.setDriverClassName(driver);
        hikariConfig.setUsername(username);
        hikariConfig.setPassword(password);
        return new HikariDataSource(hikariConfig);
    }
    
    public static void refreshDataSource(String datasourceKey, DataSource dataSource) {
        DatasourceMapCache.refreshDataSource(datasourceKey, dataSource);
    }
}
 
5. 测试
 
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("tb_user")
public class User implements Serializable {
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    private String username;
}
 
 
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.dcits.metadata.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
 * @author Wxf
 * @since 2024-03-05 11:40:43
 **/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
 
 
import com.baomidou.mybatisplus.extension.service.IService;
import com.dcits.metadata.entity.User;
import java.util.List;
public interface UserService extends IService<User> {
    
    List<User> selectUserList();
    
    List<User> getDynamicUserList();
}
 
 
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dcits.metadata.config.datasource.DatasourceContextHolder;
import com.dcits.metadata.entity.User;
import com.dcits.metadata.mapper.UserMapper;
import com.dcits.metadata.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Override
    public List<User> selectUserList() {
        return this.baseMapper.selectList(null);
    }
    @Override
    public List<User> getDynamicUserList() {
        DatasourceContextHolder.setDataSource("ds3");
        List<User> userList = this.baseMapper.selectList(null);
        DatasourceContextHolder.remove();
        return userList;
    }
}
 
 
import com.dcits.metadata.service.UserService;
import com.dcits.metadata.utils.HikariConfigUtils;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
 * 测试动态数据源
 *
 * @author Wxf
 * @since 2024-03-05 11:46:06
 **/
@SpringBootTest
public class UserTest {
    @Resource
    private UserService userService;
    @Test
    void selectUserList() {
        System.out.println(this.userService.selectUserList());
    }
    @Test
    void getDynamicUserList() {
        DataSource dataSource = HikariConfigUtils.initHikariDatasource(
                "jdbc:mysql://127.0.0.1:3306/ds3?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&useIPv6=false",
                "com.mysql.cj.jdbc.Driver",
                "root",
                "root"
        );
        HikariConfigUtils.refreshDataSource("ds3", dataSource);
        System.out.println(this.userService.getDynamicUserList());
    }
}