Springboot+MyBatis-Plus實現(xiàn)多租戶動態(tài)數(shù)據(jù)源模式

一、先實現(xiàn)動態(tài)數(shù)據(jù)源上下文模式代碼,保證在多租戶模式下,能自動根據(jù)租戶Id切換數(shù)據(jù)源

/**
 * 動態(tài)數(shù)據(jù)源上下文
 *
 * @author 李嘉
 * @version 1.0
 * @Description 動態(tài)數(shù)據(jù)源上下文
 * @date 2020/5/18 23:33
 */
public class DynamicDataSourceContextHolder {


    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
        /**
         * 將 master 數(shù)據(jù)源的 key作為默認數(shù)據(jù)源的 key
         */
        @Override
        protected String initialValue() {
            return "master";
        }
    };


    /**
     * 數(shù)據(jù)源的 key集合,用于切換時判斷數(shù)據(jù)源是否存在
     */
    public static List<Object> dataSourceKeys = new ArrayList<>();


    /**
     * 切換數(shù)據(jù)源
     * @param key  數(shù)據(jù)源
     */
    public static void setDataSourceKey(String key) {
        if (!StringUtil.isEmpty(key)) {
            contextHolder.set(key);
        }
    }


    /**
     * 獲取數(shù)據(jù)源
     * @return
     */
    public static String getDataSourceKey() {
        return contextHolder.get();
    }


    /**
     * 重置數(shù)據(jù)源
     */
    public static void clearDataSourceKey() {
        contextHolder.remove();
    }


    /**
     * 判斷是否包含數(shù)據(jù)源
     * @param key   數(shù)據(jù)源
     * @return
     */
    public static boolean containDataSourceKey(String key) {
        return dataSourceKeys.contains(key);
    }


    /**
     * 添加數(shù)據(jù)源Keys
     * @param keys
     * @return
     */
    public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
        return dataSourceKeys.addAll(keys);
    }
}

二、實現(xiàn)動態(tài)數(shù)據(jù)源添加和設(shè)置,并繼承自AbstractRoutingDataSource類,實現(xiàn)其determineTargetDataSource和determineCurrentLookupKey方法

/**
 * 動態(tài)數(shù)據(jù)源
 *
 * @author 李嘉
 * @version 1.0
 * @Description 動態(tài)數(shù)據(jù)源
 * @date 2020/5/18 23:26
 */
public class DynamicDataSource extends AbstractRoutingDataSource {


    /**
     * 如果不希望數(shù)據(jù)源在啟動配置時就加載好,可以定制這個方法,從任何你希望的地方讀取并返回數(shù)據(jù)源
     * 比如從數(shù)據(jù)庫、文件、外部接口等讀取數(shù)據(jù)源信息,并最終返回一個DataSource實現(xiàn)類對象即可
     * @return
     */
    @Override
    protected DataSource determineTargetDataSource() {
        return super.determineTargetDataSource();
    }


    /**
     * 如果希望所有數(shù)據(jù)源在啟動配置時就加載好,這里通過設(shè)置數(shù)據(jù)源Key值來切換數(shù)據(jù),定制這個方法
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }


    /**
     * 設(shè)置默認數(shù)據(jù)源
     * @param defaultDataSource
     */
    public void setDefaultDataSource(Object defaultDataSource) {
        super.setDefaultTargetDataSource(defaultDataSource);
    }


    public void setDataSources(Map<Object, Object> dataSources) {
        super.setTargetDataSources(dataSources);
        // TODO 將數(shù)據(jù)源的 key 放到數(shù)據(jù)源上下文的 key 集合中,用于切換時判斷數(shù)據(jù)源是否有效
        DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
    }
}

三、實現(xiàn)動態(tài)數(shù)據(jù)源切面攔截,并根據(jù)租戶Id實現(xiàn)數(shù)據(jù)源的動態(tài)切換

/**
 * 動態(tài)數(shù)據(jù)源切面攔截
 *
 * @author 李嘉
 * @version 1.0
 * @Description 動態(tài)數(shù)據(jù)源切面攔截
 * @date 2020/5/19 00:29
 */
