基于注解的springboot+mybatis的多数据源组件的实现代码

 更新时间:2021年04月16日 08:36:41   作者:jy的blog  
这篇文章主要介绍了基于注解的springboot+mybatis的多数据源组件的实现,会使用到多个数据源,文中通过代码讲解的非常详细,需要的朋友可以参考下

通常业务开发中,我们会使用到多个数据源,比如,部分数据存在mysql实例中,部分数据是在oracle数据库中,那这时候,项目基于springboot和mybatis,其实只需要配置两个数据源即可,只需要按照

dataSource -SqlSessionFactory - SqlSessionTemplate配置好就可以了。

如下代码,首先我们配置一个主数据源,通过@Primary注解标识为一个默认数据源,通过配置文件中的spring.datasource作为数据源配置,生成SqlSessionFactoryBean,最终,配置一个SqlSessionTemplate。

@Configuration
@MapperScan(basePackages = "com.xxx.mysql.mapper", sqlSessionFactoryRef = "primarySqlSessionFactory")
public class PrimaryDataSourceConfig {

    @Bean(name = "primaryDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druid() {
        return new DruidDataSource();
    }

    @Bean(name = "primarySqlSessionFactory")
    @Primary
    public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        bean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
        return bean.getObject();
    }

    @Bean("primarySqlSessionTemplate")
    @Primary
    public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("primarySqlSessionFactory") SqlSessionFactory sessionFactory) {
        return new SqlSessionTemplate(sessionFactory);
    }
}

然后,按照相同的流程配置一个基于oracle的数据源,通过注解配置basePackages扫描对应的包,实现特定的包下的mapper接口,使用特定的数据源。

@Configuration
@MapperScan(basePackages = "com.nbclass.oracle.mapper", sqlSessionFactoryRef = "oracleSqlSessionFactory")
public class OracleDataSourceConfig {

    @Bean(name = "oracleDataSource")
    @ConfigurationProperties(prefix = "spring.secondary")
    public DataSource oracleDruid(){
        return new DruidDataSource();
    }

    @Bean(name = "oracleSqlSessionFactory")
    public SqlSessionFactory oracleSqlSessionFactory(@Qualifier("oracleDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:oracle/mapper/*.xml"));
        return bean.getObject();
    }

    @Bean("oracleSqlSessionTemplate")
    public SqlSessionTemplate oracleSqlSessionTemplate(@Qualifier("oracleSqlSessionFactory") SqlSessionFactory sessionFactory) {
        return new SqlSessionTemplate(sessionFactory);
    }
}

这样,就实现了一个工程下使用多个数据源的功能,对于这种实现方式,其实也足够简单了,但是如果我们的数据库实例有很多,并且每个实例都主从配置,那这里维护起来难免会导致包名过多,不够灵活。

现在考虑实现一种对业务侵入足够小,并且能够在mapper方法粒度上去支持指定数据源的方案,那自然而然想到了可以通过注解来实现,首先,自定义一个注解@DBKey:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DBKey {

    String DEFAULT = "default"; // 默认数据库节点

    String value() default DEFAULT;
}

思路和上面基于springboot原生的配置的类似,首先定义一个默认的数据库节点,当mapper接口方法/类没有指定任何注解的时候,默认走这个节点,注解支持传入value参数表示选择的数据源节点名称。至于注解的实现逻辑,可以通过反射来获取mapper接口方法/类的注解值,然后指定特定的数据源。

那在什么时候执行这个操作获取呢?可以考虑使用spring AOP织入mapper层,在切入点执行具体mapper方法之前,将对应的数据源配置放入threaLocal中,有了这个逻辑,立即动手实现:

首先,定义一个db配置的上下文对象。维护所有的数据源key实例,以及当前线程使用的数据源key:

public class DBContextHolder {

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

    //在app启动时就加载全部数据源,不需要考虑并发
    private static Set<String> allDBKeys = new HashSet<>();

    public static String getDBKey() {
        return DB_KEY_CONTEXT.get();
    }

    public static void setDBKey(String dbKey) {
        //key必须在配置中
        if (containKey(dbKey)) {
            DB_KEY_CONTEXT.set(dbKey);
        } else {
            throw new KeyNotFoundException("datasource[" + dbKey + "] not found!");
        }
    }

    public static void addDBKey(String dbKey) {
        allDBKeys.add(dbKey);
    }

    public static boolean containKey(String dbKey) {
        return allDBKeys.contains(dbKey);
    }

    public static void clear() {
        DB_KEY_CONTEXT.remove();
    }
}

然后,定义切点,在切点before方法中,根据当前mapper接口的@@DBKey注解来选取对应的数据源key:

