Implementation principle of dynamic data source - SpringBoot

Keywords: Java Spring Spring Boot

Dynamic data source

By using the annotation mode of the method, you can switch the database source used by the method. The method is enhanced through the spring section.

Therefore, our construction steps are annotation - Section - data source principle - Implementation (in no order, only the logic of this article)

Learn how to use annotations

Core concept

@Retention

  • Retention in English means "keep" and "keep". It means that the annotation exists in the source code (compilation time), bytecode (class loading) or runtime (running in the JVM). Use the enumeration RetentionPolicy in the @ retention annotation to indicate the annotation retention period
  • @Retention(RetentionPolicy.SOURCE). The annotation only exists in the source code and is not included in the class bytecode file
  • @Retention(RetentionPolicy.CLASS), the default retention policy. Annotations exist in the class bytecode file, but cannot be obtained at runtime
  • @Retention(RetentionPolicy.RUNTIME), the annotation will exist in the class bytecode file and can be obtained through reflection at run time
  • If we are user-defined annotations, according to the previous analysis, our user-defined annotations will not work if they are only stored in the source code or bytecode file, and our purpose can only be achieved if they can be obtained during operation. Therefore, @ Retention(RetentionPolicy.RUNTIME) must be used in the user-defined annotations

@Target

  • Target means target in English, which is easy to understand. Using @ target meta annotation to represent the scope of our annotation is more specific. It can be class, method, method parameter variable, etc. it also expresses the action type through enumerating class ElementType
  • @Target(ElementType.TYPE) functions as interface, class, enumeration and annotation
  • @Target(ElementType.FIELD) acts as a constant for attribute fields and enumerations
  • @Target(ElementType.METHOD) action method
  • @Target(ElementType.PARAMETER) action method parameter
  • @Target(ElementType.CONSTRUCTOR) function constructor
  • @Target(ElementType.LOCAL_VARIABLE) acts as a local variable
  • @Target(ElementType.ANNOTATION_TYPE) acts on annotation (@ Retention annotation uses this attribute)
  • @Target(ElementType.PACKAGE) acts on the package
  • @Target(ElementType.TYPE_PARAMETER) acts on type generics, that is, generic methods, generic classes and generic interfaces (added by jdk1.8)
  • @Target(ElementType.TYPE_USE) can be used to label any type except class (added by jdk1.8)
  • ElementType.TYPE is commonly used

@Documented

  • Document means document in English. Its function is to include the elements in the annotation into the Javadoc.

@Inherited

  • Inherited means inheritance in English, but this inheritance is similar to what we usually understand. An annotation annotated by @ inherited modifies a parent class. If its subclass is not modified by other annotations, its subclass also inherits the annotation of the parent class.

@Repeatable

  • Repeatable means repeatable in English. As the name suggests, the annotation modified by this meta annotation can act on an object multiple times at the same time, but each action annotation can represent different meanings.

The essence of annotation

  • Annotation is essentially an annotation interface
/**Annotation Interface source code*/
public interface Annotation {

    boolean equals(Object obj);

    int hashCode();

    Class<? extends Annotation> annotationType();
}

Get annotation properties

There are three main methods to obtain through reflection:

 /**Does the corresponding Annotation object exist*/
  public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
        return GenericDeclaration.super.isAnnotationPresent(annotationClass);
    }

 /**Get Annotation object*/
    public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
        Objects.requireNonNull(annotationClass);

        return (A) annotationData().annotations.get(annotationClass);
    }
 /**Gets an array of all Annotation objects*/   
 public Annotation[] getAnnotations() {
        return AnnotationParser.toArray(annotationData().annotations);
    }    

Custom comments are as follows:

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

    String part() default "";

    int version() default 1;
}

Use cases are as follows:

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("annotation version"+version);
            return true;
        } else {
            return false;
        }
    }

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

Slice implementation

Here, we use the section directly to enhance the annotation with our custom tags. To achieve method level enhancement, other specific implementation methods can be implemented by querying data.

