Spring Boot + Mybatis + Druid 动态切换多数据源

在大型应用程序中,配置主从数据库并使用读写分离是常见的设计模式。

在Spring应用程序中,要实现读写分离,最好不要对现有代码进行改动,而是在底层透明地支持。

这样,就需要我们再一个项目中,配置两个,乃至多个数据源。

今天,小编先来介绍一下自己配置动态多数据源的步骤

项目简介:

  编译器:IDEA

  JDK:1.8

  框架:Spring Boot 2.1.0.RELEASES  + Mybatis + Druid

一、配置数据库连接数据

因为项目使用的是Spring Boot 框架,该框架会自动配置数据源,自动从application.properties中读取数据源信息,如果没有配置,启动时会报错,因此我们再配置自定义的数据源的时候,需要禁掉数据源的自动配置。

但是小编在启动项目的时候,还是报错了,可是由于jdbcTemplate重复了,框架自动帮我们定义了一个jdbcTemplate,而小编自己又自定义了一个,因此,也要将这个自动配置禁止掉

启动类方法如下:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,JdbcTemplateAutoConfiguration.class})
@MapperScan(sqlSessionTemplateRef = "jdbcTemplate")
public class DynamicDatasourseApplication {

    public static void main(String[] args) {
        SpringApplication.run(DynamicDatasourseApplication.class, args);
    }
}

下面开始配置自定义的数据源。

新建jdbc.properties文件,配置数据库的连接,数据源1为写库,数据源2为读库

jdbc.driverClassName.db=com.mysql.jdbc.Driver
#写数据源
jdbc.w.url=jdbc:mysql://localhost:3306/learning?characterEncoding=UTF-8&&serverTimezone=UTC
jdbc.w.user=root
jdbc.w.password=123456
#读数据源
jdbc.r.url=jdbc:mysql://localhost:3306/learning?characterEncoding=UTF-8&&serverTimezone=UTC
jdbc.r.user=root
jdbc.r.password=123456
#连接池属性
druid.initialSize=2
druid.minIdle=30
druid.maxActive=80
druid.maxWait=60000
druid.timeBetweenEvictionRunsMillis=60000
druid.minEvictableIdleTimeMillis=300000
druid.validationQuery=SELECT 'x'
druid.testWhileIdle=true
druid.testOnBorrow=false
druid.testOnReturn=false
druid.poolPreparedStatements=true
druid.maxPoolPreparedStatementPerConnectionSize=20
druid.filters=wall,stat
jdbc.properties

二、配置mybatis的属性

在application.properties中配置mybatis的属性

mybatis.type-aliases-package:实体类的位置,如果将实体类放到Application.java文件的同级包或者下级包时,这个属性可以不配置
mybatis.mapper-locations:mapper.xml的位置
mybatis.config-location:mybatis配置文件的位置,无则不填
mybatis.type-aliases-package=cn.com.exercise.dynamicDatasourse.module.condition
mybatis.mapper-locations=/mappers/**.xml
mybatis.config-location=/config/sqlmap-config.xml

三、使用Java文件读取资源数据

1)配置主数据源(写库)

    @Bean(name = '写库名字')
    @Primary
    public DataSource master(){
        DruidDataSource source = new DruidDataSource();
        //使用source.setXxx(Yyy);进行配置
        //数据库基本属性driverClassName url、user、password配置
        //连接池基本属性配置
        return source;
    }    

@Primary表示优先为注入的Bean,此处用来标识住数据源

2)配置从数据源(读库),配置内容和主数据源相同

    @Bean(name = '读库名字')
    public DataSource master(){
        DruidDataSource source = new DruidDataSource();
        //使用source.setXxx(Yyy);进行配置
        //数据库基本属性driverClassName url、user、password配置
        //连接池基本属性配置
        return source;
    } 

3)数据源支持,配置默认数据源

    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource(){
        DynamicDataSource dynamicRoutingDataSource = new DynamicDataSource();
        //配置多数据源
        Map<Object, Object> dataSourceMap = new HashMap<>(2);
        dataSourceMap.put("写库名字", master());
        dataSourceMap.put("读库名字", slave());
        // 将 master 数据源作为默认指定的数据源
        dynamicRoutingDataSource.setDefaultTargetDataSource(master());
        // 将 master 和 slave 数据源作为指定的数据源
        dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
        return dynamicRoutingDataSource;
    }

    @Bean(name="dataSourceProxy")
    public TransactionAwareDataSourceProxy dataSourceProxy(){
        TransactionAwareDataSourceProxy proxy = new TransactionAwareDataSourceProxy();
        proxy.setTargetDataSource(dynamicDataSource());
        return proxy;
    }

4)配置sqlSessionFactory和jdbcTemplate

在sqlSessionFactory中,配置mybatis相关的三个内容:typeAliasesPackage,configLocation和mapperLocation,分别对应了application.properties中的三个内容,有则配置,无则省略。

    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception{
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setVfs(SpringBootVFS.class);
        sqlSessionFactoryBean.setTypeAliasesPackage(typeAlias);
        sqlSessionFactoryBean.setConfigLocation( new ClassPathResource(sqlmapConfigPath));
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        String packageSearchPath = PathMatchingResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX+mapperLocation;
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources(packageSearchPath));
        sqlSessionFactoryBean.setDataSource(dataSourceProxy());
        return  sqlSessionFactoryBean;
    }

    @Bean(name = "jdbcTemplate")
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory){
        return new SqlSessionTemplate(sqlSessionFactory);
    }

5)配置事务传播相关内容

    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager() {
        DataSourceTransactionManager manager = new DataSourceTransactionManager(dataSourceProxy());
        return manager;
    }

    /**
     * 配置事务的传播特性
     */
    @Bean(name = "txAdvice")
    public TransactionInterceptor txAdvice(){
        TransactionInterceptor interceptor = new TransactionInterceptor();
        interceptor.setTransactionManager(transactionManager());
        Properties transactionAttributes = new Properties();
        //使用transactionAttributes.setProperty()配置传播特性
        interceptor.setTransactionAttributes(transactionAttributes);
        return interceptor;
    }

    @Bean(name = "txAdviceAdvisor")
    public Advisor txAdviceAdvisor() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        String transactionExecution = "execution(* cn.com.hiveview.springboot.demoapi..service.*.*(..))";
        pointcut.setExpression(transactionExecution);
        return new DefaultPointcutAdvisor(pointcut, txAdvice());
    }