@Aspect
@Order(Ordered.LOWEST_PRECEDENCE - 1)
public class DSAdvice implements BeforeAdvice {

    @Pointcut("execution(* com.xxx..*.repository.*.*(..))")
    public void daoMethod() {
    }

    @Before("daoMethod()")
    public void beforeDao(JoinPoint point) {
        try {
            innerBefore(point, false);
        } catch (Exception e) {
            logger.error("DefaultDSAdviceException",
                    "Failed to set database key,please resolve it as soon as possible!", e);
        }
    }

    /**
     * @param isClass 拦截类还是接口
     */
    public void innerBefore(JoinPoint point, boolean isClass) {
        String methodName = point.getSignature().getName();

        Class<?> clazz = getClass(point, isClass);
        //使用默认数据源
        String dbKey = DBKey.DEFAULT;
        Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
        Method method = null;
        try {
            method = clazz.getMethod(methodName, parameterTypes);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("can't find " + methodName + " in " + clazz.toString());
        }
        //方法上存在注解,使用方法定义的datasource
        if (method.isAnnotationPresent(DBKey.class)) {
            DBKey key = method.getAnnotation(DBKey.class);
            dbKey = key.value();
        } else {
            //方法上不存在注解,使用类上定义的注解
            clazz = method.getDeclaringClass();
            if (clazz.isAnnotationPresent(DBKey.class)) {
                DBKey key = clazz.getAnnotation(DBKey.class);
                dbKey = key.value();
            }
        }
        DBContextHolder.setDBKey(dbKey);
    }


    private Class<?> getClass(JoinPoint point, boolean isClass) {
        Object target = point.getTarget();
        String methodName = point.getSignature().getName();

        Class<?> clazz = target.getClass();
        if (!isClass) {
            Class<?>[] clazzList = target.getClass().getInterfaces();

            if (clazzList == null || clazzList.length == 0) {
                throw new MutiDBException("找不到mapper class,methodName =" + methodName);
            }
            clazz = clazzList[0];
        }

        return clazz;
    }
}

既然在执行mapper之前,该mapper接口最终使用的数据源已经被放入threadLocal中,那么,只需要重写新的路由数据源接口逻辑即可:

public class RoutingDatasource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        String dbKey = DBContextHolder.getDBKey();
        return dbKey;
    }

    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        for (Object key : targetDataSources.keySet()) {
            DBContextHolder.addDBKey(String.valueOf(key));
        }
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
}

另外,我们在服务启动,配置mybatis的时候,将所有的db配置加载:

@Bean
    @ConditionalOnMissingBean(DataSource.class)
    @Autowired
    public DataSource dataSource(MybatisProperties mybatisProperties) {
        Map<Object, Object> dsMap = new HashMap<>(mybatisProperties.getNodes().size());
        for (String nodeName : mybatisProperties.getNodes().keySet()) {
            dsMap.put(nodeName, buildDataSource(nodeName, mybatisProperties));
            DBContextHolder.addDBKey(nodeName);
        }
        RoutingDatasource dataSource = new RoutingDatasource();
        dataSource.setTargetDataSources(dsMap);
        if (null == dsMap.get(DBKey.DEFAULT)) {
            throw new RuntimeException(
                    String.format("Default DataSource [%s] not exists", DBKey.DEFAULT));
        }
        dataSource.setDefaultTargetDataSource(dsMap.get(DBKey.DEFAULT));
        return dataSource;
    }



@ConfigurationProperties(prefix = "mybatis")
@Data
public class MybatisProperties {

    private Map<String, String> params;

    private Map<String, Object> nodes;

    /**
     * mapper文件路径:多个location以,分隔
     */
    private String mapperLocations = "classpath*:com/iqiyi/xiu/**/mapper/*.xml";

    /**
     * Mapper类所在的base package
     */
    private String basePackage = "com.iqiyi.xiu.**.repository";

    /**
     * mybatis配置文件路径
     */
    private String configLocation = "classpath:mybatis-config.xml";
}

那threadLocal中的key什么时候进行销毁呢,其实可以自定义一个基于mybatis的拦截器,在拦截器中主动调DBContextHolder.clear()方法销毁这个key。具体代码就不贴了。这样一来,我们就完成了一个基于注解的支持多数据源切换的中间件。

那有没有可以优化的点呢?其实,可以发现,在获取mapper接口/所在类的注解的时候,使用了反射来获取的,那我们知道一般反射调用是比较耗性能的,所以可以考虑在这里加个本地缓存来优化下性能:

