springboot + Mybatis 动态热加载数据源(SaaS多租户平台)

第一部分(基础)

一个简单的租户表结构

tenant

项目依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.hydrogen</groupId>
    <artifactId>test_saas</artifactId>
    <version>1.0.0</version>
    <name>test_saas</name>
    <description>Demo project for test_saas</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- spring aop -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>


        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
            <optional>true</optional>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置文件

数据库连接的key为jdbc-urljdbcUrl
DataSourceAutoConfiguration 为关闭springboot自动数据源配置

server:
  port: 8088

spring:
  application:
    name: test_saas
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    jdbc-url: jdbc:mysql://localhost:3306/education?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&verifyServerCertificate=false&useSSL=false
    username: root
    password: root

mybatis:
  mapper-locations: classpath:mappers/*.xml
  type-aliases-package: com.hydrogen.saas.entity
  configuration:
    map-underscore-to-camel-case: true

pagehelper:
  helper-dialect: mysql
  page-size-zero: true
  reasonable: true
  auto-runtime-dialect: true

定义一个简单的租户类

/**
 * 租户类
 */
@Data
public class Tenant implements Serializable {
    private Integer id;
    /**
     * 租户名
     */
    private String tenantName;
    /**
     * 数据库类型
     */
    private String type;
    /**
     * 驱动类
     */
    private String driverClassName;
    /**
     * 数据库连接
     */
    private String jdbcUrl;
    /**
     * 数据库用户名
     */
    private String username;
    /**
     * 数据库密码
     */
    private String password;
    /**
     * 数据库名
     */
    private String dataBase;
}

Mapper实现根据租户名获取租户连接信息

public interface TenantMapper {
    Tenant getByName(String name);

    void insert(Tenant tenant);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hydrogen.saas.mapper.TenantMapper">
    <insert id="insert">
        insert into tenant values (#{id}, #{tenantName}, #{type}, #{driverClassName}, #{jdbcUrl}, #{username}, #{password}, #{dataBase})
    </insert>

    <select id="getByName" resultType="com.hydrogen.saas.entity.Tenant">
        select * from tenant where tenant_name = #{name}
    </select>
</mapper>

第二部分(配置)

关闭springboot的自动配置数据源

有2种方式:

1.配置文件中关闭

spring:
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

2.启动类注解关闭

@MapperScan("com.hydrogen.saas.mapper")
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class TestSaasApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestSaasApplication.class, args);
    }
}

自定义动态数据源

/**
 * 自定义动态数据源
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    private Map<Object, Object> dataSources = new HashMap<>();

    /**
     * 获取当前数据源的键
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceKey();
    }

    /**
     * 获取当前数据源
     */
    @Override
    protected DataSource determineTargetDataSource() {
        return super.determineTargetDataSource();
    }

    /**
     * 设置默认数据源
     *
     * @param defaultDataSource
     */
    public void setDefaultDataSource(Object defaultDataSource) {
        super.setDefaultTargetDataSource(defaultDataSource);
    }


    /**
     * 设置数据源
     *
     * @param dataSources
     */
    public void setDataSources(Map<Object, Object> dataSources) {
        this.dataSources = dataSources;
        super.setTargetDataSources(dataSources);
        // 保存数据源的key
        DataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
    }

    /**
     * 追加数据源
     *
     * @param key
     * @param dataSource
     */
    public void addDataSource(String key, DataSource dataSource) {
        dataSources.put(key, dataSource);
        super.setTargetDataSources(dataSources);
        // 保存数据源的key
        DataSourceContextHolder.addDataSourceKey(key);
        // 加载新的数据源
        super.afterPropertiesSet();
    }
}

动态数据源的配置

/**
 * 自定义动态数据源配置类
 */
@Configuration
@EnableTransactionManagement
public class DataSourceConfig {

    @Value("${mybatis.type-aliases-package}")
    private String typeAliasesPackage;

    @Value("${mybatis.mapper-locations}")
    private String mapperLocations;

    @Value("${mybatis.configuration.map-underscore-to-camel-case}")
    private Boolean mapUnderscoreToCamelCase;


    /**
     * 默认基础数据源
     *
     * @return
     */
    @Bean("defaultSource")
    @ConfigurationProperties("spring.datasource")
    public DataSource defaultSource() {
        return DataSourceBuilder.create().build();
    }


    /**
     * 自定义动态数据源
     *
     * @return
     */
    @Bean("dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("default", defaultSource());
        // 默认数据源
        dynamicDataSource.setDefaultDataSource(defaultSource());
        // 动态数据源
        dynamicDataSource.setDataSources(dataSourceMap);
        return dynamicDataSource;
    }

