天天看點

動态資料源實作原理-SpringBoot動态資料源切面實作資料源原理 *動态資料源 無注解動态資料源 注解(方法級)

文章目錄

  • 動态資料源
    • 學會注解的使用方式
      • 核心概念
      • @Retention
      • @Target
      • @Documented
      • @Inherited
      • @Repeatable
        • 注解的本質
        • 擷取注解屬性
  • 切面實作
  • 資料源原理 *
    • 動态資料源源碼解析過程
  • 動态資料源 無注解
  • 動态資料源 注解(方法級)

動态資料源

構想使用方法加注解模式就可以切換該方法所使用的資料庫源。通過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之前設定好。