Springboot实现自定义动态数据源

实现方案

1.核心是实现继承AbstractRoutingDataSource的自定义DynamicDataSource

  1. 重写determineCurrentLookupKey()方法,这个方法需要返回最终选择数据源key。
  2. 构造方法:调用父类中的方法。
    setTargetDataSources:传入一个Map其中key为配置的数据库名称,value为一个数据源。作用是将配置的所有数据源传入。
    setDefaultTargetDataSource:设置默认的数据源
    afterPropertiesSet:这个我在写代码时候有点疑问,为什么要手动调用呢,首先这个方法是实现InitializingBean接口的,spring会在初始化前调用。于是我就把这行代码删掉,确实没影响。想要了解spring生命周期相关可以看后面的总结。

DynamicDataSource

package com.example.dynamicdatasourcetest.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Component
public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(LoadDataSource loadDataSource)
    {
        Map<String, DataSource> dataSourceMap = loadDataSource.loadAllDataSource();
        super.setTargetDataSources(new HashMap<>(dataSourceMap));
        super.setDefaultTargetDataSource(dataSourceMap.get(DataSourceConst.DEFAULT_DATASOURCE));
        super.afterPropertiesSet();
    }
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

2.加载数据源

从上面核心步骤可以知道,我们现在还缺少数据源,所以接下来就是根据配置文件加载数据源。

  1. LoadDataSource这个类主要就根据配置信息,创建数据源对象。创建使用Druid提供的对象工厂去创建。
  2. 加载配置,这里是传创建一个DruidProperties类去读取配置

这里要注意的是@EnableConfigurationProperties(DruidProperties.class)和@ConfigurationProperties(prefix = “spring.datasource”)的使用。前者是为了将DruidProperties注入到LoadDataSource中,后者是将application.yml中的配置读入。

LoadDataSource


package com.example.dynamicdatasourcetest.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.example.dynamicdatasourcetest.config.DruidProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

@Component
@EnableConfigurationProperties(DruidProperties.class)
public class LoadDataSource {
    @Autowired
    DruidProperties druidProperties;

    public Map<String, DataSource> loadAllDataSource() {
        Map<String, DataSource> map = new HashMap<>();
        Map<String, Map<String, String>> ds = druidProperties.getDs();
        try {
            Set<String> keySet = ds.keySet();
            for (String key : keySet) {
                map.put(key, druidProperties.dataSource((DruidDataSource) DruidDataSourceFactory.createDataSource(ds.get(key))));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return map;
    }
}

DruidProperties

package com.example.dynamicdatasourcetest.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

@Configuration
@ConfigurationProperties(prefix = "spring.datasource")
public class DruidProperties {
    private int initialSize;

    private int minIdle;

    private int maxActive;

    private int maxWait;

    private int timeBetweenEvictionRunsMillis;

    private int minEvictableIdleTimeMillis;

    private int maxEvictableIdleTimeMillis;

    private String validationQuery;

    private boolean testWhileIdle;

    private boolean testOnBorrow;

    private boolean testOnReturn;

    private Map<String, Map<String,String>> ds;

    public DruidDataSource dataSource(DruidDataSource datasource) {
        /** 配置初始化大小、最小、最大 */
        datasource.setInitialSize(initialSize);
        datasource.setMaxActive(maxActive);
        datasource.setMinIdle(minIdle);

        /** 配置获取连接等待超时的时间 */
        datasource.setMaxWait(maxWait);

        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);

        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);

        /**
         * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
         */
        datasource.setValidationQuery(validationQuery);
        /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
        datasource.setTestWhileIdle(testWhileIdle);
        /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnBorrow(testOnBorrow);
        /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnReturn(testOnReturn);
        return datasource;
    }

    public Map<String, Map<String, String>> getDs() {
        return ds;
    }

    public void setDs(Map<String, Map<String, String>> ds) {
        this.ds = ds;
    }

    public int getInitialSize() {
        return initialSize;
    }

    public void setInitialSize(int initialSize) {
        this.initialSize = initialSize;
    }

    public int getMinIdle() {
        return minIdle;
    }

    public void setMinIdle(int minIdle) {
        this.minIdle = minIdle;
    }

    public int getMaxActive() {
        return maxActive;
    }

    public void setMaxActive(int maxActive) {
        this.maxActive = maxActive;
    }

    public int getMaxWait() {
        return maxWait;
    }

    public void setMaxWait(int maxWait) {
        this.maxWait = maxWait;
    }

    public int getTimeBetweenEvictionRunsMillis() {
        return timeBetweenEvictionRunsMillis;
    }

