springboot 多数据源(aop方式)

一、实现思路
在yml中定义多个数据源的配置,然后创建一个类DynamicDataSource去继承AbstractRoutingDataSource类

AbstractRoutingDataSource类中

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;
}

该方法用来得到当前数据源:
1:调用了determineCurrentLookupKey()方法来得到lookupKey(重写该方法决定我们的lookupKey的从哪里获取)
2:再通过lookupKey从resolvedDataSources得到DataSource,如果获取不到则取默认数据源

@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);
        }
    }

3:resolvedDataSources其实是通过该类的targetDataSources得到

实现思路:
1、在yml中配置多个数据源的参数

spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
            driver-class-name: com.mysql.jdbc.Driver
            url: jdbc:mysql://192.168.1.131/springbootDemo?characterEncoding=utf-8&useSSL=false  
            username: root
            password: 123456
            initial-size: 10
            max-active: 100
            min-idle: 10
            max-wait: 60000
            pool-prepared-statements: true
            max-pool-prepared-statement-per-connection-size: 20
            time-between-eviction-runs-millis: 60000
            min-evictable-idle-time-millis: 300000
            #Oracle需要打开注释
            #validation-query: SELECT 1 FROM DUAL
            test-while-idle: true
            test-on-borrow: false
            test-on-return: false
            stat-view-servlet:
                enabled: true
                url-pattern: /druid/*
                #login-username: admin
                #login-password: admin
            filter:
                stat:
                    log-slow-sql: true
                    slow-sql-millis: 1000
                    merge-sql: false
                wall:
                    config:
                        multi-statement-allow: true

#多数据源的配置
dynamic:
  datasource:
    slave1:
      url: jdbc:mysql://192.168.1.131/springbootDemo?characterEncoding=utf-8&useSSL=false  
      username: root
      password: 123456
    slave2:
      url: jdbc:mysql://192.168.1.131/spring_data_jpa?characterEncoding=utf-8&useSSL=false  
      username: root
      password: 123456

2、将yml中的多个配置加载进map,name做key, value就是配置属性对象

/**
 * 将yml中的配置加载进DataSourceProperties并存入map
 *
 */
package com.ganlong.dataSource.properties;

import lombok.Data;

@Data
public class DataSourceProperties {
    private String driverClassName;
    private String url;
    private String username;
    private String password;

    /**
     * Druid默认参数
     */
    private int initialSize = 2;
    private int maxActive = 10;
    private int minIdle = -1;
    private long maxWait = 60 * 1000L;
    private long timeBetweenEvictionRunsMillis = 60 * 1000L;
    private long minEvictableIdleTimeMillis = 1000L * 60L * 30L;
    private long maxEvictableIdleTimeMillis = 1000L * 60L * 60L * 7;
    private String validationQuery = "select 1";
    private int validationQueryTimeout = -1;
    private boolean testOnBorrow = false;
    private boolean testOnReturn = false;
    private boolean testWhileIdle = true;
    private boolean poolPreparedStatements = false;
    private int maxOpenPreparedStatements = -1;
    private boolean sharePreparedStatements = false;
    private String filters = "stat,wall";
}

3、用加载好的propertiesMap创建出多个DruidDataSource ,并setTargetDataSources


@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {

    @Autowired
    private DynamicDataSourceProperties dynamicDataSourceProperties;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid")
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 设置默认数据源
        DruidDataSource defaultDataSource=DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
        dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
        
        // 设置多数据源
        dynamicDataSource.setTargetDataSources(targetDataSources());
        return dynamicDataSource;
    }

    @Bean
    public Map<Object,Object> targetDataSources(){
        // 循环设置targetDataSources
        HashMap<String, DataSourceProperties>dataSourcePropertiesMap = dynamicDataSourceProperties.getDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>(dataSourcePropertiesMap.size());
        dataSourcePropertiesMap.forEach((k, v) -> {
            DruidDataSource druidDataSource = DynamicDataSourceFactory.buildDruidDataSource(v);
            targetDataSources.put(k, druidDataSource);
        });
        return targetDataSources;
    }
}

package com.ganlong.dataSource.config;


