租户多且不固定且多服务场景动态实现
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
-- 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='商品信息表'
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();
}
}
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();
}
}
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 实现,操作简单