    /**
     * @return 修改Mybatis数据源配置
     * @throws IOException
     */
    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean() throws IOException {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        // 配置自定义动态数据源
        sessionFactory.setDataSource(dynamicDataSource());
        // 开启驼峰转下划线设置
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(mapUnderscoreToCamelCase);
        sessionFactory.setConfiguration(configuration);
        // 实体、Mapper类映射
        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        return sessionFactory;
    }


    /**
     * 开启动态数据源@Transactional注解事务管理的支持
     *
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }
}

动态数据源的管理

/**
 * 动态数据源管理
 */
public class DataSourceContextHolder {

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
        /**
         * 设置默认数据源的key
         */
        @Override
        protected String initialValue() {
            return "default";
        }
    };

    /**
     * 数据源key的集合
     */
    public static Set<Object> dataSourceKeys = new HashSet<>();

    /**
     * 切换数据源
     *
     * @param key
     */
    public static void setDataSourceKey(String key) {
        contextHolder.set(key);
    }

    /**
     * 获取数据源
     *
     * @return
     */
    public static String getDataSourceKey() {
        return contextHolder.get();
    }

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

    /**
     * 判断数据源是否存在
     *
     * @param key 数据源key
     * @return
     */
    public static boolean containDataSourceKey(String key) {
        return dataSourceKeys.contains(key);
    }

    /**
     * 添加数据源key
     *
     * @return
     */
    public static boolean addDataSourceKey(Object key) {
        return dataSourceKeys.add(key);
    }

    /**
     * 添加多个数据源keys
     *
     * @param keys
     * @return
     */
    public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
        return dataSourceKeys.addAll(keys);
    }
}

第三部分(实现)

使用AOP实现数据源动态切换

@Aspect
@Order(1)  //该切面应当先于 @Transactional 执行
@Component
public class DynamicDataSourceAspect {

    @Autowired
    private TenantService tenantService;
    @Autowired
    private DynamicDataSource dynamicDataSource;

    /**
     * 切换数据源
     */
    @Before("execution(public * com.hydrogen.saas.controller.*.*(..))")
    public void switchDataSource() {
        System.out.println("切换数据源");
        // 获取token
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("token");
        // 将新租户的数据源添加到动态数据源
        if (!DataSourceContextHolder.containDataSourceKey(token)) {
            Tenant tenant = tenantService.getByName(token);
            System.out.println(tenant);
            if (tenant == null) throw new RuntimeException("租户信息异常");
            DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
            dataSourceBuilder.driverClassName(tenant.getDriverClassName());
            dataSourceBuilder.url(tenant.getJdbcUrl());
            dataSourceBuilder.username(tenant.getUsername());
            dataSourceBuilder.password(tenant.getPassword());
            DataSource source = dataSourceBuilder.build();
            dynamicDataSource.addDataSource(token, source);
        }
        // 切换数据源
        DataSourceContextHolder.setDataSourceKey(token);
        System.out.println(DataSourceContextHolder.getDataSourceKey());
    }

    /**
     * 重置数据源
     */
    @After("execution(public * com.hydrogen.saas.controller.*.*(..))")
    public void restoreDataSource() {
        // 将数据源置为默认数据源
        System.out.println("重置数据源");
        DataSourceContextHolder.clearDataSourceKey();
    }
}

切面异常处理

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = RuntimeException.class)
    @ResponseBody
    public Map exceptionHandler(RuntimeException e) {
        Map result = new HashMap();
        result.put("code", 401);
        result.put("msg", e.getMessage());
        result.put("data", null);
        return result;
    }
}

第四部分(测试)

分页查询接口

@Log4j2
@RestController
@RequestMapping("address")
public class AddressController {
    @Autowired
    private AddressService addressService;

    @GetMapping("getAddress")
    public PageInfo<Address> getAddress() {
        PageInfo<Address> pageInfo = new PageInfo<>();
        try {
            pageInfo = addressService.getList(2,2);
        } catch (Exception e) {
            log.error("getAddress Exception: ", e);
        }
        return pageInfo;
    }
}

请求头中添加token信息

请求头

响应结果

test

