Spring Boot + Druid multi data source binding

date: 2019-12-19 14:40:00
updated: 2019-12-19 15:10:00

Spring Boot + Druid multi data source binding

Version environment: Spring Boot 2.0.6

Reference document address:
https://blog.csdn.net/cllaure/article/details/81509303

1. Project structure

Several important documents are listed, and other documents are omitted

  • analysis
    • AnalysisApplication.java
    • SpringUtil.java
  • dao
    • OperateLogMapper.java
  • datasource
    • DynamicDataSource.java
    • DynamicDataSourceAspect.java
    • DynamicDataSourceContextHolder.java
    • DynamicDataSourceRegister.java
    • TargetDataSource.java
  • entity
  • service
    • AnalysisService.java
  • utils

2. About starting class AnalysisApplication

Because what I want is to let the program automatically execute a method instead of activating an interface by accessing the url when starting the project, so I need to use springutil. Com in the startup class Getapplicationcontext () to get the Spring context, so as to inject the class you need and execute the method

@SpringBootApplication
@Import(DynamicDataSourceRegister.class) // a key!!! Used to override Spring's default method for creating database connections
@ComponentScan("com.bigdata.*") // The annotation of the Service layer is @ component, but it may not be accessible. Here, the whole directory is directly introduced through annotation
@MapperScan("com.bigdata.dao")
public class AnalysisApplication {
    public static void main(String[] args) {
        SpringApplication.run(AnalysisApplication.class, args);

        // When Spring starts, it will get the analysis instance and execute the appStart() method
        ApplicationContext context = SpringUtil.getApplicationContext();
        AnalysisService analysis = context.getBean(AnalysisService.class);
        analysis.appStart();
    }
}

3. About SpringUtil

It should be noted that this class needs to be placed under the same package or sub package of the startup class before it can be scanned, otherwise it will become invalid.

@Component
public class SpringUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(SpringUtil.applicationContext == null){
            SpringUtil.applicationContext  = applicationContext;
        }
    }

    //Get applicationContext
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    //Get Bean. By name
    public static Object getBean(String name){
        return getApplicationContext().getBean(name);

    }

    //Get Bean. By class
    public static <T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }

    //Return the specified Bean through name and Clazz
    public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }
}

4. Multi data source connection initialization

First, create a folder of datasource. The files required under this folder are:

  • DynamicDataSource.java
  • DynamicDataSourceAspect.java
  • DynamicDataSourceContextHolder.java
  • DynamicDataSourceRegister.java
  • TargetDataSource.java

If you want to switch data sources through annotation, you need to use dynamicdatasourceaspect Java and targetdatasource Java, on the contrary, if you have a lot of data source connections and want to switch the data source by passing parameters, you don't need these two files.

Next, I will rewrite the code on the document according to the example of multi data source connection.

4.1 DynamicDataSource.java

The purpose of this file is to override the determineCurrentLookupKey() method. The returned value is the current database connection saved in the DynamicDataSourceContextHolder class

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

4.2 DynamicDataSourceContextHolder.java

This file is used to switch data sources and switch to the specified database connection through the setDataSourceType(String dataSourceType) method. dataSourceType is actually an alias when initializing the database connection.

Aliases for all database connections are saved in dataSourceIds.

public class DynamicDataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    public static List<String> dataSourceIds = new ArrayList<>();

    public static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }

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

//    Restore to default database
    public static void clearDataSourceType() {
        contextHolder.remove();
    }

    /**
     * Judge whether the specified datasroute currently exists
     */
    public static boolean containsDataSource(String dataSourceId) {
        return dataSourceIds.contains(dataSourceId);
    }
}

4.3 DynamicDataSourceRegister.java

This file is used to initialize the data source connection and inject it into the Bean.

Before Spring starts, it will automatically execute the setEnvironment(Environment env) method, which initializes the database connection by reading the configuration file. Because we need to switch the database connection through the alias, if the database name is unique, we can use the database name as the alias.

/**
 *  Dynamic data source registration
 *  To start a dynamic data source, please add @ Import(DynamicDataSourceRegister.class) to the startup class
 */

