动态数据源实现原理-SpringBoot

动态数据源

构想使用方法加注解模式就可以切换该方法所使用的数据库源。通过spring切面进行方法的增强。

故我们的搭建步骤为 注解 -》切面-》数据源原理-》实现 (顺序不分先后,仅为本文逻辑)

学会注解的使用方式

核心概念

@Retention

  • Retention英文意思有保留、保持的意思,它表示注解存在阶段是保留在源码(编译期),字节码(类加载)或者运行期(JVM中运行)。在@Retention注解中使用枚举RetentionPolicy来表示注解保留时期
  • @Retention(RetentionPolicy.SOURCE),注解仅存在于源码中,在class字节码文件中不包含
  • @Retention(RetentionPolicy.CLASS), 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得
  • @Retention(RetentionPolicy.RUNTIME), 注解会在class字节码文件中存在,在运行时可以通过反射获取到
  • 如果我们是自定义注解,则通过前面分析,我们自定义注解如果只存着源码中或者字节码文件中就无法发挥作用,而在运行期间能获取到注解才能实现我们目的,所以自定义注解中肯定是使用 @Retention(RetentionPolicy.RUNTIME)

@Target

  • Target的英文意思是目标,这也很容易理解,使用@Target元注解表示我们的注解作用的范围就比较具体了,可以是类,方法,方法参数变量等,同样也是通过枚举类ElementType表达作用类型
  • @Target(ElementType.TYPE) 作用接口、类、枚举、注解
  • @Target(ElementType.FIELD) 作用属性字段、枚举的常量
  • @Target(ElementType.METHOD) 作用方法
  • @Target(ElementType.PARAMETER) 作用方法参数
  • @Target(ElementType.CONSTRUCTOR) 作用构造函数
  • @Target(ElementType.LOCAL_VARIABLE)作用局部变量
  • @Target(ElementType.ANNOTATION_TYPE)作用于注解(@Retention注解中就使用该属性)
  • @Target(ElementType.PACKAGE) 作用于包
  • @Target(ElementType.TYPE_PARAMETER) 作用于类型泛型,即泛型方法、泛型类、泛型接口 (jdk1.8加入)
  • @Target(ElementType.TYPE_USE) 类型使用.可以用于标注任意类型除了 class (jdk1.8加入)
  • 一般比较常用的是ElementType.TYPE类型

@Documented

  • Document的英文意思是文档。它的作用是能够将注解中的元素包含到 Javadoc 中去。

@Inherited

  • Inherited的英文意思是继承,但是这个继承和我们平时理解的继承大同小异,一个被@Inherited注解了的注解修饰了一个父类,如果他的子类没有被其他注解修饰,则它的子类也继承了父类的注解。

@Repeatable

  • Repeatable的英文意思是可重复的。顾名思义说明被这个元注解修饰的注解可以同时作用一个对象多次,但是每次作用注解又可以代表不同的含义。
注解的本质
  • 注解的本质就是一个Annotation接口
/**Annotation接口源码*/
public interface Annotation {

    boolean equals(Object obj);

    int hashCode();

    Class<? extends Annotation> annotationType();
}
获取注解属性

通过反射来获取,主要以下三个方法:

 /**是否存在对应 Annotation 对象*/
  public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
        return GenericDeclaration.super.isAnnotationPresent(annotationClass);
    }

 /**获取 Annotation 对象*/
    public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
        Objects.requireNonNull(annotationClass);

        return (A) annotationData().annotations.get(annotationClass);
    }
 /**获取所有 Annotation 对象数组*/   
 public Annotation[] getAnnotations() {
        return AnnotationParser.toArray(annotationData().annotations);
    }    

自定义注解如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Serize {
    String name() default "Serize";

    String part() default "";

    int version() default 1;
}

使用案例如下:

public class SerizeDemo {
    @Serize(version = 15000)
    public static void TransferVersion(double version) throws NoSuchMethodException {
        System.out.println(processSerizeVersion(money));

    }

