springboot动态切换多租户

文章目录

      • 场景
      • pom配置
      • 创建2个演示的数据库
      • 3. 创建2个简单的接口
          • 3.1 用户列表接口
          • 3.2 商品列表接口
      • 4. 定义基本上数据类型BaseDto (用来标识卖家信息 生产环境可以使用token替代)
      • 创建切面
      • 实现原理

场景

租户多且不固定且多服务场景动态实现

pom配置



    4.0.0
    
        spring-boot-starter-parent
        org.springframework.boot
        2.3.7.RELEASE
    

    com.carsonlius
    dynamic-datasource-project
    pom
    1.0-SNAPSHOT
    
        pd-goods
    

    
        8
        8
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.2.0
        
        

        
            com.baomidou
            dynamic-datasource-spring-boot-starter
            3.5.1
        
        
            mysql
            mysql-connector-java
            runtime
        

        
            com.alibaba
            druid-spring-boot-starter
            1.1.21
        
        
            org.projectlombok
            lombok
        
        
            p6spy
            p6spy
            3.9.0
        

        
            io.springfox
            springfox-swagger2
            2.9.2
        
        
            io.springfox
            springfox-swagger-ui
            2.9.2
        


        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-config
            2.1.1.RELEASE
        

        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-discovery
            2.1.1.RELEASE
        
    

创建2个演示的数据库

-- pd_auth

CREATE TABLE `pd_auth_user` (
  `id` bigint(20) NOT NULL COMMENT 'ID',
  `account` varchar(30) NOT NULL COMMENT '账号',
  `name` varchar(50) NOT NULL COMMENT '姓名',
  `org_id` bigint(20) DEFAULT NULL COMMENT '组织ID\n#c_core_org',
  `station_id` bigint(20) DEFAULT NULL COMMENT '岗位ID\n#c_core_station',
  `email` varchar(255) DEFAULT NULL COMMENT '邮箱',
  `mobile` varchar(20) DEFAULT '' COMMENT '手机',
  `sex` varchar(1) DEFAULT 'N' COMMENT '性别\n#Sex{W:女;M:男;N:未知}',
  `status` bit(1) DEFAULT b'0' COMMENT '启用状态 1启用 0禁用',
  `avatar` varchar(255) DEFAULT '' COMMENT '头像',
  `work_describe` varchar(255) DEFAULT '' COMMENT '工作描述\r\n比如:  市长、管理员、局长等等   用于登陆展示',
  `password_error_last_time` datetime DEFAULT NULL COMMENT '最后一次输错密码时间',
  `password_error_num` int(11) DEFAULT '0' COMMENT '密码错误次数',
  `password_expire_time` datetime DEFAULT NULL COMMENT '密码过期时间',
  `password` varchar(64) NOT NULL DEFAULT '' COMMENT '密码',
  `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
  `create_user` bigint(20) DEFAULT '0' COMMENT '创建人id',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_user` bigint(20) DEFAULT '0' COMMENT '更新人id',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `UN_ACCOUNT` (`account`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='用户'

-- pd_goods
CREATE TABLE `pd_goods_info` (
  `id` bigint(20) NOT NULL COMMENT '商品ID',
  `code` char(16) COLLATE utf8mb4_bin NOT NULL COMMENT '商品编码',
  `name` varchar(20) COLLATE utf8mb4_bin NOT NULL COMMENT '商品名称',
  `bar_code` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '国条码',
  `brand_id` bigint(20) DEFAULT NULL COMMENT '品牌表ID',
  `one_category_id` bigint(20) DEFAULT NULL COMMENT '一级分类ID',
  `two_category_id` bigint(20) DEFAULT NULL COMMENT '二级分类ID',
  `three_category_id` bigint(20) DEFAULT NULL COMMENT '三级分类ID',
  `supplier_id` bigint(20) DEFAULT NULL COMMENT '商品的供应商ID',
  `price` decimal(8,2) NOT NULL COMMENT '商品售价价格',
  `average_cost` decimal(18,2) NOT NULL COMMENT '商品加权平均成本',
  `publish_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '上下架状态:0下架,1上架',
  `audit_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '审核状态: 0未审核,1已审核',
  `weight` float DEFAULT NULL COMMENT '商品重量',
  `length` float DEFAULT NULL COMMENT '商品长度',
  `height` float DEFAULT NULL COMMENT '商品重量',
  `width` float DEFAULT NULL COMMENT '商品宽度',
  `color` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '颜色',
  `production_date` datetime NOT NULL COMMENT '生产日期',
  `shelf_life` int(11) NOT NULL COMMENT '商品有效期',
  `descript` text COLLATE utf8mb4_bin COMMENT '商品描述',
  `update_time` datetime DEFAULT NULL,
  `update_user` bigint(20) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `create_user` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='商品信息表'

3. 创建2个简单的接口

3.1 用户列表接口
package com.carsonlius.controller;

import com.carsonlius.dto.BaseDto;
import com.carsonlius.entity.User;
import com.carsonlius.services.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @Author carsonlius
 * @Date 2022/3/6 17:43
 * @Version 1.0
 */
@RestController
@RequestMapping("/user")
@Api(value = "用户模块", tags = "用户模块")
public class UserController {
    @Autowired
    private UserService userService;

    /**
     * 获取users列表
     * */
    @GetMapping
    @ApiOperation(value = "用户列表", response = List.class, httpMethod = "GET")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "merchantId", value = "", dataType = "String", paramType = "query", required= true)
    })
    public List<User> lists(BaseDto baseDto){

        return userService.lists();
    }
}


