SoFunction
Updated on 2025-03-08

MyBatis uses Zookeeper to save database configuration to dynamically refresh the implementation code

Core key points: Encapsulate a DataSource and rewrite getConnection to achieve it

Let's take a look step by step.

Environment: Spring Cloud + MyBatis

Configuring data sources under MyBatis regular mode: Use Spring Configuration

package ;


import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;

import ;

/**
  * Database Config Multi-data source configuration: primary data source.
  *
  * @author Felix Zhang 2021-08-02 17:30
  * @version 1.0.0
  */
@Configuration
@MapperScan(basePackages = {""},
        sqlSessionFactoryRef = "sqlSessionFactoryMainDataSource")
public class MainDataSourceConfig {



    //General configuration: Use the configuration inside.    @Primary
    @Bean(name = "mainDataSource")
    @ConfigurationProperties("")
    public DataSource mainDataSource() throws Exception {
        return ().build();
    }

    @Primary
    @Bean(name = "sqlSessionFactoryMainDataSource")
    public SqlSessionFactory sqlSessionFactoryMainDataSource(@Qualifier("mainDataSource") DataSource mainDataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        // configuration = new ();
        //(true);
        //(configuration);
        (new PathMatchingResourcePatternResolver().getResource("classpath:"));

        // Use mainDataSource data source to connect to mainDataSource library        (mainDataSource);

        //The following two sentences are only used for *.xml files. If the entire persistence layer operation does not require the use of the xml file (it can be done with only annotations), then no additional        //Specify the path of entity and mapper xml        //("");
        (new PathMatchingResourcePatternResolver().getResources("classpath:com/cnscud/cavedemo/fundmain/mapper/*.xml"));
        return ();
    }

    @Primary
    @Bean
    public SqlSessionTemplate sqlSessionTemplateMainDataSource(@Qualifier("sqlSessionFactoryMainDataSource") SqlSessionFactory sqlSessionTemplateMainDataSource) throws Exception {

        //Use the Factory configured in the annotation        return new SqlSessionTemplate(sqlSessionTemplateMainDataSource);
    }

    @Primary
    @Bean
    public PlatformTransactionManager mainTransactionManager(@Qualifier("mainDataSource") DataSource prodDataSource) {
        return new DataSourceTransactionManager(prodDataSource);
    }
}

The key function to obtain the data source here is mainDataSource, which we can implement by ourselves:

Because this is a one-time job, we cannot modify the direction of the DataSource, and can only do things inside the DataSource, so we need to implement a DataSource ourselves.

There are many steps, let’s take a look at the final result:

The final DataSourceWrapper

It completely encapsulates a DataSource, and it does not have any DataSource functions:

package ;

import ;
import ;
import ;
import ;
import ;
import ;

/**
  * Datasource wrapper, for the convenience of dynamic creation of DataSource.
  *
  * @author Felix Zhang 2021-08-05 14:14
  * @version 1.0.0
  */
public class DynamicByZookeeperDataSourceWrapper implements DataSource {

    protected SimpleDBNConnectionPool simpleDBNConnectionPool;
    protected String bizName;

    public DynamicByZookeeperDataSourceWrapper(SimpleDBNConnectionPool simpleDBNConnectionPool, String bizName) {
         = simpleDBNConnectionPool;
         = bizName;
    }

    protected DataSource pickDataSource() throws SQLException{
        return (bizName);
    }