{
    "total": 4,
    "list": [
        {
            "id": 3,
            "stuId": "3",
            "address": "杭州萧山区"
        },
        {
            "id": 4,
            "stuId": "4",
            "address": "上海静安"
        }
    ],
    "pageNum": 2,
    "pageSize": 2,
    "size": 2,
    "startRow": 3,
    "endRow": 4,
    "pages": 2,
    "prePage": 1,
    "nextPage": 0,
    "isFirstPage": false,
    "isLastPage": true,
    "hasPreviousPage": true,
    "hasNextPage": false,
    "navigatePages": 8,
    "navigatepageNums": [
        1,
        2
    ],
    "navigateFirstPage": 1,
    "navigateLastPage": 2
}

更换请求头中token信息

请求头

响应结果

test2

{
    "total": 6,
    "list": [
        {
            "id": 333,
            "stuId": "33",
            "address": "深圳"
        },
        {
            "id": 444,
            "stuId": "44",
            "address": "广州"
        }
    ],
    "pageNum": 2,
    "pageSize": 2,
    "size": 2,
    "startRow": 3,
    "endRow": 4,
    "pages": 3,
    "prePage": 1,
    "nextPage": 3,
    "isFirstPage": false,
    "isLastPage": false,
    "hasPreviousPage": true,
    "hasNextPage": true,
    "navigatePages": 8,
    "navigatepageNums": [
        1,
        2,
        3
    ],
    "navigateFirstPage": 1,
    "navigateLastPage": 3
}

控制台

控制台

非法token异常

非法token

  • 3
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Springboot+MyBatis-Plus实现多租户动态数据源模式是一种在Spring Boot框架下使用MyBatis-Plus插件实现多租户数据隔离的方法。它可以根据不同的租户动态切换数据源,实现不同租户之间的数据隔离。 实现多租户动态数据源模式的关键是配置多个数据源,并在运行时根据租户信息动态选择使用哪个数据源。以下是一个简单的示例代码: 1. 首先,需要在pom.xml文件中添加Druid数据源的依赖: ```xml <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.21</version> </dependency> ``` 2. 在application.properties或application.yml文件中配置多个数据源的连接信息,例如: ```yaml spring.datasource.master.url=jdbc:mysql://localhost:3306/master_db spring.datasource.master.username=root spring.datasource.master.password=123456 spring.datasource.tenant1.url=jdbc:mysql://localhost:3306/tenant1_db spring.datasource.tenant1.username=root spring.datasource.tenant1.password=123456 spring.datasource.tenant2.url=jdbc:mysql://localhost:3306/tenant2_db spring.datasource.tenant2.username=root spring.datasource.tenant2.password=123456 ``` 3. 创建一个多租户数据源配置类,用于动态选择数据源。可以使用ThreadLocal来保存当前租户的标识,然后根据标识选择对应的数据源。以下是一个简单的示例: ```java @Configuration public class MultiTenantDataSourceConfig { @Autowired private DataSourceProperties dataSourceProperties; @Bean @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.tenant1") public DataSource tenant1DataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.tenant2") public DataSource tenant2DataSource() { return DataSourceBuilder.create().build(); } @Bean @Primary public DataSource dynamicDataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); Map<Object, Object> dataSourceMap = new HashMap<>(); dataSourceMap.put("master", masterDataSource()); dataSourceMap.put("tenant1", tenant1DataSource()); dataSourceMap.put("tenant2", tenant2DataSource()); dynamicDataSource.setTargetDataSources(dataSourceMap); dynamicDataSource.setDefaultTargetDataSource(masterDataSource()); return dynamicDataSource; } @Bean public SqlSessionFactory sqlSessionFactory(DataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(dynamicDataSource); return sessionFactory.getObject(); } @Bean public PlatformTransactionManager transactionManager(DataSource dynamicDataSource) { return new DataSourceTransactionManager(dynamicDataSource); } } ``` 4. 创建一个多租户数据源切换器,用于在每次数据库操作前切换数据源。以下是一个简单的示例: ```java public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return TenantContext.getTenantId(); } } ``` 5. 创建一个租户上下文类,用于保存当前租户的标识。以下是一个简单的示例: ```java public class TenantContext { private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>(); public static void setTenantId(String tenantId) { CONTEXT.set(tenantId); } public static String getTenantId() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } } ``` 6. 在需要切换数据源的地方,调用TenantContext.setTenantId()方法设置当前租户的标识。例如: ```java @RestController public class UserController { @Autowired private UserService userService; @GetMapping("/users") public List<User> getUsers() { TenantContext.setTenantId("tenant1"); List<User> users = userService.getUsers(); TenantContext.clear(); return users; } } ``` 通过以上步骤,就可以实现Springboot+MyBatis-Plus的多租户动态数据源模式了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值