实现Mybatis-CommonMapper通用增删改查功能记录(四-条件构造-动态数据源)

前序

系列的前三篇详细描述此项目由搭建至引入AOP的步骤,现在已经能正确获取当前dao层操作的Spring上下文的bean对象,最后就是完成对基础增删改查的SQL条件的简单构造,完成后就实现了CommonMapper的功能,更复杂的功能可以以此类推,按照基础代码添加需要的逻辑即可。

上一篇文章:# 实现Mybatis-CommonMapper通用增删改查功能记录(三-引入编程式AOP)

源码地址点这

CommonMapper

根据前几篇文章下来,中间对代码进行了一些修改重构,目前CommonMapper完成增删改查的基础方法。

java 复制代码
package org.nott.mybatis.provider;

import org.nott.mybatis.model.MybatisSqlBean;
import org.nott.mybatis.sql.builder.DeleteSqlConditionBuilder;
import org.nott.mybatis.sql.builder.SqlBuilder;
import org.nott.mybatis.support.aop.ConCurrentMapperAopFactory;

import java.io.Serializable;
import java.util.List;
import java.util.Map;
@Component
@Scope("prototype")
public interface CommonMapper<T> {

    @SelectProvider(type = BaseSelectProvider.class, method = "selectById")
    T selectById(@Param("id") Serializable id);

    @SelectProvider(type = BaseSelectProvider.class, method = "selectOne")
    T selectOne();

    @SelectProvider(type = BaseSelectProvider.class, method = "selectList")
    List<T> selectList();

    @SelectProvider(type = BaseSelectProvider.class, method = "selectListByCondition")
    List<T> selectListByCondition(SqlConditionBuilder querySqlConditionBuilder);

    @SelectProvider(type = BaseSelectProvider.class, method = "pageCount")
    Long count();

    @SelectProvider(type = BaseSelectProvider.class, method = "pageCountByCondition")
    Long countByCondition(SqlConditionBuilder querySqlConditionBuilder);

   省略...

由上面的代码不难看出,基础的方法都依赖于@xxProvider提供的类和方法名来定位具体的SQL语句获取方法,以下简要用BaseSelectProvider作为例子说明一下流程。

java 复制代码
package org.nott.mybatis.provider;


import org.nott.mybatis.model.MybatisSqlBean;
import org.nott.mybatis.sql.builder.QuerySqlConditionBuilder;
import org.nott.mybatis.sql.builder.SqlBuilder;
import org.nott.mybatis.support.aop.ConCurrentMapperAopFactory;

import java.io.Serializable;
import java.util.Map;

/**
 * 基础 mybatis selectProvider
 */
public class BaseSelectProvider {

    public static String selectById(Map<String, Serializable> map) {
        Serializable id = map.get("id");
        MybatisSqlBean bean = ConCurrentMapperAopFactory.getBean();
        return SqlBuilder.buildFindByPkSql(bean, id);
    }
    ...
 }

这里放出了selectById方法作为例子,顾名思义是按照id查找实体方法,定义了MybatisSqlBean 作为数据库对象信息存放的实体,存放操作对象对应的表名、字段、主键、class等信息 。定义的ConCurrentMapperAopFactory 负责获取MybatisAopInterceptor 存放的当前线程操作信息,将它封装到MybatisSqlBean实体里的工厂对象。得到bean信息最后使用SqlBuilder完成mybatis SQL构造

MybatisSqlBean

java 复制代码
@Data
@Builder
public class MybatisSqlBean {

    /**
     * 表名
     */
    private String tableName;

    /**
     * 字段集合
     */
    private List<String> tableColums;

    /**
     * 主键
     */
    private Pk pk;

    /**
     * bean
     */
    private Object originalBean;

    /**
     * bean的Class
     */
    private Class<?> originalBeanClass;

}

ConCurrentMapperAopFactory

java 复制代码
public class ConCurrentMapperAopFactory {

    public ConCurrentMapperAopFactory() {
    }

    public static final ConCurrentMapperAopFactory FACTORY = new ConCurrentMapperAopFactory();

