美文网首页关系型DB(MySQL,MyBatis )
Springboot+MyBatis-Plus实现多租户动态数据

Springboot+MyBatis-Plus实现多租户动态数据

作者: iBaron | 来源:发表于2020-05-19 14:08 被阅读0次

    一、先实现动态数据源上下文模式代码,保证在多租户模式下,能自动根据租户Id切换数据源

    /**
     * 动态数据源上下文
     *
     * @author 李嘉
     * @version 1.0
     * @Description 动态数据源上下文
     * @date 2020/5/18 23:33
     */
    public class DynamicDataSourceContextHolder {
    
    
        private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
            /**
             * 将 master 数据源的 key作为默认数据源的 key
             */
            @Override
            protected String initialValue() {
                return "master";
            }
        };
    
    
        /**
         * 数据源的 key集合,用于切换时判断数据源是否存在
         */
        public static List<Object> dataSourceKeys = new ArrayList<>();
    
    
        /**
         * 切换数据源
         * @param key  数据源
         */
        public static void setDataSourceKey(String key) {
            if (!StringUtil.isEmpty(key)) {
                contextHolder.set(key);
            }
        }
    
    
        /**
         * 获取数据源
         * @return
         */
        public static String getDataSourceKey() {
            return contextHolder.get();
        }
    
    
        /**
         * 重置数据源
         */
        public static void clearDataSourceKey() {
            contextHolder.remove();
        }
    
    
        /**
         * 判断是否包含数据源
         * @param key   数据源
         * @return
         */
        public static boolean containDataSourceKey(String key) {
            return dataSourceKeys.contains(key);
        }
    
    
        /**
         * 添加数据源Keys
         * @param keys
         * @return
         */
        public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
            return dataSourceKeys.addAll(keys);
        }
    }
    

    二、实现动态数据源添加和设置,并继承自AbstractRoutingDataSource类,实现其determineTargetDataSource和determineCurrentLookupKey方法

    /**
     * 动态数据源
     *
     * @author 李嘉
     * @version 1.0
     * @Description 动态数据源
     * @date 2020/5/18 23:26
     */
    public class DynamicDataSource extends AbstractRoutingDataSource {
    
    
        /**
         * 如果不希望数据源在启动配置时就加载好,可以定制这个方法,从任何你希望的地方读取并返回数据源
         * 比如从数据库、文件、外部接口等读取数据源信息,并最终返回一个DataSource实现类对象即可
         * @return
         */
        @Override
        protected DataSource determineTargetDataSource() {
            return super.determineTargetDataSource();
        }
    
    
        /**
         * 如果希望所有数据源在启动配置时就加载好,这里通过设置数据源Key值来切换数据,定制这个方法
         * @return
         */
        @Override
        protected Object determineCurrentLookupKey() {
            return DynamicDataSourceContextHolder.getDataSourceKey();
        }
    
    
        /**
         * 设置默认数据源
         * @param defaultDataSource
         */
        public void setDefaultDataSource(Object defaultDataSource) {
            super.setDefaultTargetDataSource(defaultDataSource);
        }
    
    
        public void setDataSources(Map<Object, Object> dataSources) {
            super.setTargetDataSources(dataSources);
            // TODO 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效
            DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
        }
    }
    

    三、实现动态数据源切面拦截,并根据租户Id实现数据源的动态切换

    /**
     * 动态数据源切面拦截
     *
     * @author 李嘉
     * @version 1.0
     * @Description 动态数据源切面拦截
     * @date 2020/5/19 00:29
     */
    @Slf4j
    @Aspect
    @Component
    @Order(1) // 请注意:这里order一定要小于tx:annotation-driven的order,即先执行DynamicDataSourceAspectAdvice切面,再执行事务切面,才能获取到最终的数据源
    @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("查询失败,当前租户信息未取到,请联系技术专家!");
                }
            } catch (Exception ex) {
                ex.printStackTrace();
                result = CustomResult.Fail("系统异常,请联系技术专家!");
            } finally {
                DynamicDataSourceContextHolder.clearDataSourceKey();
            }
            return result;
        }
    }
    

    四、实现动态数据源初始化,并将租户信息表中的数据库链接等查询出来一并初始化

    /**
     * 动态数据源初始化
     *
     * @author 李嘉
     * @version 1.0
     * @Description 动态数据源初始化
     * @date 2020/5/19 00:08
     */
    @Slf4j
    @Configuration
    public class DynamicDataSourceInit {
    
    
        @Autowired
        private ITenantInfoService tenantInfoService;
    
    
        @Bean
        public void initDataSource() {
            log.info("======初始化动态数据源=====");
            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);
            }
            // 设置数据源
            dynamicDataSource.setDataSources(dataSourceMap);
            /**
             * 必须执行此操作,才会重新初始化AbstractRoutingDataSource 中的 resolvedDataSources,也只有这样,动态切换才会起效
             */
            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 数据源作为默认指定的数据源
            dynamicDataSource.setDefaultDataSource(master());
            // 将 master 和 slave 数据源作为指定的数据源
            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);
            //配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource作为数据源则不能实现切换
            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() {
            // 配置事务管理, 使用事务时在方法头部添加@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;
        }
    }
    

    六、租户表相关建表语句

    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 '数据源url',
    `DATASOURCE_USERNAME` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '数据源用户名',
    `DATASOURCE_PASSWORD` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '数据源密码',
    `DATASOURCE_DRIVER` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '数据源驱动',
    `SYSTEM_ACCOUNT` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '系统账号',
    `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 '系统PROJECT',
    `STATUS` tinyint(1) DEFAULT NULL COMMENT '是否启用(1是0否)',
    `CREATE_TIME` datetime DEFAULT NULL COMMENT '创建时间',
    `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;
    

    实现源码地址https://github.com/achievejia/springboot_saas

    该资料整理自https://blog.csdn.net/qq_36521507/article/details/103452961

    相关文章

      网友评论

        本文标题:Springboot+MyBatis-Plus实现多租户动态数据

        本文链接:https://www.haomeiwen.com/subject/lahbohtx.html