    private static boolean processSerizeVersion(double version) throws NoSuchMethodException {
        Method transferVersion = SerizeDemo.class.getDeclaredMethod("TransferVersion", double.class);
        boolean present = transferVersion.isAnnotationPresent(Serize.class);
        if (present) {
            Serize serize = transferVersion.getAnnotation(Serize.class);
            int version = serize.version();
            System.out.println("注解version"+version);
            return true;
        } else {
            return false;
        }
    }

    public static void main(String[] args) throws NoSuchMethodException {
        TransferVersion(222);
    }
}

切面实现

这里直接上手使用切面,目的是给有我们自定义标记的注解进行方法增强。以实现方法级别的增强,具体其他的实现方式可以查资料进行实现。

@Aspect
@Slf4j
@Component
public class SerizeAspect {
    /**
     * 定义切入点,切入点为com.serize.annoDemo下的所有函数
     */
    @Pointcut("execution(public * com.serize.annoDemo..*.*(..))")
    public void serizePoint() {
    }

    /**
     * 自定义注解 切入点
     */
    @Pointcut("@annotation(com.serize.annoDemo.Serize)")
    public void noAnnotation() {

    }

    /**
     * 前置通知:在连接点之前执行的通知
     * 与上有注解Serize的方法
     * @param joinPoint
     * @throws Throwable
     */
    @Before("serizePoint()&&noAnnotation()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        log.info(joinPoint.getSignature().getName());
        log.info(joinPoint.getSignature().toString());
        log.info(joinPoint.getKind());
        log.info(joinPoint.getThis().toString());
        log.info(joinPoint.getTarget().toString());
        log.info(joinPoint.getSourceLocation().toString());
    }

    @AfterReturning(returning = "ret", pointcut = "serizePoint()")
    public void doAfterReturning(Object ret) throws Throwable {
        // 处理完请求,返回内容
        log.info("RESPONSE : " + ret);
    }
}

数据源原理 *

在Java中所有的连接池都按照规范实现DataSource接口,在获取连接的时候即可通过getConnection()获取连接而不用关心底层究竟是何数据库连接池。

这个规范是由java包给出的,代码如下:

package javax.sql;

public interface DataSource  extends CommonDataSource, Wrapper {

  Connection getConnection() throws SQLException;
  
  Connection getConnection(String username, String password)
    throws SQLException;
}

在大多数系统中我们只需要一个数据源,而现在WEB系统通常是Spring为基石。不管你是xml配置,javaBean配置还是yml,properties配置文件配置,其核心就是注入一个数据源交给spring的进行管理。

在Spring中从2.0.1版本默认提供了AbstractRoutingDataSource,我们继承它实现相关方法,把所有需要的数据源设置进去即可动态的切换数据源。

package org.springframework.jdbc.datasource.lookup;