import java.sql.SQLException;

import com.alibaba.druid.pool.DruidDataSource;
import com.ganlong.dataSource.properties.DataSourceProperties;

/**
 *   根据properties创建DruidDataSource
 * @author yl
 *
 */
public class DynamicDataSourceFactory {

    public static DruidDataSource buildDruidDataSource(DataSourceProperties properties) {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName(properties.getDriverClassName());
        druidDataSource.setUrl(properties.getUrl());
        druidDataSource.setUsername(properties.getUsername());
        druidDataSource.setPassword(properties.getPassword());

        druidDataSource.setInitialSize(properties.getInitialSize());
        druidDataSource.setMaxActive(properties.getMaxActive());
        druidDataSource.setMinIdle(properties.getMinIdle());
        druidDataSource.setMaxWait(properties.getMaxWait());
        druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
        druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
        druidDataSource.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis());
        druidDataSource.setValidationQuery(properties.getValidationQuery());
        druidDataSource.setValidationQueryTimeout(properties.getValidationQueryTimeout());
        druidDataSource.setTestOnBorrow(properties.isTestOnBorrow());
        druidDataSource.setTestOnReturn(properties.isTestOnReturn());
        druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements());
        druidDataSource.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements());
        druidDataSource.setSharePreparedStatements(properties.isSharePreparedStatements());

        try {
            druidDataSource.setFilters(properties.getFilters());
            druidDataSource.init();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return druidDataSource;
    }
}

4、用aop和注解去设置lookupKey

@Component
public class ThreadlocalHolder {
    
    public static final ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    
    public void set(String dataSource) {
        threadLocal.set(dataSource);
    }
    
    public String get() {
        return threadLocal.get();
    }
    
    public void removeAll() {
        threadLocal.remove();
    }
}
package com.ganlong.dataSource.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource{
    
    @Autowired
    private ThreadlocalHolder threadlocalHolder;
    
    @Override
    protected Object determineCurrentLookupKey() {
        return threadlocalHolder.get();
    }
}

LookupKey用ThreadLocal来存取。(ThreadLocal主要是避免线程安全问题,如果多个方法之间需要同一个参数,我们声明成 成员变量的话会存在线程安全问题,声明成局部变量然后通过形参去传递,又过于麻烦,这时就可以考虑用ThreadLocal来传递)

package com.ganlong.dataSource.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
    String value() default "";
}

package com.ganlong.aspect;

import java.lang.reflect.Method;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import com.ganlong.dataSource.annotation.DataSource;
import com.ganlong.dataSource.config.ThreadlocalHolder;

@Component
@Aspect
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceAspect {
    
    @Autowired
    private ThreadlocalHolder threadLocal;
    
    @Pointcut("@annotation(com.ganlong.dataSource.annotation.DataSource) || @within(com.ganlong.dataSource.annotation.DataSource)")
    public void dataSourcePointCut() {

    }

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 得到方法上的注解
        MethodSignature signature =(MethodSignature)point.getSignature();
        Method method =signature.getMethod();
        DataSource methodDataSource = method.getAnnotation(DataSource.class);

        // 得到类上的注解
        Class targetClass = point.getTarget().getClass();
        DataSource targetClassDataSource = (DataSource) targetClass.getAnnotation(DataSource.class);

        if(methodDataSource!=null || targetClassDataSource!=null) {
            String dataSourceName="";
            if(methodDataSource!=null) {
                dataSourceName = methodDataSource.value();
            }else {
                dataSourceName = targetClassDataSource.value();
            }
            //存入threadlocal
            threadLocal.set(dataSourceName);
        }
        try {
            return point.proceed();
        }finally {
            // 最后清空当前线程的数据源
            threadLocal.removeAll();
        }
    }
}

切面类记得加上@Order(Ordered.HIGHEST_PRECEDENCE),让该切面在最优先级这样才会起作用。

还有如果开启了事务那么就不能再切换数据源了(切换也会失效),一个事务只能对应一个数据源(最早的那个数据源)



posted @ 2021-10-10 13:59  牧之丨  阅读(229)  评论(0编辑  收藏  举报