• MyBatis整合双数据源


    有时候在项目中会遇到需要连接两个数据库的情况。本文就结合Spring和Mybatis来讲下怎么使用双数据源(或者是多数据源)。

    背景知识介绍

    本文中实现多数据源的关键是Spring提供的AbstractRoutingDataSource。这个类可以根据lookup key来实现底层数据源的动态转换。

    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 void setTargetDataSources(Map<Object, Object> targetDataSources) {
    		this.targetDataSources = targetDataSources;
    	}
    
    	public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
    		this.defaultTargetDataSource = defaultTargetDataSource;
    	}
    
    	public void setLenientFallback(boolean lenientFallback) {
    		this.lenientFallback = lenientFallback;
    	}
    
    	public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) {
    		this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
    	}
    
    
    	@Override
    	public void afterPropertiesSet() {
    		if (this.targetDataSources == null) {
    			throw new IllegalArgumentException("Property 'targetDataSources' is required");
    		}
    		this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
    		this.targetDataSources.forEach((key, value) -> {
    			Object lookupKey = resolveSpecifiedLookupKey(key);
    			DataSource dataSource = resolveSpecifiedDataSource(value);
    			this.resolvedDataSources.put(lookupKey, dataSource);
    		});
    		if (this.defaultTargetDataSource != null) {
    			this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
    		}
    	}
    
    	protected Object resolveSpecifiedLookupKey(Object lookupKey) {
    		return lookupKey;
    	}
    
    	protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
    		if (dataSource instanceof DataSource) {
    			return (DataSource) dataSource;
    		}
    		else if (dataSource instanceof String) {
    			return this.dataSourceLookup.getDataSource((String) dataSource);
    		}
    		else {
    			throw new IllegalArgumentException(
    					"Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
    		}
    	}
    
    
    	@Override
    	public Connection getConnection() throws SQLException {
    		return determineTargetDataSource().getConnection();
    	}
    
    	@Override
    	public Connection getConnection(String username, String password) throws SQLException {
    		return determineTargetDataSource().getConnection(username, password);
    	}
    
    	@Override
    	@SuppressWarnings("unchecked")
    	public <T> T unwrap(Class<T> iface) throws SQLException {
    		if (iface.isInstance(this)) {
    			return (T) this;
    		}
    		return determineTargetDataSource().unwrap(iface);
    	}
    
    	@Override
    	public boolean isWrapperFor(Class<?> iface) throws SQLException {
    		return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface));
    	}
        
    	protected DataSource determineTargetDataSource() {
    		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    		Object lookupKey = determineCurrentLookupKey();
    		DataSource dataSource = this.resolvedDataSources.get(lookupKey);
    		if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
    			dataSource = this.resolvedDefaultDataSource;
    		}
    		if (dataSource == null) {
    			throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    		}
    		return dataSource;
    	}
    
    	@Nullable
        //一般只需要用户实现这个方法。
    	protected abstract Object determineCurrentLookupKey();
    
    }
    

    实现流程

    step1:实现一个自定义的AbstractRoutingDataSource

    public class DynamicDataSource extends AbstractRoutingDataSource {
    
        //这边定义了一个和线程绑定的ThreadLocal变量,用于存放需要使用的数据源的名称
        private static final ThreadLocal<String> dataSourceNameHolder = new ThreadLocal<>();
    
        public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
            super.setDefaultTargetDataSource(defaultTargetDataSource);
            super.setTargetDataSources(targetDataSources);
            super.afterPropertiesSet();
        }
    
        @Override
        //重写了AbstractRoutingDataSource的determineCurrentLookupKey方法
        protected Object determineCurrentLookupKey() {
            return getDataSource();
        }
    
        public static void setDataSource(String dataSource) {
            dataSourceNameHolder.set(dataSource);
        }
    
        public static String getDataSource() {
            return dataSourceNameHolder.get();
        }
    
        public static void clearDataSource() {
            dataSourceNameHolder.remove();
        }
    
    }
    

    step2:实现一个AOP对Service层方法进行AOP拦截,调用DynamicDataSource中的ThreadLocal变量,将当前请求需要使用的数据源名称设置进去。

    //定义一个DataSource注解
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface DataSource {
        String name() default "";
    }
    //这边再定义一个常量
    public interface DataSourceNames {
        String FIRST = "first";
        String SECOND = "second";
    
    }
    

    定义AOP处理DataSource注解

    @Aspect
    @Component
    public class DataSourceAspect implements Ordered {
        protected Logger logger = LoggerFactory.getLogger(getClass());
    
        @Pointcut("@annotation(com.xx.yy.annotation.DataSource)")
        public void dataSourcePointCut() {
    
        }
    
        @Around("dataSourcePointCut()")
        public Object around(ProceedingJoinPoint point) throws Throwable {
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            
            DataSource ds = method.getAnnotation(DataSource.class);
            //如果未指定数据源就使用第一个数据源
            if(ds == null){
                DynamicDataSource.setDataSource(DataSourceNames.FIRST);
                logger.debug("set datasource is " + DataSourceNames.FIRST);
            }else {
                DynamicDataSource.setDataSource(ds.name());
                logger.debug("set datasource is " + ds.name());
            }
            try {
                return point.proceed();
            } finally {
                DynamicDataSource.clearDataSource();
                logger.debug("clean datasource");
            }
        }
        @Override
        public int getOrder() {
            return 1;
        }
    }
    

    step3:对数据源进行配置

    @Configuration
    public class DynamicDataSourceConfig {
    
        @Bean
        @ConfigurationProperties("spring.datasource.druid.first")
        public DataSource firstDataSource(){
            return DruidDataSourceBuilder.create().build();
        }
    
        @Bean
        @ConfigurationProperties("spring.datasource.druid.second")
        public DataSource secondDataSource(){
            DataSource dataSource = DruidDataSourceBuilder.create().build();
            return dataSource;
        }
    
        @Bean
        @Primary
        @DependsOn(value = {"firstDataSource","secondDataSource"})
        public DynamicDataSource dataSource(DataSource firstDataSource,DataSource secondDataSource) {
            Map<Object, Object> targetDataSources = new HashMap<>();
            targetDataSources.put(DataSourceNames.FIRST, firstDataSource);
            targetDataSources.put(DataSourceNames.SECOND,  secondDataSource);
            return new DynamicDataSource(firstDataSource, targetDataSources);
        }
    }
    

    以上就是实现双数据源的全部配置。

    使用

    使用的时候非常简单,只需要在Service层的方法上加上@DataSource注解就可以了。

    @DataSource(name = DataSourceNames.SECOND)
    public String selectByInfoName(String name){
       //...
    }
    

    一些注意点

    如果你使用了pageHelper等分页插件,请将方言设置成自动模式, autoRuntimeDialect: true

    pagehelper:
      reasonable: false
      supportMethodsArguments: true
      params: count=countSql
      autoRuntimeDialect: true
    

    如果你使用了Druid数据源,并通过下面的形式创建数据源,要保障数据源的用户名和密码字段不为null。不然DruidDataSourceWrapper这个Bean会检测这个字段的值,导致启动失败。

    
    @Bean
        @ConfigurationProperties("spring.datasource.druid.first")
        public DataSource firstDataSource(){
            return DruidDataSourceBuilder.create().build();
        }
    
        @Bean
        @ConfigurationProperties("spring.datasource.druid.second")
        public DataSource secondDataSource(){
            DataSource dataSource = DruidDataSourceBuilder.create().build();
            return dataSource;
        }
    
    
  • 相关阅读:
    [背包问题][二进制优化] Jzoj P4224 食物
    [并查集][排序] Jzoj P4223 旅游
    [哈夫曼树][优先队列] Bzoj P4198 荷马史诗
    [hash][差分][虚树] Jzoj P6011 天天爱跑步
    [dp] Jzoj P6012 荷马史诗
    [dp][递归] Jzoj P4211 送你一棵圣诞树
    [数学] Jzoj P3912 超氧化钾
    堆学习笔记(未完待续)(洛谷p1090合并果子)
    [AC自动机]luogu P2444 病毒
    [概率期望][DP]luogu P3830 随机树
  • 原文地址:https://www.cnblogs.com/54chensongxia/p/11850841.html
Copyright © 2020-2023  润新知