    @Override
    public Connection getConnection() throws SQLException {
        return pickDataSource().getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return pickDataSource().getConnection(username, password);
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return pickDataSource().unwrap(iface);
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return pickDataSource().isWrapperFor(iface);
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return pickDataSource().getLogWriter();
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {
        pickDataSource().setLogWriter(out);
    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {
        pickDataSource().setLoginTimeout(seconds);
    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return pickDataSource().getLoginTimeout();
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        throw new SQLFeatureNotSupportedException();
    }
}

SimpleDBNConnectionPool

Supports temporary storage pools for multiple data sources, and can obtain different database DataSource instances according to name:

This class is responsible for creating a DataSource, which is stored in a Map. It can monitor changes in Zookeeper. Once the changes are heard, it closes the existing DataSource.

package ;

import ;
import ;
import org.;
import org.;

import ;
import ;
import ;
import ;
import ;
import ;
import ;

import static ;

/**
  * The simple datasource pool.
  *
  * Store DataSources of multiple databases according to the name, and will listen to Zookeeper configuration and dynamic reconstruction.
  *
  * @author adyliu (imxylz@)
  * @since 2011-7-27
  */
public class SimpleDBNConnectionPool {

    final Logger logger = (getClass());


    private Map<String, DataSource> instances = new ConcurrentHashMap<>();
    private final Set<String> watcherSchema = new HashSet<String>();


    public DataSource getInstance(String bizName) {
        try {
            return findDbInstance(bizName);
        }
        catch (SQLException e) {
            ();
        }
        return null;
    }

    public Connection getConnection(String bizName) throws SQLException {
        DataSource ds = getDataSource(bizName);
        return ();
    }

    public DataSource getDataSource(String bizName) throws SQLException {
        return findDbInstance(bizName);
    }


    protected void destroyInstance(final String bizName) {
        synchronized (instances) {
            DataSource oldInstanceIf = (bizName);
            (format("destoryInstance %s and %s", bizName, oldInstanceIf != null ? "close datasource" : "do nothing"));
            if (oldInstanceIf != null) {
                closeDataSource(oldInstanceIf);
            }
        }
    }

    protected void closeDataSource(DataSource ds) {
        if (ds instanceof HikariDataSource) {
            try {
                ((HikariDataSource) ds).close();
            }
            catch (Exception e) {
                ("Close datasource failed. ", e);
            }
        }
    }


    private DataSource createInstance(Map<String, String> dbcfg) {
        return new SimpleDataSourceBuilder().buildDataSource(dbcfg);
    }


    private DataSource findDbInstance(final String bizName) throws SQLException {
        DataSource ins = (bizName);
        if (ins != null) {
            return ins;
        }
        synchronized (instances) {// Synchronous operation            ins = (bizName);
            if (ins != null) {
                return ins;
            }
            boolean success = false;
            try {
                Map<String, String> dbcfg = (bizName);
                if (dbcfg == null) {
                    throw new SQLException("No such datasouce: " + bizName);
                }
                ins = createInstance(dbcfg);
                //("ins put "+ins);
                (bizName, ins);


                if ((bizName)) {
                    (bizName, new IZkDataListener() {

                        public void handleDataDeleted(String dataPath) throws Exception {
                            (dataPath + " was deleted, so destroy the bizName " + bizName);
                            destroyInstance(bizName);
                        }

                        public void handleDataChange(String dataPath, byte[] data) throws Exception {
                            (dataPath + " was changed, so destroy the bizName " + bizName);
                            destroyInstance(bizName);
                        }
                    });
                }
                success = true;
            }
            catch (SQLException e) {
                throw e;
            }
            catch (Throwable t) {
                throw new SQLException("cannot build datasource for bizName: " + bizName, t);
            }
            finally {
                if (!success) {
                    (bizName);
                }
            }
        }
        return ins;
    }

}

The code to actually create a DataSource:

package ;

import ;
import ;
import ;

import ;

/**
  * Hikari DataSource.
  *
  * Thinking: You can use different libraries to create DataSource according to the type in the parameter, such as Druid. (Default is HikariDataSource)
  *
  *
  * @author Felix Zhang 2021-08-05 11:14
  * @version 1.0.0
  */
public class SimpleDataSourceBuilder {


    public HikariDataSource buildDataSource(Map<String, String> args) {
        HikariConfig config = new HikariConfig();
        (getUrl(args));
        (("username"));
        (("password"));
        (getDriverClassName(args));

        String maximumPoolSizeKey = "maximum-pool-size";
        int maximumPoolSize = 30;
        if(((maximumPoolSizeKey))){
            maximumPoolSize = ((maximumPoolSizeKey));
        }

        ("cachePrepStmts", "true"); //Whether to customize the configuration, the following two parameters will take effect when true        ("prepStmtCacheSize", maximumPoolSize); //The connection pool size is 25 by default, and the official recommendation is 250-500        ("prepStmtCacheSqlLimit", "2048"); //The maximum length of a single sentence is 256 by default, officially recommended 2048        ("useServerPrepStmts", "true"); //The new version of MySQL supports server-side preparation, and can achieve significant performance improvement by enabling enabled        ("useLocalSessionState", "true");
        ("useLocalTransactionState", "true");
        ("rewriteBatchedStatements", "true");
        ("cacheResultSetMetadata", "true");
        ("cacheServerConfiguration", "true");
        ("elideSetAutoCommits", "true");
        ("maintainTimeStats", "false");

        (maximumPoolSize); //
        (10);//The minimum number of idle connections, default is 0        (600000);//Maximum survival time        (30000);//Timeout time 30 seconds        (60000);

        ("select 1");

        return new HikariDataSource(config);
    }

    private String getDriverClassName(Map<String, String> args) {
        return ("driver-class-name");
    }

    private String getUrl(Map<String, String> args) {
        return ("jdbc-url") == null ? ("url"): ("jdbc-url");
    }
}

In order to facilitate reading of Zookeeper nodes, there is also a SchemeNodeHelper:

Supports two configuration files: json or Properties format:

package ;

import ;
import ;
import ;
import ;
import org.;
import org.;

import ;
import ;
import ;
import ;
import ;

/**
  * Read database configuration from Zookeeper's /xpower/dbn node.
  * The content supports two formats: json or properties format.
  *
  * The JSON format is as follows:
  * {
  * "jdbc-url": "jdbc:mysql://127.0.0.1:3306/cavedemo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC",
  * "username": "dbuser",
  * "password": "yourpassword",
  * "driver-class-name": ""
  * }
  *
  * Properties format is as follows:
  * jdbc-url: jdbc:mysql://127.0.0.1:3306/cavedemo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
  * username: dbuser
  * password: password
  * driver-class-name:
  *
  * @author Felix Zhang
  * @since 2021-8-5
  */
public class SchemeNodeHelper {

    static final Logger logger = ();

    //Support two formats: json, properties    public static Map<String, String> getInstance(final String instanceName) throws Exception {
        String data = ().getDataAsString("/xpower/dbn/" + instanceName);
        if((data)){
            return null;
        }

        data = ();
        if (("{")) {
            //as json
            Map<String, String> swap = (data, );
            Map<String, String> result = new HashMap<>();

            if (swap != null) {
                for (String name : ()) {
                    ((), (name));
                }
            }

            return result;
        }
        else {
            //as properties
            Properties props = new Properties();
            try {
                (new StringReader(data));
            }
            catch (IOException e) {
                ("loading global config failed", e);
            }

            Map<String, String> result = new HashMap<>();

            for (String name : ()) {
                ((), (name));
            }

            return result;
        }
    }

    public static void watchInstance(final String bizName, final IZkDataListener listener) {
        final String path = "/xpower/dbn/" + bizName;
        ().subscribeDataChanges(path, listener);
    }
}

Practical application

Finally, in the MyBatis project, replace the original MainDataSource code as:

/**
      * Add @Primary annotation, set the default data source, transaction manager.
      * A DataSource that can be dynamically rebuilt is used here. If the Zookeeper configuration changes, it will be dynamically rebuilt.
      */
    @Primary
    @Bean(name = "mainDataSource")
    public DataSource mainDataSource() throws Exception {
        return ().getDataSource("cavedemo");
    }

When you run the project, you find that you can connect to the database and do not restart the project, you can dynamically modify the database configuration and automatically reconnect.

Project code:

/cnscud/xpower/tree/main/xpower-main/src/main/java/com/cnscud/xpower/dbn

The ConfigCenter used is also in this project, and can also be implemented by yourself, so you can get rid of this project.

This article comes from Blog Park, author:Flying Clouds ~ Valley of Wind, please indicate the original link when reprinting:/cnscud/p/

This is the article about MyBatis using Zookeeper to save database configuration and dynamic refresh. This is the end of this article. For more related MyBatis using Zookeeper to save database configuration and dynamic refresh content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!