四、动态数据源支持

在上面配置动态数据源支持的时候,我们使用了一个类“DynamicDataSource.java”。

这个类是自定义的类,继承了抽象类AbstractRoutingDataSource,正是通过这个抽象类来实现动态数据源的选择的。

来看下这个抽象类的成员变量:

private Map<Object, Object> targetDataSources;
    private Object defaultTargetDataSource;private Map<Object, DataSource> resolvedDataSources;

以下介绍可以参考上一节(3)的内容。

【1】targetDataSources:保存了key和数据库连接的映射关系

【2】defaultTargetDataSource:表示默认的数据库连接

【3】resolvedDataSources:是通过targetDataSources构建而来,存储的结构也是数据库标识和数据源的映射关系

接下来就是根据这个类,实现我们自己的类DynamicDataSource.java

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Autowired
    private DBHelper helper;
    
    @Override
    protected Object determineCurrentLookupKey() {
        return helper.getDBType();
    }

}

determineCurrentLookUpKey():决定需要使用哪个数据库,这个方法需要我们自己实现

先看一下在抽象类中,是如何使用这个方法的

    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource 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 + "]");
        } else {
            return dataSource;
        }
    }

因此我们只需在determineCurrentLookUpKey方法中,返回数据库的标志即可。

DBHelper类也是自定义的类,数据源持有类,存放了读、写库名字,以及设置数据源类型、获取数据源类型、清除数据源类型的方法

@Component
public class DBHelper {
     /** 
     * 线程独立 
     */  
    private ThreadLocal<String> contextHolder = new ThreadLocal<String>();  
    
    public static final String DB_TYPE_RW = "dataSource_db01";
    public static final String DB_TYPE_R = "dataSource_db02";
  
    public String getDBType() {
        String db = contextHolder.get();  
        if (db == null) {
            db = DB_TYPE_RW;
            // 默认是读写库
        }
        return db;  
    }
  
    public void setDBType(String str) {
        contextHolder.set(str);
    }
   
    public void clearDBType() {  
        contextHolder.remove();  
    } 
}

五、动态切换

1)配置注解 DS.java

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DS {
    String value();
}

2)使用AOP切换

@Aspect
@Component
public class DynamicDataSourceAspect {
    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class.getName());

    @Around("execution(* cn.com.exercise.dynamicDatasourse.module..service.*.*(..))")
    public Object switchDS(ProceedingJoinPoint point) throws Throwable {
        Class<?> className = point.getTarget().getClass();
        String dataSource = DBHelper.DB_TYPE_RW;
        if (className.isAnnotationPresent(DS.class)) {
            DS ds = className.getAnnotation(DS.class);
            dataSource = ds.value();
        }else{
            // 得到访问的方法对象
            String methodName = point.getSignature().getName();
            Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();
            Method method = className.getMethod(methodName, argClass);
            // 判断是否存在@DS注解
            if (method.isAnnotationPresent(DS.class)) {
                DS annotation = method.getAnnotation(DS.class);
                // 取出注解中的数据源名
                dataSource = annotation.value();
            }
        }
        // 切换数据源
        DBHelper dbHelper = new DBHelper();
        dbHelper.setDBType(dataSource);
        logger.info("当前数据源"+dataSource);
        try {
            return point.proceed();
        } finally {
            dbHelper.clearDBType();
        }
    }
}

完成以上内容后,就可以在service层的方法上,添加@DS注解,来实现数据源的切换了。

六、使用

service层代码

@Service
public class DynamicService {
    @Autowired
    DynamicDao dynamicDao;

    public List<DynamicCondition> getListFromSource1(){
        return dynamicDao.getListFromSource1();
    }

    @DS('读库名字')
    public List<DynamicCondition> getListFromSource2(){
        return dynamicDao.getListFromSource2();
    }
}

由于写库是默认数据源,因此当不使用@DS配置数据源,以及使用@DS(“写库名字”)时,使用的都是写库。

依次访问地址:

http://localhost:8082/dynamic/source1,

 http://localhost:8082/dynamic/source2

运行结果如下:

 按照以上步骤,就可以完成动态切换数据源了,下面附上 完整代码连接

猜你喜欢

转载自www.cnblogs.com/wulisz/p/10078270.html
今日推荐