    public void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis) {
        this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
    }

    public int getMinEvictableIdleTimeMillis() {
        return minEvictableIdleTimeMillis;
    }

    public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) {
        this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
    }

    public int getMaxEvictableIdleTimeMillis() {
        return maxEvictableIdleTimeMillis;
    }

    public void setMaxEvictableIdleTimeMillis(int maxEvictableIdleTimeMillis) {
        this.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis;
    }

    public String getValidationQuery() {
        return validationQuery;
    }

    public void setValidationQuery(String validationQuery) {
        this.validationQuery = validationQuery;
    }

    public boolean isTestWhileIdle() {
        return testWhileIdle;
    }

    public void setTestWhileIdle(boolean testWhileIdle) {
        this.testWhileIdle = testWhileIdle;
    }

    public boolean isTestOnBorrow() {
        return testOnBorrow;
    }

    public void setTestOnBorrow(boolean testOnBorrow) {
        this.testOnBorrow = testOnBorrow;
    }

    public boolean isTestOnReturn() {
        return testOnReturn;
    }

    public void setTestOnReturn(boolean testOnReturn) {
        this.testOnReturn = testOnReturn;
    }
}

3. 用ThreadLocal来保存创建的datasource

第一步中的determineCurrentLookupKey方法中,我们是从DynamicDataSourceContextHolder中来获取数据源key,那么这个key是什么时候来的呢?为什么要用ThreadLocal保存?

  1. 这DynamicDataSourceContextHolder对象就是一个工具类,用来注入到想要切换数据源的地方。
  2. 使用ThreadLocal是为了让不同线程使用自己定义的数据源,在多线程情况下不会被其他线程更改。

DynamicDataSourceContextHolder

package com.example.dynamicdatasourcetest.datasource;

public class DynamicDataSourceContextHolder {
    private static ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    public static void setDataSourceType(String dsType) {
        CONTEXT_HOLDER.set(dsType);
    }

    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }

}

4. 使用自定义数据源

这里就是一种简单的使用,根据业务需要更改。一般像一些多租户模式的项目,会将用户使用的数据源放在请求头中,后台用拦截器来切换数据源。

TestController

@RestController
@RequestMapping("/testDatasource")
public class TestController {

    @Autowired
    private UserService userService;
    @Autowired
    private DynamicDataSourceUtil dataSourceUtil;

    @GetMapping(value = "/getData/{dsType}")
    public HttpResonse getData(@PathVariable("dsType") String dsType) {
        //http://localhost:8888/testDatasource/getData/master

        if (dataSourceUtil.checkDataSourceType(dsType)){
            DynamicDataSourceContextHolder.setDataSourceType(dsType);
        }
        List<User> list = userService.list();
        return new HttpResonse().successful(list);
    }
}

5. 使用注解来切换数据源

虽然我们经过上面的方法已经可以做到动态切换数据源了,但是它的使用场景非常局限,而且比较麻烦。下面我们就使用aop的形式来实现动态切换。

  1. 下面就是一个简单的aop实现,首先我们先定义一个注解,就叫他DataSouce(@DataSouce)其中@Retention,@Target是定义生命周期和作用域。
  2. 定义一个切面,pc()是切入点,around是环绕通知。这个类作用就是 去拿注解在类或者方法上@DataSouce中的value,拿到后放入DynamicDataSourceContextHolder中,也就是TreadLocal中。

@DataSource

package com.example.dynamicdatasourcetest.annotation;

import com.example.dynamicdatasourcetest.datasource.DataSourceConst;


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

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

    String value() default DataSourceConst.DEFAULT_DATASOURCE;
}

DataSourceAspect

package com.example.dynamicdatasourcetest.aop;

import com.example.dynamicdatasourcetest.annotation.DataSource;
import com.example.dynamicdatasourcetest.datasource.DynamicDataSourceContextHolder;
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.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class DataSourceAspect {
    @Pointcut("@annotation(com.example.dynamicdatasourcetest.annotation.DataSource) || @within(com.example.dynamicdatasourcetest.annotation.DataSource)")
    public void pc(){

    }

    @Around("pc()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint){
        DataSource dataSource = getDataSource(proceedingJoinPoint);
        if (dataSource !=null){
            String value = dataSource.value();
            DynamicDataSourceContextHolder.setDataSourceType(value);
        }
        try {
            return proceedingJoinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }finally {
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
        return null;
    }

    private DataSource getDataSource(ProceedingJoinPoint proceedingJoinPoint) {
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();

        //方法上找
        DataSource annotation = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if (annotation != null) {
            return annotation;
        }
        //类上找
        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    }
}

这样是不是很方便,直接在方法上打注解就可以了,要注意的是,这两种方法不能同时使用,因为他最终还是要使用service上的注解。其次就是方法优先级大于类上的加的优先级。

UserServiceImpl

package com.example.dynamicdatasourcetest.service.impl;

import com.example.dynamicdatasourcetest.annotation.DataSource;
import com.example.dynamicdatasourcetest.bean.User;
import com.example.dynamicdatasourcetest.datasource.DataSourceConst;
import com.example.dynamicdatasourcetest.mapper.UserMapper;
import com.example.dynamicdatasourcetest.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;
    @Override
    @DataSource(DataSourceConst.DATASOURCE_TEST2)
    public List<User> list() {
        return userMapper.selectAll();
    }
}