3.2 商品列表接口
package com.carsonlius.controller;

import com.carsonlius.dto.BaseDto;
import com.carsonlius.entity.GoodsInfo;
import com.carsonlius.services.GoodsService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @version V1.0
 * @author: liusen
 * @date: 2022年02月25日 15时55分
 * @contact 
 * @company
 */
@RestController
@RequestMapping("/goods")
@Api(value = "商品模块", tags = "商品模块")
public class GoodsController {
    @Autowired
    private GoodsService goodsService;

    @GetMapping("/list")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "merchantId", value = "", dataType = "String", paramType = "query", required= true)
    })
    @ApiOperation(value = "获取全量商品列表", httpMethod = "GET", response = List.class)
    public List<GoodsInfo> goods(BaseDto baseDto){
        return goodsService.getGoods();
    }
}

4. 定义基本上数据类型BaseDto (用来标识卖家信息 生产环境可以使用token替代)

package com.carsonlius.dto;

import lombok.Data;

/**
 * 基参
 * @Author carsonlius
 * @Date 2022/3/12 15:38
 * @Version 1.0
 */
@Data
public class BaseDto {
    /**
     * 租户ID
     * */
    public String merchantId;
}

创建切面

package com.carsonlius.aspects;

import com.alibaba.druid.util.StringUtils;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.carsonlius.config.PinDaConfig;
import com.carsonlius.dto.BaseDto;
import com.carsonlius.dto.DataSourceDTO;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.util.UUID;


/**
 * @Author carsonlius
 * @Date 2022/3/6 19:01
 * @Version 1.0
 */
@Component
@Aspect
public class DynamicChangeDatasource {
    private Logger logger = LoggerFactory.getLogger(DynamicChangeDatasource.class);

    @Autowired
    private PinDaConfig pinDaConfig;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private DefaultDataSourceCreator dataSourceCreator;

    @Pointcut("execution(* com.carsonlius.controller.*.*(..))")
    public void changeDatasource() {
    }

    @Around("changeDatasource()")
    public Object around(ProceedingJoinPoint joinPoint) {
        Object result = null;
        String poolName = "";
        try {

            //
            logger.info("开始加载数据库");
            poolName = setDataSource(joinPoint);
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        } finally {
            logger.info("执行切换数据源之后");
            removeDatasource(poolName);
            //Do Something useful, If you have
        }
        return result;
    }

    /**
     * 删除久数据源
     */
    private void removeDatasource(String poolName) {
        if (StringUtils.isEmpty(poolName)) {
            logger.error("没有加载到数据库:" + poolName);
            return;
        }

        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        ds.removeDataSource(poolName);
    }

    /**
     * 切换数据源头
     */
    private String setDataSource(ProceedingJoinPoint joinPoint) {

        BaseDto baseDto = null;
        for (Object arg : joinPoint.getArgs()) {
            System.out.println("arg" + arg);
            if (arg instanceof BaseDto) {
                baseDto = (BaseDto) arg;
                break;
            }
        }

        // todo 缺少必须参数
//        if (baseDto == null || StringUtils.isEmpty(baseDto.getMerchantId())) {
//
//            throw new BizException(ExceptionCode.ILLEGALA_ARGUMENT_EX.getCode(), ExceptionCode.ILLEGALA_ARGUMENT_EX.getMsg());
//        }
        String merchantId = "goods";

        if (baseDto != null) {
            merchantId = baseDto.getMerchantId();
        }


        // todo 这里要从数据库获取商家数据库配置 (测试这里可以写死)
        DataSourceDTO dto = new DataSourceDTO();
        dto.setPassword(pinDaConfig.getPassword());
        dto.setUsername(pinDaConfig.getUsername());
        String poolName = wrapperPoolName(merchantId);
        dto.setPoolName(poolName);

        String url = "";
        String ip = pinDaConfig.getIp();
        String port = pinDaConfig.getPort();
        if ("goods".equals(merchantId)) {

            url = "jdbc:mysql://" + ip + ":" + port + "/pd_goods?serverTimezone=CTT&characterEncoding=utf8&useUnicode=true&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true";
        } else {
            url = "jdbc:mysql://" + ip + ":" + port + "/pd_auth?serverTimezone=CTT&characterEncoding=utf8&useUnicode=true&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true";
        }

        dto.setUrl(url);

        // 注入数据源
        add(dto);

        //  手动切换数据源
        DynamicDataSourceContextHolder.push(poolName);

        return poolName;
    }

    /**
     * 保证PoolName
     *
     * @param merchantId
     * @return
     */
    private String wrapperPoolName(String merchantId) {
        String uuid = UUID.randomUUID().toString().replace("-", "");
        return merchantId.concat("_").concat(uuid);
    }

    /**
     * 添加数据源
     * 通用数据源会根据maven中配置的连接池根据顺序依次选择 默认的顺序为druid>hikaricp>beecp>dbcp>spring basic
     */
    public void add(DataSourceDTO dto) {
        // 添加数据源
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        BeanUtils.copyProperties(dto, dataSourceProperty);
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
        ds.addDataSource(dto.getPoolName(), dataSource);
    }
}

实现原理

1. 节点: 进入impl实现之前设置数据源(可能需要先添加数据源); 离开impl之后 删除数据源

2. 新增数据源的实现: dynamic-datasource-spring-boot-starter 实现,操作简单

你可能感兴趣的:(java,java)