业务场景
现有股票与基金业务,不同的业务分在不同的库中,但有些业务类似可以基于同一套代码,例如组织架构、权限控制与客户管理,但是为区分业务线,要将数据拆分在不同的数据库中
达成效果
不同的业务传对应的业务参数,保存到相应的库中
基金业务
新增用户
查询用户
股票业务
新增用户
查询用户
具体实现
项目结构
数据库
表结构
CREATE TABLE `rbac_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(64) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
代码实现
1. POM依赖
xml version="1.0" encoding="UTF-8"?> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot spring-boot-starter-parent 2.4.0 com.sure learn-mybatis-dynamic-datasource 0.0.1-SNAPSHOT learn-mybatis-dynamic-datasource Demo project for Spring Boot 1.8 org.springframework.boot spring-boot-starter-web org.mybatis.spring.boot mybatis-spring-boot-starter 2.1.4 mysql mysql-connector-java runtime org.projectlombok lombok true org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin
2. 配置文件
不同的业务线配置不同的数据库
spring: datasource: fund: driverClassName: com.mysql.cj.jdbc.Driver username: root password: root jdbcUrl: jdbc:mysql://localhost:3306/fund?useUnicode=true&characterEncoding=utf8 stock: driverClassName: com.mysql.cj.jdbc.Driver username: root password: root jdbcUrl: jdbc:mysql://localhost:3306/stock?useUnicode=true&characterEncoding=utf8logging: level: com.sure: debug
3. 创建业务平台枚举类型
/** * 业务平台枚举 * * @author sure */public enum PlatformEnum { FUND("fund"), STOCK("stock"); public String value; PlatformEnum(String value){ this.value = value; }}
4. 动态数据源上下文管理
/** * 动态数据源上下文管理 * * @author sure */public class DataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); /** * 设置数据源 * * @param dataSourceKey */ public static void setDataSource(String dataSourceKey){ contextHolder.set(dataSourceKey); } /** * 获取数据源 * @return */ public static String getDataSource(){ return contextHolder.get(); } /** * 清空数据源设置 */ public static void clearDataSource(){ contextHolder.remove(); }}
5.动态数据源
/** * 动态数据源 */public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); }}
6. Mybatis多数据源配置
/** * Mybatais 动态数据源配置 * * @author sure */@Configuration@MapperScan(basePackages = "com.sure.learnmybatisdynamicdatasource.mapper")public class MybaticDynamicDataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.fund") public DataSource fundDataSource(){ return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.stock") public DataSource stockDataSource(){ return DataSourceBuilder.create().build(); } @Bean public DynamicDataSource dataSource(@Qualifier("fundDataSource") DataSource fundDataSource, @Qualifier("stockDataSource") DataSource stockDataSource){ Map<Object,Object> dataSources = new HashMap<>(); dataSources.put(PlatformEnum.FUND.value,fundDataSource); dataSources.put(PlatformEnum.STOCK.value,stockDataSource); DynamicDataSource dynamicDataSource = new DynamicDataSource(); dynamicDataSource.setTargetDataSources(dataSources); dynamicDataSource.setDefaultTargetDataSource(fundDataSource); return dynamicDataSource; } @Bean public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dynamicDataSource); // 扫描mapper路径 Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"); sqlSessionFactoryBean.setMapperLocations(resources); return sqlSessionFactoryBean.getObject(); } @Bean public PlatformTransactionManager transactionManager(DynamicDataSource dynamicDataSource){ return new DataSourceTransactionManager(dynamicDataSource); }}
7. 动态数据源切面
/** * 动态数据源切面 * * @author sure */@Aspect@Componentpublic class DataSourceAspect { @Pointcut("execution(public * com.sure.learnmybatisdynamicdatasource.mapper..*.*(..))") public void aop(){} @Around("aop()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { Object[] args = joinPoint.getArgs(); if (Objects.nonNull(args) && args.length>0){ DataSourceContextHolder.setDataSource(args[0].toString()); } try { return joinPoint.proceed(); }finally { DataSourceContextHolder.clearDataSource(); } }}
8. 编写Mapper
注意mapper中的第一个参数预留来指定数据源
@Mapperpublic interface RbacUserMapper { Integer save(String dataSource, @Param("user") RbacUserDO rbacUserDO); RbacUserDO get(String dataSource, @Param("id") Integer id);}
namespace="com.sure.learnmybatisdynamicdatasource.mapper.RbacUserMapper"> id="save" keyProperty="id"> insert into rbac_user(name) values (#{user.name}) id="get" resultType="com.sure.learnmybatisdynamicdatasource.domain.RbacUserDO"> select id,name from rbac_user where id = #{id}
9.编写service
同样注意方法参数中的数据源参数
@Servicepublic class RbacUserService { @Autowired private RbacUserMapper rbacUserMapper; public Integer save(String dataSource, RbacUserDO rbacUserDO){ return rbacUserMapper.save(dataSource, rbacUserDO); } public RbacUserDO get(String dataSource, Integer id){ RbacUserDO rbacUserDO = rbacUserMapper.get(dataSource, id); return rbacUserDO; }}
10. 编写Controller层
通用返回参数
@Datapublic class Result<T> implements Serializable { private Integer code; private String msg; private T data; private Result(Integer code , String msg){ this.code = code; this.msg = msg; } private Result(Integer code , String msg, T data){ this.code = code; this.msg = msg; this.data = data; } public static Result ok(){ return new Result(ResultEnum.SUCCESS.code,ResultEnum.SUCCESS.msg); } public static <T> Result ok(T data){ return new Result(ResultEnum.SUCCESS.code,ResultEnum.SUCCESS.msg,data); } public static Result error(){ return new Result(ResultEnum.ERROR.code,ResultEnum.ERROR.msg); } public static Result error(Integer code,String msg){ return new Result(code,msg); }}
状态码枚举
public enum ResultEnum { SUCCESS(1,"success"), ERROR(-1,"system error"); public Integer code; public String msg; ResultEnum (Integer code,String msg){ this.code = code; this.msg = msg; }}
使用path参数来指定业务平台
@RestController@RequestMapping("api/1")public class RbacUserController { @Autowired public RbacUserService rbacUserService; @PostMapping("{platform}/user") public Result addUser(@PathVariable String platform, RbacUserDO rbacUserDO){ return Result.ok(rbacUserService.save(platform, rbacUserDO)); } @GetMapping("{platform}/user/{id}") public Result getUser(@PathVariable("platform") String platform,@PathVariable("id") Integer id){ RbacUserDO rbacUserDO = rbacUserService.get(platform, id); return Result.ok(rbacUserDO); }}
踩到的坑
注意切面类返回值!!!