全部代码

由于其它代码都比较简单,而且配置文件这些都有差异,所以在上面只贴了主要代码整个项目我会上传到gitee上。
代码非常简单,有兴趣可以pull下来看看。

gitee地址

小结

此项目是一个练习项目,可以通过他学习自定义加载数据源,切换数据源,ThreadLocal使用,注解,aop等等。
git上也有很多现成的解决方案,比如baomido(mybatis-plus)开源的组件,完全可以作为项目解决方案,兼容性很好。==> dynamic-datasource-spring-boot-starter

知识点总结

1. spring生命周期

说起spring bean的生命周期,那就不得不放上一张图了。
Springboot实现自定义动态数据源_第1张图片
其实简单来说就是,bean的生命周期就只有4步,实例化 → 属性赋值 → 初始化 → 销毁。我们一般使用bean就是在初始化后面到销毁前。如果想要对bean做一些特殊的操作,或者在使用之前做一些初始化行为,那就必须要使用到spring提供的接口。
首先说说这个InstantiationAwareBeanPostProcessor接口,根据名字可以看出他是和实例化有关,这个类里面有四个方法,其中postProcessBeforeInstantiation,postProcessAfterInstantiation看名字 就知道是在实例化之前后做一些事情。那么这两个方法到底是做什么的呢。其他两个不是很清楚,有兴趣自己查阅。

  default Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
        return null;//返回一个对象(废话)-->怎么用?一般是在bean实例化前去自定义构造一个bean然后替换当前的bean
    }

    default boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        return true;//true 表示在初始化结束后,不调用后面的生命周期方法例如 postProcessProperties等等
    }
       @Nullable
    default PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
        return null;
    }

    /** @deprecated */
    @Deprecated
    @Nullable
    default PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {
        return pvs;
    }

说完InstantiationAwareBeanPostProcessor,该说说BeanPostProcessor 接口了,其实InstantiationAwareBeanPostProcessor本身就继承了BeanPostProcessor 接口。意味着你可以通过重写,控制实例化和初始化过程。
下面是BeanPostProcessor ,根据方法名称就知道,即使在初始化前后做一些事情

public interface BeanPostProcessor {
    @Nullable
    default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Nullable
    default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}

说到这有人就说,那这也没有afterPropertiesSet方法啊,这是InitializingBean接口中定义的抽象方法,这个方法的实现会在属性赋值之后调用,说直白点就是,在@Autowired等各种注入完成之后执行。这是一个什么状态点呢?属于说是spring已经干差不多了,现在想做什么最后的增强,赶紧告诉spring。aop就是在这一步完成的。

常用的就讲完了,剩下的就靠自己慢慢研究了。

2.AOP

这里还是先给出一个比较专业的概念定义:

  • Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的
    Advice。

  • Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。

  • Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。

  • Advice(增强):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。

  • Target(目标对象):织入 Advice 的目标对象.。 Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程.

aop实现就不多说了,面试题都有,我们就讲实战怎么去用。
工作中一般都是基于AspectJ实现AOP操作,AspectJ不是Spring组成部分,独立AOP框架,一般把AspectJ和Spring框架一起使用,进行AOP操作。
DataSourceAspect就是一个简单的使用样例,一般步骤如下:

  1. 切面类 @Aspect: 定义切面类,加上@Aspect、@Component注解
  2. 定义切点 @Pointcut,和切面表达式。
    切面表达式详解

3.Advice,在切入点上执行的增强处理,主要有五个注解:

@Before 在切点方法之前执行

@After 在切点方法之后执行

@AfterReturning 切点方法返回后执行

@AfterThrowing 切点方法抛异常执行

@Around 属于环绕增强,能控制切点执行前,执行后

你可能感兴趣的:(spring,boot,java,spring)