public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceRegister.class);

    // If the data source type is not specified in the configuration file, use this default value
    private static final Object DATASOURCE_TYPE_DEFAULT = "com.alibaba.druid.pool.DruidDataSource";

    // data source
    private DataSource defaultDataSource;
    private Map<String, DataSource> customDataSources = new HashMap<>();

    //Load multi data source configuration
    @Override
    public void setEnvironment(Environment env) {
        // Read configuration file for more data sources
        PropertiesUtil props = new PropertiesUtil("relations.properties");
        PropertiesEntity propsEntity = PropertiesEntity.newInstance();
        ...... Parse configuration file and assign value ......

        System.out.println("Start initializing dynamic data source~~~~");
        initCustomDataSources(propsEntity);
    }

    //Initialize more data sources
    private void initCustomDataSources(PropertiesEntity propsEntity) {
        Map<String, Object> dsMap = new HashMap<>();

        // The initialization of multiple database connections is realized through a loop
        dsMap.put("driver-class-name", propsEntity.getDriveClass());
        dsMap.put("url", propsEntity.getUrl());
        dsMap.put("username", propsEntity.getUsername());
        dsMap.put("password", propsEntity.getPassword());
        DataSource ds = buildDataSource(dsMap);

        // Assuming that the library name is "db1", set it as the default database connection. Of course, it can not be set. It doesn't matter. Bind the alias of the default database connection to default in the 'registerBeanDefinitions' method
        if(dbName.equals("db1")){
            defaultDataSource = buildDataSource(dsMap);
        }else{
            customDataSources.put(dbName, ds);
        }
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        // Add master data source to more data sources
        targetDataSources.put("default", defaultDataSource);
        DynamicDataSourceContextHolder.dataSourceIds.add("default");
        // Add more data sources
        targetDataSources.putAll(customDataSources);
        for (String key : customDataSources.keySet()) {
            DynamicDataSourceContextHolder.dataSourceIds.add(key);
        }

        // Create DynamicDataSource
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(DynamicDataSource.class);
        beanDefinition.setSynthetic(true);
        MutablePropertyValues mpv = beanDefinition.getPropertyValues();
        mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
        mpv.addPropertyValue("targetDataSources", targetDataSources);
        registry.registerBeanDefinition("default", beanDefinition);

        logger.info("Dynamic DataSource Registry");

    }

    //Create DataSource
    @SuppressWarnings("unchecked")
    public DataSource buildDataSource(Map<String, Object> dsMap) {
        try {
            Object type = dsMap.get("type");
            if (type == null)
                type = DATASOURCE_TYPE_DEFAULT;// Default DataSource

            Class<? extends DataSource> dataSourceType;
            dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);

            String driverClassName = dsMap.get("driver-class-name").toString();
            String url = dsMap.get("url").toString();
            String username = dsMap.get("username").toString();
            String password = dsMap.get("password").toString();

            DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
                    .username(username).password(password).type(dataSourceType);
            return factory.build();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}

4.4 DynamicDataSourceAspect.java

This file is mainly used to write the operations that need to be done before and after the execution of the method that references the annotation.

/**
 * Switch data source Advice
 */
@Aspect
@Order(-1)// Ensure that the AOP is executed before @ Transactional
@Component
public class DynamicDataSourceAspect {
    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

    @Before("@annotation(ds)")
    public void changeDataSource(JoinPoint point, TargetDataSource ds) {
        String dsId = ds.name();
        if (!DynamicDataSourceContextHolder.containsDataSource(dsId)) {
            logger.error("data source[{}]Does not exist, use default data source > {}", ds.name(), point.getSignature());
        }else {
            logger.debug("Use DataSource : {} > {}", dsId, point.getSignature());
            DynamicDataSourceContextHolder.setDataSourceType(dsId);
        }

    }
    @After("@annotation(ds)")
    public void restoreDataSource(JoinPoint point, TargetDataSource ds) {
        logger.debug("Revert DataSource : {} > {}", ds.name(), point.getSignature());
        DynamicDataSourceContextHolder.clearDataSourceType();
    }
}

4.5 TargetDataSource.java

Annotation file

// Used on methods to specify which data source to use
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    String name();
}

5. Method implementation

If it is implemented through annotation, you can use annotation on the method in this file or the method in dao layer, and switch the database by assigning a value to name. If there is no value, the default database connection is used.

@Component
public class AnalysisService {

    @Autowired
    OperateLogMapper opLogMapper;

    // @TargetDataSource(name="")
    public void appStart() {
        // Switch to the database connection with the alias db1
        DynamicDataSourceContextHolder.setDataSourceType("db1");
        ... Database operation ...
        // Restore to default connection
        DynamicDataSourceContextHolder.clearDataSourceType();

        // Switch to the default connection through the alias. If the database operation is not carried out directly through the set method, it is also carried out in the default connection
        DynamicDataSourceContextHolder.setDataSourceType("default");
        ... Database operation ...
    }
}

6. About mybatis xml

When using MyBatis to write sql statements, the table name is passed through variables. At this time, if #{0} is used, an error will be reported, because # will automatically add single quotation marks to the parameters inside and splice them to the string. If it is similar to where name = 'Zhang San', there is no problem, but there is a problem with select * from 'tableName'.

There are two solutions. One is to package the parameters to be passed into a HashMap and use select * from ${_key} when assigning values, so that they can be assigned dynamically. The second method is to pass N parameters and assign values using select * from ${param1}.

Tags: Spring Boot

Posted by roswell on Tue, 10 May 2022 09:47:48 +0300