@Slf4j
@Aspect
@Component
@Order(1) // 請注意:這里order一定要小于tx:annotation-driven的order,即先執(zhí)行DynamicDataSourceAspectAdvice切面,再執(zhí)行事務(wù)切面,才能獲取到最終的數(shù)據(jù)源
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DynamicDataSourceAspect {


    @Around("execution(* com.example.demo.controller.*.*(..)) || execution(* com.example.demo.*.*(..))")
    public Object doAround(ProceedingJoinPoint jp) throws Throwable {
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        Object result = null;
        try {
            HttpServletRequest request = sra.getRequest();
            HttpSession session = sra.getRequest().getSession(true);
            String tenantId = (String)session.getAttribute("tenantId");
            if (StringUtil.isEmpty(tenantId)) {
                tenantId = request.getParameter("tenantId");
            }


            log.info("當前租戶Id:{}", tenantId);
            if (!StringUtil.isEmpty(tenantId)) {
                DynamicDataSourceContextHolder.setDataSourceKey(tenantId);
                result = jp.proceed();
            } else {
                result = CustomResult.Fail("查詢失敗,當前租戶信息未取到,請聯(lián)系技術(shù)專家!");
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            result = CustomResult.Fail("系統(tǒng)異常,請聯(lián)系技術(shù)專家!");
        } finally {
            DynamicDataSourceContextHolder.clearDataSourceKey();
        }
        return result;
    }
}

四、實現(xiàn)動態(tài)數(shù)據(jù)源初始化,并將租戶信息表中的數(shù)據(jù)庫鏈接等查詢出來一并初始化

/**
 * 動態(tài)數(shù)據(jù)源初始化
 *
 * @author 李嘉
 * @version 1.0
 * @Description 動態(tài)數(shù)據(jù)源初始化
 * @date 2020/5/19 00:08
 */
@Slf4j
@Configuration
public class DynamicDataSourceInit {


    @Autowired
    private ITenantInfoService tenantInfoService;


    @Bean
    public void initDataSource() {
        log.info("======初始化動態(tài)數(shù)據(jù)源=====");
        DynamicDataSource dynamicDataSource = (DynamicDataSource) SpringContextUtils.getBean("dynamicDataSource");
        HikariDataSource master = (HikariDataSource) SpringContextUtils.getBean("master");
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("master", master);


        List<TenantInfo> tenantList = tenantInfoService.list();
        for (TenantInfo tenantInfo : tenantList) {
            log.info(tenantInfo.toString());
            HikariDataSource dataSource = new HikariDataSource();
            dataSource.setDriverClassName(tenantInfo.getDatasourceDriver());
            dataSource.setJdbcUrl(tenantInfo.getDatasourceUrl());
            dataSource.setUsername(tenantInfo.getDatasourceUsername());
            dataSource.setPassword(tenantInfo.getDatasourcePassword());
            dataSource.setDataSourceProperties(master.getDataSourceProperties());
            dataSourceMap.put(tenantInfo.getTenantId(), dataSource);
        }
        // 設(shè)置數(shù)據(jù)源
        dynamicDataSource.setDataSources(dataSourceMap);
        /**
         * 必須執(zhí)行此操作,才會重新初始化AbstractRoutingDataSource 中的 resolvedDataSources,也只有這樣,動態(tài)切換才會起效
         */
        dynamicDataSource.afterPropertiesSet();
    }
}

五、配置Mybatis

/**
 * MyBatisPlus配置
 *
 * @author 李嘉
 * @version 1.0
 * @Description MyBatisPlus配置
 * @date 2020/5/18 23:50
 */
@EnableTransactionManagement
@Configuration
@MapperScan({"com.example.demo.dao","com.example.demo.*.*.mapper"})
public class MybatisPlusConfig {


    @Bean("master")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.hikari")
    public DataSource master() {
        return DataSourceBuilder.create().build();
    }


    @Bean("dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("master", master());
        // 將 master 數(shù)據(jù)源作為默認指定的數(shù)據(jù)源
        dynamicDataSource.setDefaultDataSource(master());
        // 將 master 和 slave 數(shù)據(jù)源作為指定的數(shù)據(jù)源
        dynamicDataSource.setDataSources(dataSourceMap);
        return dynamicDataSource;
    }


    @Bean
    public MybatisSqlSessionFactoryBean sqlSessionFactoryBean() throws Exception {
        MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
        /**
         * 重點,使分頁插件生效
         */
        Interceptor[] plugins = new Interceptor[1];
        plugins[0] = paginationInterceptor();
        sessionFactory.setPlugins(plugins);
        //配置數(shù)據(jù)源,此處配置為關(guān)鍵配置,如果沒有將 dynamicDataSource作為數(shù)據(jù)源則不能實現(xiàn)切換
        sessionFactory.setDataSource(dynamicDataSource());
        // 掃描Model
        sessionFactory.setTypeAliasesPackage("com.example.demo.*.*.entity,com.example.demo.model");
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        // 掃描映射文件
        sessionFactory.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml"));
        return sessionFactory;
    }


    @Bean
    public PlatformTransactionManager transactionManager() {
        // 配置事務(wù)管理, 使用事務(wù)時在方法頭部添加@Transactional注解即可
        return new DataSourceTransactionManager(dynamicDataSource());
    }


    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();


        List<ISqlParser> sqlParserList = new ArrayList<>();
        // 攻擊 SQL 阻斷解析器、加入解析鏈
        sqlParserList.add(new BlockAttackSqlParser());
        paginationInterceptor.setSqlParserList(sqlParserList);
        return paginationInterceptor;
    }
}

六、租戶表相關(guān)建表語句

CREATE TABLE `tenant_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`TENANT_ID` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '租戶id',
`TENANT_NAME` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '租戶名稱',
`DATASOURCE_URL` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '數(shù)據(jù)源url',
`DATASOURCE_USERNAME` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '數(shù)據(jù)源用戶名',
`DATASOURCE_PASSWORD` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '數(shù)據(jù)源密碼',
`DATASOURCE_DRIVER` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '數(shù)據(jù)源驅(qū)動',
`SYSTEM_ACCOUNT` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '系統(tǒng)賬號',
`SYSTEM_PASSWORD` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '賬號密碼',
`SYSTEM_PROJECT` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '系統(tǒng)PROJECT',
`STATUS` tinyint(1) DEFAULT NULL COMMENT '是否啟用(1是0否)',
`CREATE_TIME` datetime DEFAULT NULL COMMENT '創(chuàng)建時間',
`UPDATE_TIME` datetime DEFAULT NULL COMMENT '更新時間',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;


SET FOREIGN_KEY_CHECKS = 1;

實現(xiàn)源碼地址https://github.com/achievejia/springboot_saas

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容