    /**
     * mapper与所属泛型的map
     */
    public static final Map<String,Class<?>> mapperGenericClassMap = new ConcurrentHashMap<>();
    public static MybatisSqlBean getBean(){
        return FACTORY.getCurrentMapperBean();
    }
    ...具体操作
 }

SqlBuilder

SqlBuilder实际上是对mybatis的SQL语句构建器的封装,基础用法如下,SqlBuilder内部按照条件对mybatis的SQL对象进行修改。

java 复制代码
public String deletePersonSql() {
  return new SQL() {{
    DELETE_FROM("PERSON");
    WHERE("ID = #{id}");
  }}.toString();
}
// 所以selectById实际上构建的语句如下
public String selectById() {
  return new SQL() {{
    FORM("user");
    WHERE("id = #{id}");
  }}.toString();
}

// 封装实现
public static String buildFindByPkSql(MybatisSqlBean bean, Serializable value) {
        String valStr = value.toString();
        Pk pk = bean.getPk();
        QuerySqlConditionBuilder builder = QuerySqlConditionBuilder.build();
        builder.setSqlConditions(Arrays.asList(SqlConditions.basicBuilder().value(valStr).colum(pk.getName()).build()));
        return buildSql(bean, builder);
    }

基于CommonMapper的数据操作流向:

CommonMapper -> BaseSelectProvider -> ConCurrentMapperAopFactory -> SqlBuilder

动态数据源

找到org.springframework.jdbc.datasource.lookup 包下的AbstractRoutingDataSource抽象类,根据名称可以大致得知它的作用是路由数据源的。

部分源码如下所示,需要定义自定义的数据源类提供成员变量值和重写determineCurrentLookupKey抽象方法

java 复制代码
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    @Nullable
    private Map<Object, Object> targetDataSources;
    @Nullable
    private Object defaultTargetDataSource;
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;
    @Nullable
    private DataSource resolvedDefaultDataSource;

    public AbstractRoutingDataSource() {
    }
    
    @Nullable
    protected abstract Object determineCurrentLookupKey();

新增DynamicDataSourceHolder类,存放当前线程操作数据源的key。

java 复制代码
public class DynamicDataSourceHolder {

    private static final ThreadLocal<String> DYNAMIC_DATASOURCE_KEY = new ThreadLocal<>();

    public static void setDynamicDataSourceKey(String key){
        DYNAMIC_DATASOURCE_KEY.set(key);
    }

    public static String getDynamicDataSourceKey(){
        String key = DYNAMIC_DATASOURCE_KEY.get();
        return key == null ? "default" : key;
    }

编写动态数据源类,继承AbstractRoutingDataSource,重写获取determineCurrentLookupKey方法,标识获取数据源的方式。

java 复制代码
@Getter
@Setter
public class DynamicDataSource extends AbstractRoutingDataSource {

    private Map<Object, Object> defineTargetDataSources;