@Aspect
@Slf4j
@Component
public class SerizeAspect {
    /**
     * Define pointcuts. Pointcuts are all functions under com.serize.annoDemo
     */
    @Pointcut("execution(public * com.serize.annoDemo..*.*(..))")
    public void serizePoint() {
    }

    /**
     * Custom annotation pointcuts
     */
    @Pointcut("@annotation(com.serize.annoDemo.Serize)")
    public void noAnnotation() {

    }

    /**
     * Pre notification: notification executed before the connection point
     * Same as the method annotated serialize
     * @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 {
        // After processing the request, return the content
        log.info("RESPONSE : " + ret);
    }
}

Data source principle*

In Java, all connection pools implement the DataSource interface according to the specification. When obtaining the connection, you can obtain the connection through getConnection(), regardless of the underlying database connection pool.

This specification is given by the java package. The code is as follows:

package javax.sql;

public interface DataSource  extends CommonDataSource, Wrapper {

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

In most systems, we only need one data source, and now WEB systems are usually based on spring. Whether you are xml configuration, javaBean configuration or yml,properties configuration file configuration, the core is to inject a data source to spring for management.

In Spring, AbstractRoutingDataSource is provided by default from version 2.0.1. We inherit its implementation methods and set all required data sources to dynamically switch data sources.

package org.springframework.jdbc.datasource.lookup;

/**
 An abstract data source implementation that routes getConnection() calls to one of the various target data sources according to the lookup key. The latter is usually (but not necessarily) determined by the transaction context bound by a thread.
 */
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

	 //Set all data sources
    private Map<Object, Object> targetDataSources;
    //Set the default data source. If no relevant data source is found, the default data source will be returned
    private Object defaultTargetDataSource;
    //Quick failure, negligible
    private boolean lenientFallback = true;
    //Jndi related, negligible
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    //All data sources after parsing, core
    private Map<Object, DataSource> resolvedDataSources;
    //Parsed default data source, core
    private DataSource resolvedDefaultDataSource;


	/**
	Specifies the mapping of the target DataSources, using the lookup key as the key. The mapping value can be the corresponding DataSource instance or the data source name string (resolved through DataSourceLookup).
	Keys can be of any type; This class implements only the generic lookup procedure. The specific key representation will be handled by resolvespecifiedlookup key (object) and determineCurrentLookupKey().
	 */
	public void setTargetDataSources(Map<Object, Object> targetDataSources) {
		this.targetDataSources = targetDataSources;
	}

	/**
	If so, specify the default target data source. The mapping value can be the corresponding DataSource instance or the data source name string (resolved through DataSourceLookup).
	If no keyed targetDataSources match the current lookup key of determineCurrentLookupKey(), this DataSource will be used as the target.
	 */
	public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
		this.defaultTargetDataSource = defaultTargetDataSource;
	}

	/**
	If a specific data source for the current lookup key cannot be found, specify whether to apply a relaxed fallback to the default data source.
	The default is "true" and accepts a lookup key that does not have a corresponding entry in the target DataSource map -- in this case, simply go back to the default DataSource.
	If you want to apply fallback only when the lookup key is empty, switch this flag to "false". A lookup key without a data source entry will result in an IllegalStateException.
	 */
	public void setLenientFallback(boolean lenientFallback) {
		this.lenientFallback = lenientFallback;
	}

	/**
	Sets the DataSourceLookup implementation used to resolve the data source name string in the targetDataSources map.
	The default is JNDI datasourcelookup, which allows you to directly specify the JNDI name of the application server DataSources.
	 */
	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);
		}
	}

	/**
	Resolves the given lookup key object to the actual lookup key in the manner specified in the targetDataSources map to match the current lookup key.
    The default implementation simply returns the given key as is.
    Parameters:
    lookupKey—The lookup key object specified by the user
    return:
    Match the required lookup key	
	 */
	protected Object resolveSpecifiedLookupKey(Object lookupKey) {
		return lookupKey;
	}

	/**
	Resolves the specified data source object to a DataSource instance.
    The default implementation handles the DataSource instance and data source name (resolved through DataSourceLookup).
    Parameters:
    dataSource—The data source value object specified in the targetDataSources map
    return:
    Resolved datasource (never empty)
    Throw:
    IllegalArgumentException-In case of unsupported value type
	 */
	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));
	}

	/**
	Retrieves the current target data source. Determine the current lookup key, perform the lookup in the targetDataSources map, and return the specified default target DataSource if necessary.
	 */
	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;
	}

	/**
    Determines the current lookup key. This is usually used to check the transaction context of the thread binding.
    Allow any key. The returned key must match the stored lookup key type and be resolved by the resolvespecificedlookup key method.
	 */
	@Nullable
	protected abstract Object determineCurrentLookupKey();
}

