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

网友投稿 315 2022-10-28


基于注解的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 primarySqlSessionTemplahttp://te(@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 DB_KEY_CONTEXT = new ThreadLocal<>();

//在app启动时就加载全部数据源,不需要考虑并发

private static Set 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 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 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 params;

private Map 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 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接口的时候,才会走反射调用的逻辑去获取对应的数据源,后续,都会走本地缓存,提升了性能。


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:企业VPN组网IP地址冲突解决方案
下一篇:瞻博-Juniper-SSG系列之PBR(策略路由)配置终结篇
相关文章

 发表评论

暂时没有评论,来抢沙发吧~