    /**
     * 重写AbstractRoutingDataSource的determineCurrentLookupKey,定义选中当前数据源key的方法
     * @return DataSource's key
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceHolder.getDynamicDataSourceKey();
    }

}

现在需要的工作是在配置文件中获取数据源列表配置内容,遍历列表将每个数据源作为bean注册到Spring上下文中,后续按需获取。

刚开始的思路是在项目启动时加载application.yml中的内容,作为配置类,但在启动时读取的内容为空,没做到动态注册DataSource bean,后续的做法是加入snakeyaml依赖,读取名为data-source.yml的自定义配置文件,把内容set进入bean中,加载到上下文中,后续通过name属性获取bean。

yml 复制代码
dataSourceConfigs:
  - name: mysql-db01
    url: 
    username: root
    password: 
    driverClassName: com.mysql.cj.jdbc.Driver
    # 标识默认数据源
    primary: true
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimumIdle: 0
      maximumPoolSize: 20
      idleTimeout: 10000
      connectionTestQuery: select 1
      poolName: hikari-01

编写MultiplyDataSourceSupport,实现BeanDefinitionRegistryPostProcessor接口来完成bean注册到Spring容器的作用。

java 复制代码
@Configuration
public class MultiplyDataSourceSupport implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        MultiplyDataSourceConfig multiplyDataSourceConfig = loadDataSourceConfigs();
        if (multiplyDataSourceConfig == null || multiplyDataSourceConfig.getDataSourceConfigs() == null) {
            throw new DynamicInitException("Dont found any dynamic data source config info.");
        }
        // 遍历配置类
        for (DataSourceConfig properties : multiplyDataSourceConfig.getDataSourceConfigs()) {
            GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
            beanDefinition.setBeanClass(DataSource.class);
            // 组装DataSource对象
            DataSource dataSource = DataSourceConfigUtils.createDataSource(properties);
            beanDefinition.setInstanceSupplier(() -> dataSource);
            registry.registerBeanDefinition(properties.getName(), beanDefinition);
            if (properties.isPrimary()) {
                registry.registerBeanDefinition(DataSourceConstant.DEFAULT_DB, beanDefinition);
            }

        }
    }

    public MultiplyDataSourceConfig loadDataSourceConfigs() {
        // 加载yml内容,省略
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // do nothing
    }
}

最后一步编写动态数据源加载时的配置类,把以上已经装入Spring容器的DataSource列表bean全部装载到DynamicDataSource继承的成员变量中。

java 复制代码
@Configuration
@RequiredArgsConstructor
public class MultiplyDataSourceContextConfiguration {
    @Autowired
    private BeanFactory beanFactory;

    @Bean
    @Primary
    public DynamicDataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        MultiplyDataSourceConfig multiplyDataSourceConfig = yamlDataSourceInfoProvider().getMultiplyDataSourceConfig();
        dynamicDataSource.setDefaultTargetDataSource(beanFactory.getBean(DataSourceConstant.DEFAULT_DB));
        if (multiplyDataSourceConfig == null) {
            return dynamicDataSource;
        }
        Map<Object, Object> defineTargetDataSources = new HashMap<>(16);
        List<DataSourceConfig> dataSourceConfigs = multiplyDataSourceConfig.getDataSourceConfigs();
        for (DataSourceConfig config : dataSourceConfigs) {
            Object bean = beanFactory.getBean(config.getName());
            defineTargetDataSources.put(config.getName(), bean);
        }
        dynamicDataSource.setDefaultTargetDataSource(beanFactory.getBean(DataSourceConstant.DEFAULT_DB));
        dynamicDataSource.setTargetDataSources(defineTargetDataSources);
        dynamicDataSource.setDefineTargetDataSources(defineTargetDataSources);
        return dynamicDataSource;
    }
}

目前配置完可以通过DynamicDataSourceHolder来切换数据源。

java 复制代码
public void test(){
    // 方法1
   doSomethingOnDb01(); DynamicDataSourceHolder.setDynamicDataSourceKey(name);
    // 方法2
    doSomethingOnDb02(); 
}

同样可以通过定义注解,在方法执行前通过aop切面方式,切换数据源。

java 复制代码
@Aspect
@Component
public class ChangeDataSourceInterceptor {
 @Around("@annotation(org.nott.datasource.annotations.DataSource)")
    public Object around(ProceedingJoinPoint point) {

        MethodSignature signature = (MethodSignature) point.getSignature();

        Method method = signature.getMethod();

        String value = DataSourceConstant.DEFAULT_DB;

        boolean isCustomDataSource = method.isAnnotationPresent(DataSource.class);

        if (isCustomDataSource) {
            DataSource annotation = method.getAnnotation(DataSource.class);
            value = annotation.value();
        }
        DynamicDataSourceHolder.setDynamicDataSourceKey(value);

        Object result;
        try {
            result = point.proceed();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        return result;
    }
}

测试

将以下两条MySql语句分别插入到test和test01数据库中。

mysql 复制代码
INSERT INTO `test`.`user` (`id`, `age`, `email`, `name`, `password`) VALUES ('410544b2-4001-4271-9855-fec4b62350b', 15, 'power@win.com', 'mybatis-test', '$10$xupd3MaYf1fA2wMjaNl6AuTyBE2ifOGd2xbUVpdV1fgqYyBi9XiRy');

INSERT INTO `test01`.`user` (`id`, `age`, `email`, `name`, `password`) VALUES ('410544b2-4001-4271-9855-fec4b62350b', 15, 'test01@win.com', 'mybatis-test01', '$10$xupd3MaYf1fA2wMjaNl6AuTyBE2ifOGd2xbUVpdV1fgqYyBi9XiRy');
java 复制代码
// 本来用junit测试方法,不起作用后换成REST API测试
@RequestMapping("/test")
public void test() {
    User one = userMapper.selectOneByCondition(QuerySqlConditionBuilder.build().eq("id", "410544b2-4001-4271-9855-fec4b62350b"));
        DynamicDataSourceHolder.setDynamicDataSourceKey("mysql-db02");
        User two = userMapper.selectOneByCondition(QuerySqlConditionBuilder.build().eq("id", "410544b2-4001-4271-9855-fec4b62350b"));
        Assert.isTrue("mybatis-test".equals(one.getName()),"");
        Assert.isTrue("mybatis-test01".equals(two.getName()),"");
}

程序中的断言不抛出异常,且日志输出类似于c.z.h.HikariDataSource -hikari-01/02的语句,视为成功。

小结

这一系列的几篇文章篇幅有意压缩后还是很长,受限于篇幅,中间有些概念点没法深挖,造成读者观感不好,所以是CommonMapper话题的最后一篇,后续再挖掘其他有意思的干货学习分享。

相关推荐
小徐敲java1 小时前
通用mybatis-plus查询封装(QueryGenerator)
mybatis
姜学迁1 小时前
Rust-枚举
开发语言·后端·rust
北极小狐1 小时前
Java vs JavaScript:类型系统的艺术 - 从 Object 到 any,从静态到动态
后端
【D'accumulation】2 小时前
令牌主动失效机制范例(利用redis)注释分析
java·spring boot·redis·后端
2401_854391082 小时前
高效开发:SpringBoot网上租赁系统实现细节
java·spring boot·后端
Cikiss2 小时前
微服务实战——SpringCache 整合 Redis
java·redis·后端·微服务
wxin_VXbishe2 小时前
springboot合肥师范学院实习实训管理系统-计算机毕业设计源码31290
java·spring boot·python·spring·servlet·django·php
Cikiss2 小时前
微服务实战——平台属性
java·数据库·后端·微服务
OEC小胖胖2 小时前
Spring Boot + MyBatis 项目中常用注解详解(万字长篇解读)
java·spring boot·后端·spring·mybatis·web
2401_857617623 小时前
SpringBoot校园资料平台:开发与部署指南
java·spring boot·后端