Note that the AOP order must precede the transaction order. If you don't understand the concept of order, you can baidu.

Dynamic data source code analysis process

Refer to the following blog post: Dynamic data source - springmanagedtransaction & & abstractroutingdatasource source code parsing process

No annotation for dynamic data source

The idea is to directly use aop to intercept the specified method, and use the characteristics of aop to set the part in advance.

@RestController
public class DysourceController {
    @Autowired
    DysourceService dysourceService;

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

Enhanced under controller:

@Aspect
@Component
public class DataSourceAop {
    //Execute before the primary method
    @Before("execution(* com.serize.controller.DysourceController.primary(..))")
    public void setDataSource2test01() {
        System.err.println("Primary business");
        DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Primary);
    }

    //Execute before using method
    @Before("execution(* com.serize.controller.DysourceController.secondary(..))")
    public void setDataSource2test02() {
        System.err.println("Secondary business");
        DataSourceType.setDataBaseType(DataSourceType.DataBaseType.Secondary);
    }
}

Profile:

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
#Configure master database
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

##Configure secondary database
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 {

    //The internal enumeration class is used to select a specific data type
    public enum DataBaseType {
        Primary, Secondary
    }

    // Using ThreadLocal to ensure thread safety
    private static final ThreadLocal<DataBaseType> TYPE = new ThreadLocal<DataBaseType>();

    // Set the data source type to the current process
    public static void setDataBaseType(DataBaseType dataBaseType) {
        if (dataBaseType == null) {
            throw new NullPointerException();
        }
        TYPE.set(dataBaseType);
    }

    // Get data source type
    public static DataBaseType getDataBaseType() {
        DataBaseType dataBaseType = TYPE.get() == null ? DataBaseType.Primary : TYPE.get();
        return dataBaseType;
    }

    // Clear data type
    public static void clearDataBaseType() {
        TYPE.remove();
    }
}
@Configuration
@MapperScan(basePackages = "com.serize.mapper", sqlSessionFactoryRef = "SqlSessionFactory") //basePackages the address of our interface file
public class DynamicDataSourceConfig {

    // Put this object into the Spring container
    @Bean(name = "PrimaryDataSource")
    // Indicates that this data source is the default data source
    @Primary
    // Read the configuration parameters in application.properties and map them into an object
    // Prefix indicates the prefix of the parameter
    @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) {

        //This place is the core of the comparison. The targetDataSource collection is the mapping between our database and the name
        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);//Set default object
        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"));//Set our xml file path
        return bean.getObject();
    }
}

Dynamic data source annotation (method level)

Add dynamic data source annotation

/**
 * Toggle data annotation can be used at class or method level. Method level priority > class level
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "primary"; //This value is the key value. The default database is used by default
}

Add annotation method section

@Aspect
@Component
@Slf4j
public class DynamicDataSourceAspect {
    
    @Before("@annotation(dataSource)")//Block our comments
    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);//The primary database is used by default
        }

    }

    @After("@annotation(dataSource)") //Clear configuration of data source
    public void restoreDataSource(JoinPoint point, DataSource dataSource) {
        DataSourceType.clearDataBaseType();
    }
}

The summary is to give the method annotated and indicating which database to use to aop and set it before obtaining the connection.

Posted by michealholding on Tue, 21 Sep 2021 01:40:36 -0700