private final static Map<String, String> METHOD_CACHE = new ConcurrentHashMap<>();
//....
public void innerBefore(JoinPoint point, boolean isClass) {
        String methodName = point.getSignature().getName();

        Class<?> clazz = getClass(point, isClass);
        //key为类名+方法名
        String keyString = clazz.toString() + methodName;
        //使用默认数据源
        String dbKey = DBKey.DEFAULT;
        //如果缓存中已经有这个mapper方法对应的数据源的key,那直接设置
        if (METHOD_CACHE.containsKey(keyString)) {
            dbKey = METHOD_CACHE.get(keyString);
        } else {
            Class<?>[] parameterTypes =
                    ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
            Method method = null;

            try {
                method = clazz.getMethod(methodName, parameterTypes);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException("can't find " + methodName + " in " + clazz.toString());
            }
             //方法上存在注解,使用方法定义的datasource
            if (method.isAnnotationPresent(DBKey.class)) {
                DBKey key = method.getAnnotation(DBKey.class);
                dbKey = key.value();
            } else {
                clazz = method.getDeclaringClass();
                //使用类上定义的注解
                if (clazz.isAnnotationPresent(DBKey.class)) {
                    DBKey key = clazz.getAnnotation(DBKey.class);
                    dbKey = key.value();
                }
            }
           //先放本地缓存
            METHOD_CACHE.put(keyString, dbKey);
        }
        DBContextHolder.setDBKey(dbKey);
    }

这样一来,只有在第一次调用这个mapper接口的时候,才会走反射调用的逻辑去获取对应的数据源,后续,都会走本地缓存,提升了性能。

到此这篇关于基于注解的springboot+mybatis的多数据源组件的实现代码的文章就介绍到这了,更多相关springboot mybatis多数据源组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java接口默认方法带来的问题分析【二义性问题】

    Java接口默认方法带来的问题分析【二义性问题】

    这篇文章主要介绍了Java接口默认方法带来的问题,结合实例形式分析了java接口带来的二义性问题,需要的朋友可以参考下
    2019-08-08
  • PowerJob的MapProcessor工作流程源码解读

    PowerJob的MapProcessor工作流程源码解读

    这篇文章主要为大家介绍了PowerJob的MapProcessor工作流程源码解读,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2024-01-01
  • Spring boot 集成 MQTT详情

    Spring boot 集成 MQTT详情

    这篇文章主要介绍了Spring boot 集成 MQTT详情,MQTT是一种基于发布/订阅模式的"轻量级"通讯协议,可以以极少的代码和有限的带宽为连接远程设备提供实时可靠的消息服,下文更多相关介绍,需要的小伙伴可以参考一下
    2022-04-04
  • Netty搭建WebSocket服务器实战教程

    Netty搭建WebSocket服务器实战教程

    这篇文章主要介绍了Netty搭建WebSocket服务器实战,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2024-03-03
  • 在Spring Boot中从类路径加载文件的示例

    在Spring Boot中从类路径加载文件的示例

    创建Spring Boot Web应用程序时,有时有时需要从类路径中加载文件;war和jar的加载文件格式是不一样的,在下面,您将找到在WAR和JAR中加载文件的解决方案。
    2020-10-10
  • 使用java的Calendar对象获得当前日期

    使用java的Calendar对象获得当前日期

    本文给大家分享的是使用使用java的Calendar对象获得当前日期的上几个度开始、结束时间,主要思路是先获得当前季度的开始和结束日期,在当前日期的基础上往前推3个月即上个季度的开始和结束日期,十分的实用,小伙伴们可以参考下。
    2015-07-07
  • MyBatis 多表联合查询及优化方法

    MyBatis 多表联合查询及优化方法

    大家都知道Hibernate 是全自动的数据库持久层框架,它可以通过实体来映射数据库,通过设置一对多、多对一、一对一、多对多的关联来实现联合查询,接下来通过本文给大家介绍MyBatis 多表联合查询及优化,需要的朋友可以参考下
    2022-08-08
  • Springboot集成阿里云OSS上传文件系统教程

    Springboot集成阿里云OSS上传文件系统教程

    这篇文章主要介绍了Springboot集成阿里云OSS上传文件系统教程,通过详细的图文展示,代码步骤的展示和文件配置信息,希望对你有所帮助
    2021-06-06
  • Java同步函数代码详解

    Java同步函数代码详解

    这篇文章主要介绍了Java线程中的同步函数的相关内容,涉及了实例代码,需要的朋友,可以参考下。
    2017-10-10
  • spring-boot 如何实现单次执行程序

    spring-boot 如何实现单次执行程序

    这篇文章主要介绍了spring-boot 实现单次执行程序方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09

最新评论