/**
 抽象数据源实现,它根据查找键将getConnection()调用路由到各种目标数据源之一。后者通常(但不一定)是通过某个线程绑定的事务上下文确定的。
 */
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

	 //设置所有的数据源
    private Map<Object, Object> targetDataSources;
    //设置默认的数据源,在没有找到相关数据源的时候会返回默认数据源
    private Object defaultTargetDataSource;
    //快速失败,可忽略
    private boolean lenientFallback = true;
    //Jndi相关,可忽略
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    //经过解析后的所有数据源,核心
    private Map<Object, DataSource> resolvedDataSources;
    //经过解析后的默认数据源,核心
    private DataSource resolvedDefaultDataSource;


	/**
	指定目标DataSources的映射,使用查找键作为键。映射值可以是相应的DataSource实例,也可以是数据源名称String(通过DataSourceLookup解析)。
	键可以是任意类型;该类只实现泛型查找过程。具体的键表示将由resolvespecificedlookupkey (Object)和determineCurrentLookupKey()处理。
	 */
	public void setTargetDataSources(Map<Object, Object> targetDataSources) {
		this.targetDataSources = targetDataSources;
	}

	/**
	如果有,请指定默认目标数据源。映射值可以是相应的DataSource实例,也可以是数据源名称String(通过DataSourceLookup解析)。
	如果没有一个键化的targetDataSources与determineCurrentLookupKey()当前查找键匹配,则此DataSource将被用作目标。
	 */
	public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
		this.defaultTargetDataSource = defaultTargetDataSource;
	}

	/**
	如果找不到当前查找键的特定数据源,请指定是否对默认数据源应用宽松的回退。
	默认为"true",接受在目标DataSource映射中没有相应条目的查找键——在这种情况下,简单地退回到默认DataSource。
	如果您希望只在查找键为空时才应用回退,请将此标志切换为"false"。没有数据源条目的查找键将导致IllegalStateException。
	 */
	public void setLenientFallback(boolean lenientFallback) {
		this.lenientFallback = lenientFallback;
	}

	/**
	设置用于解析targetDataSources映射中的数据源名称字符串的DataSourceLookup实现。
	默认是JndiDataSourceLookup,允许直接指定应用服务器DataSources的JNDI名称。
	 */
	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);
		}
	}

	/**
	按照targetDataSources映射中指定的方式,将给定的查找键对象解析为实际的查找键,以便与当前查找键进行匹配。
    默认实现只是按原样返回给定的键。
    参数:
    lookupKey—由用户指定的查找键对象
    返回:
    匹配所需的查找键	
	 */
	protected Object resolveSpecifiedLookupKey(Object lookupKey) {
		return lookupKey;
	}

	/**
	将指定的数据源对象解析为DataSource实例。
    默认实现处理DataSource实例和数据源名称(通过DataSourceLookup解析)。
    参数:
    dataSource—在targetDataSources映射中指定的数据源值对象
    返回:
    已解析的DataSource(从不为空)
    抛出:
    IllegalArgumentException——在不支持的值类型的情况下
	 */
	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));
	}

	/**
	检索当前目标数据源。确定当前查找键,在targetDataSources映射中执行查找,必要时返回指定的默认目标DataSource。
	 */
	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;
	}

	/**
    确定当前查找键。这通常用于检查线程绑定的事务上下文。
    允许任意键。返回的键需要与存储的查找键类型匹配,由resolvespecificedlookupkey方法解析。
	 */
	@Nullable
	protected abstract Object determineCurrentLookupKey();
}

注意的是AOP的order必须在事务的order之前。有关order的概念不懂的可以百度。

动态数据源源码解析过程

参考下面一篇博文:动态数据源-SpringManagedTransaction&&AbstractRoutingDataSource 源码解析过程

动态数据源 无注解

思路是直接使用aop对指定方法进行拦截,利用aop的特性,预先设定好部分。

@RestController
public class DysourceController {
    @Autowired
    DysourceService dysourceService;

    @GetMapping("primary")
    public Object primary(){
        return dysourceService.getAll();
    }
    @GetMapping("secondary")
    public Object secondary(){
        return dysourceService.getAll();
    }
}

在controller下增强:

@Aspect
@Component
public class DataSourceAop {
    //在primary方法前执行
    @Before("execution(* com.serize.controller.DysourceController.primary(..))")
    public void setDataSource2test01() {
        System.err.println("Primary业务");
        DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Primary);
    }

    //在secondary方法前执行
    @Before("execution(* com.serize.controller.DysourceController.secondary(..))")
    public void setDataSource2test02() {
        System.err.println("Secondary业务");
        DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Secondary);
    }
}

配置文件:

server.port=8086
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/dysource?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=x5
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.com.serize.mapper-locations=classpath*:mapper/**/*Mapper.xml
#配置主数据库
spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/dysource?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
spring.datasource.primary.username=root
spring.datasource.primary.password=x5
spring.datasource.primary.driver-class-name=com.mysql.jdbc.Driver

##配置次数据库
spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/dysource2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
spring.datasource.secondary.username=root
spring.datasource.secondary.password=x5
spring.datasource.secondary.driver-class-name=com.mysql.jdbc.Driver
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        DataSourceType.DataBaseType dataBaseType = DataSourceType.getDataBaseType();
        return dataBaseType;
    }

}
public class DataSourceType {

    //内部枚举类,用于选择特定的数据类型
    public enum DataBaseType {
        Primary, Secondary
    }

    // 使用ThreadLocal保证线程安全
    private static final ThreadLocal<DataBaseType> TYPE = new ThreadLocal<DataBaseType>();

    // 往当前线程里设置数据源类型
    public static void setDataBaseType(DataBaseType dataBaseType) {
        if (dataBaseType == null) {
            throw new NullPointerException();
        }
        TYPE.set(dataBaseType);
    }

    // 获取数据源类型
    public static DataBaseType getDataBaseType() {
        DataBaseType dataBaseType = TYPE.get() == null ? DataBaseType.Primary : TYPE.get();
        return dataBaseType;
    }

    // 清空数据类型
    public static void clearDataBaseType() {
        TYPE.remove();
    }
}
@Configuration
@MapperScan(basePackages = "com.serize.mapper", sqlSessionFactoryRef = "SqlSessionFactory") //basePackages 我们接口文件的地址
public class DynamicDataSourceConfig {

    // 将这个对象放入Spring容器中
    @Bean(name = "PrimaryDataSource")
    // 表示这个数据源是默认数据源
    @Primary
    // 读取application.properties中的配置参数映射成为一个对象
    // prefix表示参数的前缀
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource getDateSource1() {
        return DataSourceBuilder.create().build();
    }


    @Bean(name = "SecondaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource getDateSource2() {
        return DataSourceBuilder.create().build();
    }


    @Bean(name = "dynamicDataSource")
    public DynamicDataSource DataSource(@Qualifier("PrimaryDataSource") DataSource primaryDataSource,
                                        @Qualifier("SecondaryDataSource") DataSource secondaryDataSource) {

        //这个地方是比较核心的targetDataSource 集合是我们数据库和名字之间的映射
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DataSourceType.DataBaseType.Primary, primaryDataSource);
        targetDataSource.put(DataSourceType.DataBaseType.Secondary, secondaryDataSource);
        DynamicDataSource dataSource = new DynamicDataSource(); 
        dataSource.setTargetDataSources(targetDataSource);
        dataSource.setDefaultTargetDataSource(primaryDataSource);//设置默认对象
        return dataSource;
    }


    @Bean(name = "SqlSessionFactory")
    public SqlSessionFactory SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dynamicDataSource);
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/**/*Mapper.xml"));//设置我们的xml文件路径
        return bean.getObject();
    }
}

动态数据源 注解(方法级)

增加动态数据源注解

/**
 * 切换数据注解 可以用于类或者方法级别 方法级别优先级 > 类级别
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "primary"; //该值即key值,默认使用默认数据库
}

增加注解方法切面

@Aspect
@Component
@Slf4j
public class DynamicDataSourceAspect {
    
    @Before("@annotation(dataSource)")//拦截我们的注解
    public void changeDataSource(JoinPoint point, DataSource dataSource) throws Throwable {
        String value = dataSource.value();
        if (value.equals("primary")){
            DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Primary);
        }else if (value.equals("secondary")){
            DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Secondary);
        }else {
            DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Primary);//默认使用主数据库
        }

    }

    @After("@annotation(dataSource)") //清除数据源的配置
    public void restoreDataSource(JoinPoint point, DataSource dataSource) {
        DataSourceType.clearDataBaseType();
    }
}

总结就是将带有注解并注明使用哪个数据库的方法交给aop在获取connection之前设置好。

事务相关

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值