精华内容
下载资源
问答
  • datasource
    千次阅读
    2021-09-15 19:13:36

    概述

            DataSource 翻译过来为数据源,它是 jdk 提供的一个接口,然后只提供了两个 getConnection 方法,分别是无参数和需要传入用户名和密码。所以说它是跟数据库连接有关的东西,可以通过它来获取数据库连接。

    public interface DataSource  extends CommonDataSource, Wrapper {
    
      /**
       * <p>Attempts to establish a connection with the data source that
       * this {@code DataSource} object represents.
       *
       * @return  a connection to the data source
       * @exception SQLException if a database access error occurs
       * @throws java.sql.SQLTimeoutException  when the driver has determined that the
       * timeout value specified by the {@code setLoginTimeout} method
       * has been exceeded and has at least tried to cancel the
       * current database connection attempt
       */
      Connection getConnection() throws SQLException;
    
      /**
       * <p>Attempts to establish a connection with the data source that
       * this {@code DataSource} object represents.
       *
       * @param username the database user on whose behalf the connection is
       *  being made
       * @param password the user's password
       * @return  a connection to the data source
       * @exception SQLException if a database access error occurs
       * @throws java.sql.SQLTimeoutException  when the driver has determined that the
       * timeout value specified by the {@code setLoginTimeout} method
       * has been exceeded and has at least tried to cancel the
       * current database connection attempt
       * @since 1.4
       */
      Connection getConnection(String username, String password)
        throws SQLException;
    }

            但是这只是一个接口,具体怎么获取到数据库连接,还是由实现它的子类来决定。本文就是来讲一下 DruidDataSource, 为什么讲它呢?因为它是阿里写出来的,比较 diao。

    DruidDataSource

            DruidDataSource 是阿里写出来的一个数据源, 它不仅可以获取数据库连接,还把这些数据库连接管理了起来,也就是所谓的数据库连接池。这样的话,当通过该数据源获取数据库连接的时候,如果数据库连接池里有可以使用的连接,那么就直接返回该连接,就省的每次获取数据库连接都要创建了。

            首先看一下怎么使用 DruidDataSource。

    // 配置一个 DruidDataSource 实例 bean 交由 Spring 容器管理
    @Configuration
    public class DataSourceConfig {
        @Bean
        public DataSource dataSource() {
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setUsername("root");
            dataSource.setPassword("123456");
            dataSource.setUrl("jdbc:mysql://localhost:3306/transaction?useSSL=false");
            dataSource.setDriverClassName("com.mysql.jdbc.Driver");
            //连接池最小空闲的连接数
            dataSource.setMinIdle(5);
            //连接池最大活跃的连接数
            dataSource.setMaxActive(20);
            //初始化连接池时创建的连接数
            dataSource.setInitialSize(10);
            return dataSource;
        }
    }
    
    //测试类
    public class DataSourceMain {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(DataSourceConfig.class);
            //获取 dataSource 实例
            DataSource dataSource = (DataSource) applicationContext.getBean("dataSource");
            try {
                //通过数据源获取数据库连接
                Connection connection = dataSource.getConnection();
                Statement statement = connection.createStatement();
                statement.execute("update AccountInfo set balance = balance + 1 where id = 1");
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
    }

            从上可以看出,通过数据源访问跟 jdbc 方式相比,省略了实例化数据库连接驱动 Driver 驱动这一步,此外调用 getConnection() 获取连接的时候,并没有传用户名和密码,说明 dataSource 把这些配置信息都管理起来了。后面获取连接的逻辑会自给自足。

            总结来说,DruidDataSource 管理了数据库连接的一些配置信息,还帮助我们创建了连接驱动 Driver, 剩下的逻辑就跟 jdbc 一毛一样了。管理配置信息就不说了,无非就是定义一些变量,然后通过 set 方法给写进去。接下来我们开一下什么时候创建的 Driver, 以及怎么获取数据库连接的。

            首先进入 DruidDataSource 类的 getConnection() 方法

        public DruidPooledConnection getConnection() throws SQLException {
            return getConnection(maxWait);
        }
    
        //这个参数代表超时时间,过了这个时间还没有获取到连接就代表获取连接失败。
        public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
            //初始化方法
            init();
    
            if (filters.size() > 0) {
                FilterChainImpl filterChain = new FilterChainImpl(this);
                return filterChain.dataSource_connect(this, maxWaitMillis);
            } else {
                return getConnectionDirect(maxWaitMillis);
            }
        }

            这里主要有两个方法,第一个是初始化方法,第二个是 getConnectionDirect(maxWaitMillis),很明显第二个是获取连接的,那么第一个是干什么的?

        public void init() throws SQLException {
            // inited 控制该初始化方法只调用一次,在方法后面会设置为 true,再次调用该方法就会直接返回。
            if (inited) {
                return;
            }
    
            // bug fixed for dead lock, for issue #2980
            //这一行是为了解决死锁问题,https://github.com/alibaba/druid/issues/2980 详情可以去这个连接看一下
            DruidDriver.getInstance();
    
            final ReentrantLock lock = this.lock;
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                throw new SQLException("interrupt", e);
            }
    
            boolean init = false;
            try {
                if (inited) {
                    return;
                }
    
                initStackTrace = Utils.toString(Thread.currentThread().getStackTrace());
    
                this.id = DruidDriver.createDataSourceId();
                if (this.id > 1) {
                    long delta = (this.id - 1) * 100000;
                    this.connectionIdSeedUpdater.addAndGet(this, delta);
                    this.statementIdSeedUpdater.addAndGet(this, delta);
                    this.resultSetIdSeedUpdater.addAndGet(this, delta);
                    this.transactionIdSeedUpdater.addAndGet(this, delta);
                }
    
                if (this.jdbcUrl != null) {
                    this.jdbcUrl = this.jdbcUrl.trim();
                    initFromWrapDriverUrl();
                }
    
                for (Filter filter : filters) {
                    filter.init(this);
                }
    
                if (this.dbType == null || this.dbType.length() == 0) {
                    this.dbType = JdbcUtils.getDbType(jdbcUrl, null);
                }
    
                if (JdbcConstants.MYSQL.equals(this.dbType)
                        || JdbcConstants.MARIADB.equals(this.dbType)
                        || JdbcConstants.ALIYUN_ADS.equals(this.dbType)) {
                    boolean cacheServerConfigurationSet = false;
                    if (this.connectProperties.containsKey("cacheServerConfiguration")) {
                        cacheServerConfigurationSet = true;
                    } else if (this.jdbcUrl.indexOf("cacheServerConfiguration") != -1) {
                        cacheServerConfigurationSet = true;
                    }
                    if (cacheServerConfigurationSet) {
                        this.connectProperties.put("cacheServerConfiguration", "true");
                    }
                }
    
                if (maxActive <= 0) {
                    throw new IllegalArgumentException("illegal maxActive " + maxActive);
                }
    
                if (maxActive < minIdle) {
                    throw new IllegalArgumentException("illegal maxActive " + maxActive);
                }
    
                if (getInitialSize() > maxActive) {
                    throw new IllegalArgumentException("illegal initialSize " + this.initialSize + ", maxActive " + maxActive);
                }
    
                if (timeBetweenLogStatsMillis > 0 && useGlobalDataSourceStat) {
                    throw new IllegalArgumentException("timeBetweenLogStatsMillis not support useGlobalDataSourceStat=true");
                }
    
                if (maxEvictableIdleTimeMillis < minEvictableIdleTimeMillis) {
                    throw new SQLException("maxEvictableIdleTimeMillis must be grater than minEvictableIdleTimeMillis");
                }
    
                if (this.driverClass != null) {
                    this.driverClass = driverClass.trim();
                }
    
                initFromSPIServiceLoader();
                //此时 driver 还没有初始化
                if (this.driver == null) {
                    if (this.driverClass == null || this.driverClass.isEmpty()) {
                        this.driverClass = JdbcUtils.getDriverClassName(this.jdbcUrl);
                    }
    
                    if (MockDriver.class.getName().equals(driverClass)) {
                        driver = MockDriver.instance;
                    } else {
                        if (jdbcUrl == null && (driverClass == null || driverClass.length() == 0)) {
                            throw new SQLException("url not set");
                        }
                        // 会调用这个方法获取 driver 实例
                        driver = JdbcUtils.createDriver(driverClassLoader, driverClass);
                    }
                } else {
                    if (this.driverClass == null) {
                        this.driverClass = driver.getClass().getName();
                    }
                }
    
                initCheck();
    
                initExceptionSorter();
                initValidConnectionChecker();
                validationQueryCheck();
    
                if (isUseGlobalDataSourceStat()) {
                    dataSourceStat = JdbcDataSourceStat.getGlobal();
                    if (dataSourceStat == null) {
                        dataSourceStat = new JdbcDataSourceStat("Global", "Global", this.dbType);
                        JdbcDataSourceStat.setGlobal(dataSourceStat);
                    }
                    if (dataSourceStat.getDbType() == null) {
                        dataSourceStat.setDbType(this.dbType);
                    }
                } else {
                    dataSourceStat = new JdbcDataSourceStat(this.name, this.jdbcUrl, this.dbType, this.connectProperties);
                }
                dataSourceStat.setResetStatEnable(this.resetStatEnable);
                
                //初始化数据库连接池,其实就是个数组
                connections = new DruidConnectionHolder[maxActive];
                evictConnections = new DruidConnectionHolder[maxActive];
                keepAliveConnections = new DruidConnectionHolder[maxActive];
    
                SQLException connectError = null;
    
                if (createScheduler != null && asyncInit) {
                    for (int i = 0; i < initialSize; ++i) {
                        submitCreateTask(true);
                    }
                } else if (!asyncInit) {
                    // init connections
                    // 这个就是通过配置设置的初始化的连接的个数
                    while (poolingCount < initialSize) {
                        try {
                            // 获取数据库连接
                            PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
                            DruidConnectionHolder holder = new DruidConnectionHolder(this, pyConnectInfo);
                            //将获取到的数据库连接放入到连接池中,也就是 connections 数组中
                            connections[poolingCount++] = holder;
                        } catch (SQLException ex) {
                            LOG.error("init datasource error, url: " + this.getUrl(), ex);
                            if (initExceptionThrow) {
                                connectError = ex;
                                break;
                            } else {
                                Thread.sleep(3000);
                            }
                        }
                    }
    
                    if (poolingCount > 0) {
                        poolingPeak = poolingCount;
                        poolingPeakTime = System.currentTimeMillis();
                    }
                }
    
                createAndLogThread();
                //创建一个线程负责创建新的数据库连接
                createAndStartCreatorThread();
                createAndStartDestroyThread();
    
                initedLatch.await();
                init = true;
    
                initedTime = new Date();
                registerMbean();
    
                if (connectError != null && poolingCount == 0) {
                    throw connectError;
                }
    
                if (keepAlive) {
                    // async fill to minIdle
                    if (createScheduler != null) {
                        for (int i = 0; i < minIdle; ++i) {
                            submitCreateTask(true);
                        }
                    } else {
                        this.emptySignal();
                    }
                }
    
            } catch (SQLException e) {
                LOG.error("{dataSource-" + this.getID() + "} init error", e);
                throw e;
            } catch (InterruptedException e) {
                throw new SQLException(e.getMessage(), e);
            } catch (RuntimeException e){
                LOG.error("{dataSource-" + this.getID() + "} init error", e);
                throw e;
            } catch (Error e){
                LOG.error("{dataSource-" + this.getID() + "} init error", e);
                throw e;
    
            } finally {
                // 设置 inited 为 true,当再次调用 init() 方法的时候就会直接返回。
                inited = true;
                lock.unlock();
    
                if (init && LOG.isInfoEnabled()) {
                    String msg = "{dataSource-" + this.getID();
    
                    if (this.name != null && !this.name.isEmpty()) {
                        msg += ",";
                        msg += this.name;
                    }
    
                    msg += "} inited";
    
                    LOG.info(msg);
                }
            }
        }

            上面的代码量很多,但是我们目前先把目光放到主线代码中,那么很明显有几个重要的点

    • 获取 driver 实例 driver = JdbcUtils.createDriver(driverClassLoader, driverClass);
    • 获取数据库连接 PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
    • 此外我们还知道了所谓连接池就是一个数组,它保存着一个个连接对象实例。

            首先看下如何获取 driver

     public static Driver createDriver(ClassLoader classLoader, String driverClassName) throws SQLException {
            Class<?> clazz = null;
            if (classLoader != null) {
                try {
                    clazz = classLoader.loadClass(driverClassName);
                } catch (ClassNotFoundException e) {
                    // skip
                }
            }
    
            if (clazz == null) {
                try {
                    ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
                    if (contextLoader != null) {
                        clazz = contextLoader.loadClass(driverClassName);
                    }
                } catch (ClassNotFoundException e) {
                    // skip
                }
            }
    
            if (clazz == null) {
                try {
                    clazz = Class.forName(driverClassName);
                } catch (ClassNotFoundException e) {
                    throw new SQLException(e.getMessage(), e);
                }
            }
    
            try {
                //通过反射获取实例,其实也就是 jdbc 获取 driver
                return (Driver) clazz.newInstance();
            } catch (IllegalAccessException e) {
                throw new SQLException(e.getMessage(), e);
            } catch (InstantiationException e) {
                throw new SQLException(e.getMessage(), e);
            }
        }

                可以看出,它跟使用 jdbc 时一样,都是通过反射获取到 driver 实例,然后我们再看一下如何获取一个连接

    public PhysicalConnectionInfo createPhysicalConnection() throws SQLException {
            String url = this.getUrl();
            Properties connectProperties = getConnectProperties();
    
            String user;
            if (getUserCallback() != null) {
                user = getUserCallback().getName();
            } else {
                user = getUsername();
            }
    
            String password = getPassword();
            PasswordCallback passwordCallback = getPasswordCallback();
    
            if (passwordCallback != null) {
                if (passwordCallback instanceof DruidPasswordCallback) {
                    DruidPasswordCallback druidPasswordCallback = (DruidPasswordCallback) passwordCallback;
    
                    druidPasswordCallback.setUrl(url);
                    druidPasswordCallback.setProperties(connectProperties);
                }
    
                char[] chars = passwordCallback.getPassword();
                if (chars != null) {
                    password = new String(chars);
                }
            }
    
            Properties physicalConnectProperties = new Properties();
            if (connectProperties != null) {
                physicalConnectProperties.putAll(connectProperties);
            }
    
            if (user != null && user.length() != 0) {
                physicalConnectProperties.put("user", user);
            }
    
            if (password != null && password.length() != 0) {
                physicalConnectProperties.put("password", password);
            }
    
            Connection conn = null;
    
            long connectStartNanos = System.nanoTime();
            long connectedNanos, initedNanos, validatedNanos;
    
            Map<String, Object> variables = initVariants
                    ? new HashMap<String, Object>()
                    : null;
            Map<String, Object> globalVariables = initGlobalVariants
                    ? new HashMap<String, Object>()
                    : null;
    
            createStartNanosUpdater.set(this, connectStartNanos);
            creatingCountUpdater.incrementAndGet(this);
            try {
                // 这时physicalConnectProperties已经拼装好了配置信息,也获取到了url
                conn = createPhysicalConnection(url, physicalConnectProperties);
                connectedNanos = System.nanoTime();
    
                if (conn == null) {
                    throw new SQLException("connect error, url " + url + ", driverClass " + this.driverClass);
                }
    
                initPhysicalConnection(conn, variables, globalVariables);
                initedNanos = System.nanoTime();
    
                validateConnection(conn);
                validatedNanos = System.nanoTime();
    
                setFailContinuous(false);
                setCreateError(null);
            } catch (SQLException ex) {
                setCreateError(ex);
                JdbcUtils.close(conn);
                throw ex;
            } catch (RuntimeException ex) {
                setCreateError(ex);
                JdbcUtils.close(conn);
                throw ex;
            } catch (Error ex) {
                createErrorCountUpdater.incrementAndGet(this);
                setCreateError(ex);
                JdbcUtils.close(conn);
                throw ex;
            } finally {
                long nano = System.nanoTime() - connectStartNanos;
                createTimespan += nano;
                creatingCountUpdater.decrementAndGet(this);
            }
    
            return new PhysicalConnectionInfo(conn, connectStartNanos, connectedNanos, initedNanos, validatedNanos, variables, globalVariables);
        }
        public Connection createPhysicalConnection(String url, Properties info) throws SQLException {
            Connection conn;
            if (getProxyFilters().size() == 0) {
                // 这里是获取到我们刚刚实例化的 driver, 然后再调用它的 connect 方法
                conn = getDriver().connect(url, info);
            } else {
                conn = new FilterChainImpl(this).connection_connect(info);
            }
    
            createCountUpdater.incrementAndGet(this);
    
            return conn;
        }

            从上可以看出,获取数据库连接首先也是要获取到 driver 然后再调用它的 connect 方法。

            总的来说,上面代码的主线逻辑就是先实例化 driver, 然后通过 driver 获取一个数据库连接,这其实也就是 jdbc 的方式,只不过别让帮我们做了这件事情,我们不用再自己做了而已。并且DruidDataSource 在获取到连接之后,还会把这个连接存起来,以免每次请求连接都要重新从数据库获取一个连接。

            上一部分讲的是初始化方法 init(), 别忘了我们是讲 DruidDataSource 获取数据库连接方法的,如下

        public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
            // 执行初始化方法后,这时连接池里已经有了数据库连接了
            init();
    
            if (filters.size() > 0) {
                FilterChainImpl filterChain = new FilterChainImpl(this);
                return filterChain.dataSource_connect(this, maxWaitMillis);
            } else {
                return getConnectionDirect(maxWaitMillis);
            }
        }

            我们需要看一下 getConnectionDirect(maxWaitMillis) 是怎么获取连接的

        public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
            int notFullTimeoutRetryCnt = 0;
            for (;;) {
                // handle notFullTimeoutRetry
                DruidPooledConnection poolableConnection;
                try {
                    //很明显这个方法就是获取数据库连接的, 剩下的代码可以先不看,直接跳到getConnectionInternal
                    poolableConnection = getConnectionInternal(maxWaitMillis);
                } catch (GetConnectionTimeoutException ex) {
                    if (notFullTimeoutRetryCnt <= this.notFullTimeoutRetryCount && !isFull()) {
                        notFullTimeoutRetryCnt++;
                        if (LOG.isWarnEnabled()) {
                            LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);
                        }
                        continue;
                    }
                    throw ex;
                }
    
                。。。。。。。。。。。//省略下面的代码
    private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException {
    
                   。。。。。。。。。。//省略该方法前面的一些代码
    
    
                    // 等待超时时间,默认是 -1
                    if (maxWait > 0) {
                        holder = pollLast(nanos);
                    } else {
                        holder = takeLast();
                    }
    
                    if (holder != null) {
                        activeCount++;
                        if (activeCount > activePeak) {
                            activePeak = activeCount;
                            activePeakTime = System.currentTimeMillis();
                        }
                    }
                } catch (InterruptedException e) {
                    connectErrorCountUpdater.incrementAndGet(this);
                    throw new SQLException(e.getMessage(), e);
                } catch (SQLException e) {
                    connectErrorCountUpdater.incrementAndGet(this);
                    throw e;
                } finally {
                    lock.unlock();
                }
    
                break;
            }
    
            
            DruidPooledConnection poolalbeConnection = new DruidPooledConnection(holder);
            return poolalbeConnection;
        }

            我们在代码里看到 pollLast 和 takeLast 是获取数据库连接的,这两个方法差不多,pollLast()会加入时间的判断,但是核心逻辑都是一样的,所以我们跟一下默认的 takeLast 方法

    DruidConnectionHolder takeLast() throws InterruptedException, SQLException {
            try {
                //如果没有设置连接池初始化大小,则会重新创建一个连接放入连接池, 如果设置了初始化大小,则在调用init()方法的时候会创建一些连接放在连接池中保存。
                while (poolingCount == 0) {
                    emptySignal(); // send signal to CreateThread create connection
    
                    if (failFast && isFailContinuous()) {
                        throw new DataSourceNotAvailableException(createError);
                    }
    
                    notEmptyWaitThreadCount++;
                    if (notEmptyWaitThreadCount > notEmptyWaitThreadPeak) {
                        notEmptyWaitThreadPeak = notEmptyWaitThreadCount;
                    }
                    try {
                        notEmpty.await(); // signal by recycle or creator
                    } finally {
                        notEmptyWaitThreadCount--;
                    }
                    notEmptyWaitCount++;
    
                    if (!enable) {
                        connectErrorCountUpdater.incrementAndGet(this);
                        if (disableException != null) {
                            throw disableException;
                        }
    
                        throw new DataSourceDisableException();
                    }
                }
            } catch (InterruptedException ie) {
                notEmpty.signal(); // propagate to non-interrupted thread
                notEmptySignalCount++;
                throw ie;
            }
    
            //将数据库连接池中的连接数减一
            decrementPoolingCount();
            //获取数据库连接池中的的一个连接
            DruidConnectionHolder last = connections[poolingCount];
            connections[poolingCount] = null;
    
            return last;
        }

            通过上面的分析,我们应该已经知道,当调用 Connection connection = dataSource.getConnection() 的时候,如果是第一次调用,则会在 init() 方法的时候创建一些连接放入数据库连接池里,然后获取连接的时候,直接从连接池中获取。

    总结

            DruidDataSource 博大精深,我只是分析了其中一些基本的功能,还有很多代码没有分析,但是本文的主要目的是为了让大家了解 DataSource 是个啥玩意,我们现在应该知道了它就是管理数据库连接的,如果需要一个连接的话问他要就行了。

    更多相关内容
  • 赠送jar包:dynamic-datasource-spring-boot-starter-3.4.1.jar; 赠送原API文档:dynamic-datasource-spring-boot-starter-3.4.1-javadoc.jar; 赠送源代码:dynamic-datasource-spring-boot-starter-3.4.1-sources...
  • 赠送jar包:dynamic-datasource-spring-boot-starter-3.4.1.jar; 赠送原API文档:dynamic-datasource-spring-boot-starter-3.4.1-javadoc.jar; 赠送源代码:dynamic-datasource-spring-boot-starter-3.4.1-sources...
  • 该项目采用标签形式对Datasource进行注入将Datasource组件交给容器进行统一管理
  • java SpringMVC动态数据源的DataSource工具类,springmvc中配置数据源为工具类中的数据源,多线程使用不同数据源配置
  • 可以帮助你了解,以及配置实现DruidDataSource数据源的配置
  • mybatis报错Error querying database. Cause: com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException: dynamic-datasource can not find primary datasource解决方案

    mybatis报错Error querying database. Cause: com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException: dynamic-datasource can not find primary datasource解决方案

    问题背景

    因为之前mybatis使用了多源数据,引入了dynamic-datasource-spring-boot-starter依赖,导致报错

    Creating a new SqlSession
    SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@72eb6200] was not registered for synchronization because synchronization is not active
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@72eb6200]
    
    org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: 
    ### Error querying database.  Cause: com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException: dynamic-datasource can not find primary datasource
    ### The error may exist in file [D:\code\csdn\mybatis-plus\target\classes\mapper\UserMapper.xml]
    ### The error may involve com.yg.mybatisplus.mapper.UserMapper.selectUserById
    ### The error occurred while executing a query
    ### Cause: com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException: dynamic-datasource can not find primary datasource
    
    	at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:96)
    	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:441)
    	at com.sun.proxy.$Proxy65.selectOne(Unknown Source)
    	at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:160)
    	at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:89)
    	at com.baomidou.mybatisplus.core.override.MybatisMapperProxy$PlainMethodInvoker.invoke(MybatisMapperProxy.java:148)
    	at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:89)
    	at com.sun.proxy.$Proxy66.selectUserById(Unknown Source)
    	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    	at java.lang.reflect.Method.invoke(Method.java:498)
    	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
    	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
    	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
    	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
    	at com.sun.proxy.$Proxy67.selectUserById(Unknown Source)
    	at com.yg.mybatisplus.MybatisPlusTest.testSelectUserById(MybatisPlusTest.java:139)
    	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    	at java.lang.reflect.Method.invoke(Method.java:498)
    	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
    	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
    	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
    	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
    	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
    	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
    	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
    	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
    	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
    	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
    	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
    	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
    	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    	at java.util.ArrayList.forEach(ArrayList.java:1257)
    	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    	at java.util.ArrayList.forEach(ArrayList.java:1257)
    	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
    	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
    	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
    	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
    	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
    	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
    	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
    	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
    	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
    	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
    	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
    	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
    	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:221)
    	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
    Caused by: org.apache.ibatis.exceptions.PersistenceException: 
    ### Error querying database.  Cause: com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException: dynamic-datasource can not find primary datasource
    ### The error may exist in file [D:\code\csdn\mybatis-plus\target\classes\mapper\UserMapper.xml]
    ### The error may involve com.yg.mybatisplus.mapper.UserMapper.selectUserById
    ### The error occurred while executing a query
    ### Cause: com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException: dynamic-datasource can not find primary datasource
    	at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
    	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:153)
    	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:145)
    	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
    	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:76)
    	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    	at java.lang.reflect.Method.invoke(Method.java:498)
    	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:427)
    	... 85 more
    Caused by: com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException: dynamic-datasource can not find primary datasource
    	at com.baomidou.dynamic.datasource.DynamicRoutingDataSource.determinePrimaryDataSource(DynamicRoutingDataSource.java:91)
    	at com.baomidou.dynamic.datasource.DynamicRoutingDataSource.getDataSource(DynamicRoutingDataSource.java:120)
    	at com.baomidou.dynamic.datasource.DynamicRoutingDataSource.determineDataSource(DynamicRoutingDataSource.java:78)
    	at com.baomidou.dynamic.datasource.ds.AbstractRoutingDataSource.getConnection(AbstractRoutingDataSource.java:48)
    	at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:159)
    	at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:117)
    	at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:80)
    	at org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:80)
    	at org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction.java:67)
    	at org.apache.ibatis.executor.BaseExecutor.getConnection(BaseExecutor.java:337)
    	at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:86)
    	at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:62)
    	at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:325)
    	at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
    	at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
    	at com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor.intercept(MybatisPlusInterceptor.java:81)
    	at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:62)
    	at com.sun.proxy.$Proxy100.query(Unknown Source)
    	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:151)
    	... 93 more
    

    解决方案

    删除掉dynamic-datasource-spring-boot-starter依赖,或者在application中设置为多源数据

    <!--        <dependency>-->
    <!--            <groupId>com.baomidou</groupId>-->
    <!--            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>-->
    <!--            <version>3.5.0</version>-->
    <!--        </dependency>-->
    




    作为程序员第 160 篇文章,每次写一句歌词记录一下,看看人生有几首歌的时间,wahahaha …

    Lyric: 脑袋瓜有一点秀逗

    展开全文
  • Java DataSource对象

    千次阅读 2020-11-29 09:41:34
    本节介绍DataSource对象,这是获得与数据源的连接的首选方法。除了它们的其他优点(将在后面解释)之外,DataSource对象还可以提供连接池和分布式事务。此功能对于企业数据库计算至关重要。特别是,它是Enterprise ...

     

    连接数据源对象

    本节介绍DataSource对象,这是获得与数据源的连接的首选方法。除了它们的其他优点(将在后面解释)之外,DataSource对象还可以提供连接池和分布式事务。此功能对于企业数据库计算至关重要。特别是,它是Enterprise JavaBeans(EJB)技术不可或缺的。

    本节向您展示如何使用该DataSource接口获得连接以及如何使用分布式事务和连接池。两者都涉及JDBC应用程序中很少的代码更改。

    系统管理员通常使用工具(例如Apache Tomcat或Oracle WebLogic Server)来部署使这些操作成为可能的类而执行的工作因要部署的DataSource对象类型而异。因此,本节的大部分内容专门介绍系统管理员如何设置环境,以便程序员可以使用DataSource对象来获得连接。

    涵盖以下主题:

    使用数据源对象获取连接

    建立连接中,您学习了如何使用DriverManager该类来获得连接。本节说明如何使用DataSource对象来建立与数据源的连接,这是首选方法。

    由实现的类实例化的对象DataSource表示特定的DBMS或某些其他数据源,例如文件。甲DataSource对象表示一个特定的DBMS或一些其它数据源,诸如一个文件。如果公司使用多个数据源,它将DataSource为每个数据源部署一个单独的对象。该DataSource接口由驱动程序供应商实现。它可以通过三种不同的方式实现:

    • 基本DataSource实现产生的标准Connection对象没有在分布式事务中合并或使用。
    • DataSource实现,它支持连接池产生Connection参与连接池,即,可循环使用的连接对象。
    • DataSource那分布式支持事务执行产生Connection,可以在分布式事务,即,访问两个或多个DBMS服务器事务中使用的对象。

    JDBC驱动程序应至少包括一个基本DataSource实现。例如,Java DB JDBC驱动程序包括org.apache.derby.jdbc.ClientDataSourceMySQL的实现和com.mysql.jdbc.jdbc2.optional.MysqlDataSource。如果您的客户端在Java 8 Compact Profile 2上运行,则Java DB JDBC驱动程序为org.apache.derby.jdbc.BasicClientDataSource40。本教程的示例要求压缩配置文件3或更高。

    DataSource类是支持分布式事务通常也实现了连接池支持。例如,DataSourceEJB供应商提供的类几乎总是支持连接池和分布式事务。

    假设从前面的示例来看,The Coffee Break商店蓬勃发展的连锁店的所有者已决定通过在Internet上出售咖啡来进一步扩大规模。预期会有大量的在线业务,因此所有者肯定需要连接池。打开和关闭连接会涉及大量开销,并且所有者希望此在线订购系统需要大量的查询和更新。使用连接池,可以反复使用连接池,从而避免了为每次数据库访问创建新连接的开销。此外,所有者现在拥有第二个DBMS,其中包含最近收购的咖啡烘焙公司的数据。这意味着所有者将希望能够编写使用旧DBMS服务器和新DBMS服务器的分布式事务。

    链所有者已重新配置计算机系统,以服务于新的更大的客户群。所有者购买了最新的JDBC驱动程序和与其一起使用的EJB应用程序服务器,以便能够使用分布式事务并获得连接池带来的更高性能。提供了许多与最近购买的EJB服务器兼容的JDBC驱动程序。所有者现在具有三层体系结构,中间层是新的EJB应用程序服务器和JDBC驱动程序,第三层是两个DBMS服务器。发出请求的客户端计算机是第一层。

    部署基本数据源对象

    系统管理员需要部署DataSource对象,以便Coffee Break的编程团队可以开始使用它们。部署DataSource对象包括三个任务:

    1. 创建DataSource类的实例
    2. 设置其属性
    3. 在使用Java命名和目录接口(JNDI)API的命名服务中注册它

    首先,考虑最基本的情况,即使用DataSource接口的基本实现,即不支持连接池或分布式事务的接口。在这种情况下,仅DataSource需要部署一个对象。基本的实现DataSource产生与类产生的相同类型的连接DriverManager

    创建数据源类的实例并设置其属性

    假设一家只希望基本实现的DataSource公司从JDBC供应商DB Access,Inc.购买了一个驱动程序。该驱动程序包括com.dbaccess.BasicDataSource实现该DataSource接口的类。以下代码摘录创建该类的实例BasicDataSource并设置其属性。BasicDataSource部署完实例之后,程序员可以调用该方法DataSource.getConnection来获取与公司数据库的连接CUSTOMER_ACCOUNTS。首先,系统管理员使用默认构造函数创建BasicDataSource对象ds。然后,系统管理员设置三个属性。请注意,以下代码通常由部署工具执行:

    com.dbaccess.BasicDataSource ds = new com.dbaccess.BasicDataSource();
    ds.setServerName("grinder");
    ds.setDatabaseName("CUSTOMER_ACCOUNTS");
    ds.setDescription("Customer accounts database for billing");

    ds现在,该变量表示CUSTOMER_ACCOUNTS服务器上安装的数据库。该BasicDataSource对象产生的任何连接ds都将是与数据库的连接CUSTOMER_ACCOUNTS

    向使用JNDI API的命名服务注册数据源对象

    通过设置属性,系统管理员可以向BasicDataSourceJNDI(Java命名和目录接口)命名服务注册对象。通常使用的特定命名服务由系统属性确定,此处未显示。以下代码摘录注册该BasicDataSource对象并将其与逻辑名绑定jdbc/billingDB

    Context ctx = new InitialContext();
    ctx.bind("jdbc/billingDB", ds);

    此代码使用JNDI API。第一行创建一个InitialContext对象,该对象用作名称的起点,类似于文件系统中的根目录。第二行将BasicDataSource对象关联或绑定ds到逻辑名jdbc/billingDB。在下一个代码摘录中,为命名服务赋予此逻辑名,然后它返回BasicDataSource对象。逻辑名称可以是任何字符串。在这种情况下,公司决定使用该名称billingDB作为CUSTOMER_ACCOUNTS数据库的逻辑名称。

    在前面的示例中,jdbc是初始上下文下的子上下文,就像根目录下的目录是子目录一样。该名称jdbc/billingDB类似于路径名,其中路径中的最后一项类似于文件名。在这种情况下,billingDB是赋予BasicDataSource对象的逻辑名ds。子上下文jdbc是保留给逻辑名绑定到DataSource对象的,因此jdbc它将始终是数据源逻辑名的第一部分。

    使用部署的数据源对象

    DataSource系统管理员部署了基本实现之后,程序员就可以使用它了。这意味着程序员可以提供绑定到DataSource类实例的逻辑数据源名称,并且JNDI命名服务将返回DataSource该类的实例。getConnection然后可以在该DataSource对象上调用该方法以获取与其表示的数据源的连接。例如,程序员可能编写以下两行代码来获取一个DataSource对象,该对象产生与数据库的连接CUSTOMER_ACCOUNTS

    Context ctx = new InitialContext();
    DataSource ds = (DataSource)ctx.lookup("jdbc/billingDB");

    代码的第一行以初始上下文为检索DataSource对象的起点。在jdbc/billingDB为方法提供逻辑名时lookup,该方法将返回DataSource系统管理员jdbc/billingDB在部署时绑定到的对象。因为该方法的返回值lookup是Java Object,所以DataSource在将其分配给变量之前,必须将其转换为更特定的类型ds

    变量dscom.dbaccess.BasicDataSource实现DataSource接口的类的实例。调用该方法将ds.getConnection产生与CUSTOMER_ACCOUNTS数据库的连接。

    Connection con = ds.getConnection("fernanda","brewed");

    getConnection方法仅需要用户名和密码,因为该变量在其属性中ds具有与CUSTOMER_ACCOUNTS数据库建立连接所需的其余信息,例如数据库名称和位置。

    数据源对象的优点

    由于其属性,DataSourceDriverManager用于连接的类相比,对象是更好的替代方法。程序员不再需要在其应用程序中对驱动程序名称或JDBC URL进行硬编码,从而使它们更易于移植。而且,DataSource属性使代码维护更加简单。如果进行了更改,则系统管理员可以更新数据源属性,而不必担心更改与该数据源建立连接的每个应用程序。例如,如果将数据源移动到其他服务器,则系统管理员要做的就是将serverName属性设置为新的服务器名称。

    除了可移植性和易于维护之外,使用DataSource对象进行连接还可以提供其他优点。当实现DataSource接口以与实现一起使用ConnectionPoolDataSource时,DataSource该类实例产生的所有连接将自动为池连接。类似地,当DataSource实现被实现为与XADataSource类一起使用时,它产生的所有连接将自动为可在分布式事务中使用的连接。下一节将说明如何部署这些类型的DataSource实现。

    部署其他数据源实现

    系统管理员或以该身份工作的其他人可以部署DataSource对象,以便它产生的连接为池连接。为此,他(她)首先部署一个ConnectionPoolDataSource对象,然后部署一个DataSource实现为可以使用的对象。ConnectionPoolDataSource设置对象的属性,使其代表将与其建立连接的数据源。将ConnectionPoolDataSource对象注册到JNDI命名服务后,将DataSource部署对象。通常,仅需为该DataSource对象设置两个属性:descriptiondataSourceName。赋予该dataSourceName属性的值是标识ConnectionPoolDataSource先前部署的对象的逻辑名,该逻辑名是包含进行连接所需的属性的对象。

    随着ConnectionPoolDataSourceDataSource对象部署,您可以调用该方法DataSource.getConnectionDataSource的对象,并得到一个连接池。此连接将连接到ConnectionPoolDataSource对象属性中指定的数据源。

    以下示例描述了The Coffee Break的系统管理员如何部署DataSource实现为提供池化连接的对象。系统管理员通常会使用部署工具,因此本节中显示的代码片段是部署工具将执行的代码。

    为了获得更好的性能,Coffee Break公司从DB Access,Inc.购买了JDBC驱动程序com.dbaccess.ConnectionPoolDS,其中包括实现该ConnectionPoolDataSource接口的class 。系统管理员创建创建此类的实例,设置其属性,并将其注册到JNDI命名服务。Coffee Break从其EJB服务器供应商Application Logic,Inc.购买了其DataSourcecom.applogic.PooledDataSource。该类com.applogic.PooledDataSource使用ConnectionPoolDataSource该类提供的基础支持来实现连接池com.dbaccess.ConnectionPoolDS

    ConnectionPoolDataSource对象必须首先部署。以下代码创建的实例com.dbaccess.ConnectionPoolDS并设置其属性:

    com.dbaccess.ConnectionPoolDS cpds = new com.dbaccess.ConnectionPoolDS();
    cpds.setServerName("creamer");
    cpds.setDatabaseName("COFFEEBREAK");
    cpds.setPortNumber(9040);
    cpds.setDescription("Connection pooling for " + "COFFEEBREAK DBMS");

    ConnectionPoolDataSource部署对象后,系统管理员将部署DataSource对象。以下代码向JNDI命名服务注册com.dbaccess.ConnectionPoolDS对象cpds。请注意,与cpds变量关联的逻辑名具有在子上下文pool下添加的子上下文jdbc,这类似于将子目录添加到分层文件系统中的另一个子目录。该类的任何实例的逻辑名称com.dbaccess.ConnectionPoolDS始终以开头jdbc/pool。Oracle建议将所有ConnectionPoolDataSource对象放在子上下文下jdbc/pool

    Context ctx = new InitialContext();
    ctx.bind("jdbc/pool/fastCoffeeDB", cpds);

    接下来,部署DataSource实现为与cpds变量和com.dbaccess.ConnectionPoolDS该类的其他实例交互的类。以下代码创建此类的实例并设置其属性。请注意,仅为此实例设置了两个属性com.applogic.PooledDataSourcedescription设置该属性是因为它始终是必需的。设置的另一个属性dataSourceName给出了的逻辑JNDI名称cpds,它是com.dbaccess.ConnectionPoolDS该类的实例。换句话说,cpds表示ConnectionPoolDataSource将为对象实现连接池的DataSource对象。

    以下代码(可能由部署工具执行)创建一个PooledDataSource对象,设置其属性,并将其绑定到逻辑名称jdbc/fastCoffeeDB

    com.applogic.PooledDataSource ds = new com.applogic.PooledDataSource();
    ds.setDescription("produces pooled connections to COFFEEBREAK");
    ds.setDataSourceName("jdbc/pool/fastCoffeeDB");
    Context ctx = new InitialContext();
    ctx.bind("jdbc/fastCoffeeDB", ds);

    此时,将DataSource部署一个对象,应用程序可以从该对象获得与数据库的池化连接COFFEEBREAK

    获取和使用池化连接

    一个连接池是数据库连接对象的缓存。这些对象表示物理数据库连接,应用程序可以使用这些物理数据库连接来连接数据库。在运行时,应用程序请求池中的连接。如果池包含可以满足请求的连接,则它将连接返回给应用程序。如果未找到任何连接,则会创建一个新的连接并返回到应用程序。应用程序使用该连接在数据库上执行某些工作,然后将对象返回到池中。然后,该连接可用于下一个连接请求。

    连接池可促进连接对象的重用,并减少创建连接对象的次数。连接池显着提高了数据库密集型应用程序的性能,因为创建连接对象在时间和资源上都非常昂贵。

    现在这些DataSourceConnectionPoolDataSource对象的部署,程序员可以使用DataSource对象来获取连接池。获取池化连接的代码与获取非池化连接的代码一样,如以下两行所示:

    ctx = new InitialContext();
    ds = (DataSource)ctx.lookup("jdbc/fastCoffeeDB");

    该变量ds表示一个DataSource对象,该对象产生与数据库的池化连接COFFEEBREAK。您只需要检索DataSource一次该对象,因为您可以使用它来产生所需的任意多个池连接。getConnectionds变量上调用方法会自动产生一个池化连接,因为DataSourceds变量代表的对象已配置为产生池化连接。

    连接池通常对程序员是透明的。使用池连接时,只需要做两件事:

    1. 使用DataSource对象而不是DriverManager类来获得连接。在下面的代码行中,ds是一个DataSource对象的实现和部署,以便它将创建池连接,username并且password是代表有权访问数据库的用户凭据的变量:

      Connection con = ds.getConnection(username, password);
    2. 使用finally语句关闭池化连接。在适用于使用池化连接的代码的代码块finally之后,将出现以下try/catch代码块:

      try {
          Connection con = ds.getConnection(username, password);
          // ... code to use the pooled
          // connection con
      } catch (Exception ex {
          // ... code to handle exceptions
      } finally {
          if (con != null) con.close();
      }

    否则,使用池连接的应用程序与使用常规连接的应用程序相同。应用程序程序员在完成连接池时可能会注意到的唯一另一件事是性能更好。

    以下示例代码获取一个DataSource对象,该对象产生与数据库的连接,COFFEEBREAK并使用它来更新表中的价格COFFEES

    import java.sql.*;
    import javax.sql.*;
    import javax.ejb.*;
    import javax.naming.*;
    
    public class ConnectionPoolingBean implements SessionBean {
    
        // ...
    
        public void ejbCreate() throws CreateException {
            ctx = new InitialContext();
            ds = (DataSource)ctx.lookup("jdbc/fastCoffeeDB");
        }
    
        public void updatePrice(float price, String cofName,
                                String username, String password)
            throws SQLException{
    
            Connection con;
            PreparedStatement pstmt;
            try {
                con = ds.getConnection(username, password);
                con.setAutoCommit(false);
                pstmt = con.prepareStatement("UPDATE COFFEES " +
                            "SET PRICE = ? " +
                            "WHERE COF_NAME = ?");
                pstmt.setFloat(1, price);
                pstmt.setString(2, cofName);
                pstmt.executeUpdate();
    
                con.commit();
                pstmt.close();
    
            } finally {
                if (con != null) con.close();
            }
        }
    
        private DataSource ds = null;
        private Context ctx = null;
    }

    此代码示例中的连接参与连接池,因为以下是正确的:

    • 一个类实现的实例ConnectionPoolDataSource已部署。
    • DataSource已经部署了一个类实现的实例,并且为其dataSourceName属性设置的值是绑定到先前部署的ConnectionPoolDataSource对象的逻辑名称。

    请注意,尽管此代码与您之前看到的代码非常相似,但在以下方面有所不同:

    • 它进口javax.sqljavax.ejbjavax.naming包除java.sql

      DataSourceConnectionPoolDataSource接口处于javax.sql封装,JNDI构造InitialContext和方法Context.lookup是的一部分javax.naming封装。此特定示例代码采用使用javax.ejb包中API的EJB组件的形式。该示例的目的是说明使用池化连接的方式与使用非池化连接的方式相同,因此您不必担心理解EJB API。

    • 它使用DataSource对象来获得连接,而不是使用DriverManager设施。

    • 它使用一个finally块来确保关闭连接。

    获取和使用池化连接类似于获取和使用常规连接。当某人充当系统管理员正确部署了一个ConnectionPoolDataSource对象和一个DataSource对象后,应用程序将使用该DataSource对象来获得池化连接。但是,应用程序应使用finally块来关闭池化连接。为简单起见,前面的示例使用一个finally块,但不使用任何catch块。如果该try块中的方法引发了异常,则默认情况下将引发该异常,finally无论如何该子句都将执行。

    部署分布式事务

    DataSource可以部署对象以获得可在分布式事务中使用的连接。与连接池一样,必须部署两个不同的类实例:一个XADataSource对象和一个DataSource实现为与之协同工作的对象。

    假设The Coffee Break企业家购买的EJB服务器包含DataSourcecom.applogic.TransactionalDSXADataSource该类与诸如com.dbaccess.XATransactionalDS。它可以与任何XADataSource类一起使用的事实使EJB服务器可以跨JDBC驱动程序移植。当DataSourceXADataSource物体的部署,产生的连接将能够参与分布式事务。在这种情况下,将com.applogic.TransactionalDS实现该类,以使生成的连接也成为池连接,对于DataSource作为EJB服务器实现的一部分提供的类,通常是这种情况。

    XADataSource对象必须首先部署。以下代码创建的实例com.dbaccess.XATransactionalDS并设置其属性:

    com.dbaccess.XATransactionalDS xads = new com.dbaccess.XATransactionalDS();
    xads.setServerName("creamer");
    xads.setDatabaseName("COFFEEBREAK");
    xads.setPortNumber(9040);
    xads.setDescription("Distributed transactions for COFFEEBREAK DBMS");

    以下代码向JNDI命名服务注册com.dbaccess.XATransactionalDS对象xads。请注意,与之关联的逻辑名称在下方添加xads了子上下文。Oracle建议类的任何实例的逻辑名称始终以开头。xajdbccom.dbaccess.XATransactionalDSjdbc/xa

    Context ctx = new InitialContext();
    ctx.bind("jdbc/xa/distCoffeeDB", xads);

    接下来,部署DataSource实现xads与其他XADataSource对象交互的对象。请注意,DataSourcecom.applogic.TransactionalDS可以与XADataSource任何JDBC驱动程序供应商的类一起使用。部署DataSource对象涉及创建com.applogic.TransactionalDS类的实例并设置其属性。该dataSourceName属性设置为jdbc/xa/distCoffeeDB,与关联的逻辑名称com.dbaccess.XATransactionalDS。这是实现XADataSource该类的分布式事务处理功能的DataSource类。以下代码部署DataSource该类的实例:

    com.applogic.TransactionalDS ds = new com.applogic.TransactionalDS();
    ds.setDescription("Produces distributed transaction " +
                      "connections to COFFEEBREAK");
    ds.setDataSourceName("jdbc/xa/distCoffeeDB");
    Context ctx = new InitialContext();
    ctx.bind("jdbc/distCoffeeDB", ds);

    既然类的实例com.applogic.TransactionalDS,并com.dbaccess.XATransactionalDS已经部署,应用程序可以调用该方法getConnection的实例TransactionalDS类来获取到的连接COFFEEBREAK可在分布式事务中使用的数据库。

    使用连接进行分布式事务

    要获得可用于分布式事务的连接,必须使用DataSource已正确实现和部署的对象,如“部署分布式事务”部分中所示。使用这样的DataSource对象,对其调用方法getConnection。建立连接后,请像使用其他任何连接一样使用它。由于jdbc/distCoffeesDB已与XADataSourceJNDI命名服务中的对象相关联,因此以下代码生成了Connection可在分布式事务中使用的对象:

    Context ctx = new InitialContext();
    DataSource ds = (DataSource)ctx.lookup("jdbc/distCoffeesDB");
    Connection con = ds.getConnection();

    对于此连接作为分布式事务的一部分时的使用方式,存在一些较小但重要的限制。事务管理器控制分布式事务何时开始以及何时提交或回滚。因此,应用程序代码绝不应调用方法Connection.commitConnection.rollback。应用程序同样不应调用Connection.setAutoCommit(true),它启用了自动提交模式,因为这也会干扰事务管理器对事务边界的控制。这说明了为什么在分布式事务范围内创建的新连接默认情况下会禁用其自动提交模式。请注意,这些限制仅在连接参与分布式事务时才适用。连接不是分布式事务的一部分时,没有任何限制。

    对于以下示例,假设已订购一份咖啡,这将触发对位于不同DBMS服务器上的两个表的更新。第一个表是一个新INVENTORY表,第二个COFFEES表是该表。因为这些表位于不同的DBMS服务器上,所以涉及它们的事务将是分布式事务。以下示例中的代码(该示例获得一个连接,更新该COFFEES表并关闭该连接)是分布式事务的第二部分。

    请注意,由于分布式事务的范围由中间层服务器的基础系统基础结构控制,因此代码不会明确地提交或回退更新。同样,假设用于分布式事务的连接是池化连接,则应用程序使用一个finally块来关闭该连接。这样可以保证即使抛出异常也将关闭有效的连接,从而确保将连接返回到连接池以进行回收。

    下面的代码示例说明了一个Enterprise Bean,它是一个实现可以由客户端计算机调用的方法的类。这个例子的目的是说明用于分布式事务应用程序代码是没有从其他代码不同,除了它不调用Connection方法commitrollbacksetAutoCommit(true)。因此,您不必担心了解所使用的EJB API。

    import java.sql.*;
    import javax.sql.*;
    import javax.ejb.*;
    import javax.naming.*;
    
    public class DistributedTransactionBean implements SessionBean {
    
        // ...
    
        public void ejbCreate() throws CreateException {
    
            ctx = new InitialContext();
            ds = (DataSource)ctx.lookup("jdbc/distCoffeesDB");
        }
    
        public void updateTotal(int incr, String cofName, String username,
                                String password)
            throws SQLException {
    
            Connection con;
            PreparedStatement pstmt;
    
            try {
                con = ds.getConnection(username, password);
                pstmt = con.prepareStatement("UPDATE COFFEES " +
                            "SET TOTAL = TOTAL + ? " +
                            "WHERE COF_NAME = ?");
                pstmt.setInt(1, incr);
                pstmt.setString(2, cofName);
                pstmt.executeUpdate();
                stmt.close();
            } finally {
                if (con != null) con.close();
            }
        }
    
        private DataSource ds = null;
        private Context ctx = null;
    }
    展开全文
  • dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。 github: https://github.com/baomidou/dynamic-datasource-spring-boot-starter 文档: ...

    目录

    一、简介

    二、源码分析

    2.1 整体结构

    2.2 自动配置怎么实现的

    2.3 如何集成众多连接池

    2.4 DS注解如何被拦截处理的

    2.5 多数据源动态切换及如何管理多数据源

    2.6 组数据源的负载均衡怎么实现的

    2.7 如何动态增减数据源

    三、总结


    一、简介

    dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。
    github: https://github.com/baomidou/dynamic-datasource-spring-boot-starter
    文档: https://github.com/baomidou/dynamic-datasource-spring-boot-starter/wiki

    它与mybatis-plus属于同一个生态圈,很容易集成mybatis-plus。

    特性:

    1. 数据源分组,适用于多种场景:纯粹多库,读写分离,一主多从,混合模式。
    2. 内置敏感参数加密和启动初始化表结构schema数据库database。
    3. 提供对Druid、Mybatis-Plus、P6sy和Jndi的快速集成。
    4. 简化Druid和HikariCp配置,提供全局参数配置。
    5. 提供自定义数据源来源接口(默认使用yml或properties配置)。
    6. 提供项目启动后增减数据源方案。
    7. 提供Mybatis环境下的纯读写分离方案。
    8. 使用spel动态参数解析数据源,如从session、header或参数中获取数据源。(多租户架构神器)
    9. 提供多层数据源嵌套切换。(ServiceA >>> ServiceB >>> ServiceC,每个Service都是不同的数据源)
    10. 提供“不使用注解而使用正则或 spel”来切换数据源方案(实验性功能)。
    11. 基于seata的分布式事务支持。
    无论是动态增减数据源、数据源分组,还是纯粹多库、读写分离、一主多从、从其他数据库或者配置中心读取数据源,比起Mapper分包方式或自定义AOP注解切片方式实现多数据源方案,使用dynamic-datasource-spring-boot-starter要便捷许多,极大简化了工作量。

    具体使用,请参考示例:

    springboot整合mybatis-plus、druid连接池和多数据源配置_WorldMvp的专栏-CSDN博客_druid连接池多数据源

    二、源码分析

    本文源码解析基于3.3.1版本。由于篇幅限制,只截了重点代码,如果需要看完整代码,可以去github拉取。

    2.1 整体结构

    整体结构如下图所示:

     拿到代码后,要找到入手点,带着问题阅读代码。

    2.2 自动配置怎么实现的

    一般情况下,一个starter的最好入手点就是自动配置类,在 META-INF/spring.factories文件中指定自动配置类入口

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration

    在spring.factories中,可以看到这个工程的自动配置类路径。从核心自动配置类DynamicDataSourceAutoConfiguration入手,可以认为这就是程序的Main入口。

    /**
     * 动态数据源核心自动配置类
     */
    @Slf4j
    @Configuration
    @AllArgsConstructor
    // 读取以spring.datasource.dynamic为前缀的配置
    @EnableConfigurationProperties(DynamicDataSourceProperties.class)
    // 需要在spring boot的DataSource bean自动配置之前注入我们的DataSource bean
    @AutoConfigureBefore(DataSourceAutoConfiguration.class)
    // 引入了Druid的autoConfig和各种数据源连接池的Creator
    @Import(value = {DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class})
    // 当含有spring.datasource.dynamic配置的时候,启用这个autoConfig
    @ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
    public class DynamicDataSourceAutoConfiguration {
    
        private final DynamicDataSourceProperties properties;
    
         /**
         * 多数据源加载接口,默认从yml中读取多数据源配置
         * @return DynamicDataSourceProvider
         */
        @Bean
        @ConditionalOnMissingBean
        public DynamicDataSourceProvider dynamicDataSourceProvider() {
            Map<String, DataSourceProperty> datasourceMap = properties.getDatasource();
            return new YmlDynamicDataSourceProvider(datasourceMap);
        }
    
        /**
         * 注册自己的动态多数据源DataSource
         * @param dynamicDataSourceProvider 各种数据源连接池创建者
         * @return DataSource
         */
        @Bean
        @ConditionalOnMissingBean
        public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
            DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
            dataSource.setPrimary(properties.getPrimary());
            dataSource.setStrict(properties.getStrict());
            dataSource.setStrategy(properties.getStrategy());
            dataSource.setProvider(dynamicDataSourceProvider);
            dataSource.setP6spy(properties.getP6spy());
            dataSource.setSeata(properties.getSeata());
            return dataSource;
        }
    
        /**
         * AOP切面,对DS注解过的方法进行增强,达到切换数据源的目的。
         * @param dsProcessor 动态参数解析数据源。如果数据源名称以#开头,就会进入这个解析器链。
         * @return advisor
         */
        @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
        @Bean
        @ConditionalOnMissingBean
        public DynamicDataSourceAnnotationAdvisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
            // aop方法拦截器在方法调用前后做操作,设置动态参数解析器
            DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(properties.isAllowedPublicOnly(), dsProcessor);
            // 使用AbstractPointcutAdvisor将pointcut和advice连接构成切面
            DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
            advisor.setOrder(properties.getOrder());
            return advisor;
        }
    
        /**
         * seata分布式事务支持
         *
         */
        @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
        @ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "seata", havingValue = "false", matchIfMissing = true)
        @Bean
        public Advisor dynamicTransactionAdvisor() {
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
            pointcut.setExpression("@annotation(com.baomidou.dynamic.datasource.annotation.DSTransactional)");
            return new DefaultPointcutAdvisor(pointcut, new DynamicTransactionAdvisor());
        }
    
        /**
         * 动态参数解析器链
         * @return DsProcessor
         */
        @Bean
        @ConditionalOnMissingBean
        public DsProcessor dsProcessor() {
            DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
            DsSessionProcessor sessionProcessor = new DsSessionProcessor();
            DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
            headerProcessor.setNextProcessor(sessionProcessor);
            sessionProcessor.setNextProcessor(spelExpressionProcessor);
            return headerProcessor;
        }
    
    }
    

    这里自动配置的五个Bean都是非常重要的。

    自动配置类的几个注解都写了注释,其中重要的是这个注解:

    // 读取以spring.datasource.dynamic为前缀的配置
    @EnableConfigurationProperties(DynamicDataSourceProperties.class)

    @EnableConfigurationProperties:使 @ConfigurationProperties 注解的类生效,主要是用来把properties或者yml配置文件转化为bean来使用,这个在实际使用中非常实用。

    @Slf4j
    @Getter
    @Setter
    @ConfigurationProperties(prefix = DynamicDataSourceProperties.PREFIX)
    public class DynamicDataSourceProperties {
    
        public static final String PREFIX = "spring.datasource.dynamic";
        public static final String HEALTH = PREFIX + ".health";
    
        /**
         * 必须设置默认的库,默认master
         */
        private String primary = "master";
        /**
         * 是否启用严格模式,默认不启动. 严格模式下未匹配到数据源直接报错, 非严格模式下则使用默认数据源primary所设置的数据源
         */
        private Boolean strict = false;
        /**
         * 是否使用p6spy输出,默认不输出
         */
        private Boolean p6spy = false;
        /**
         * 是否使用开启seata,默认不开启
         */
        private Boolean seata = false;
        /**
         * seata使用模式,默认AT
         */
        private SeataMode seataMode = SeataMode.AT;
        /**
         * 是否使用 spring actuator 监控检查,默认不检查
         */
        private boolean health = false;
        /**
         * 每一个数据源
         */
        private Map<String, DataSourceProperty> datasource = new LinkedHashMap<>();
        /**
         * 多数据源选择算法clazz,默认负载均衡算法
         */
        private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
        /**
         * aop切面顺序,默认优先级最高
         */
        private Integer order = Ordered.HIGHEST_PRECEDENCE;
        /**
         * Druid全局参数配置
         */
        @NestedConfigurationProperty
        private DruidConfig druid = new DruidConfig();
        /**
         * HikariCp全局参数配置
         */
        @NestedConfigurationProperty
        private HikariCpConfig hikari = new HikariCpConfig();
    
        /**
         * 全局默认publicKey
         */
        private String publicKey = CryptoUtils.DEFAULT_PUBLIC_KEY_STRING;
        /**
         * aop 切面是否只允许切 public 方法
         */
        private boolean allowedPublicOnly = true;
    }
    

    可以发现,我们在spring.datasource.dynamic配置的属性都会注入到这个配置Bean中。需要注意的是,使用了@NestedConfigurationProperty嵌套了其他的配置类。如果不清楚配置项是什么,看看DynamicDataSourceProperties这个类就清楚了。比如DruidConfig,这个DruidConfig是自定义的一个配置类,不是Druid里面的,它下面有个toProperties方法,为了实现yml配置中每个dataSource下面的durid可以独立配置(若不独立配置,则使用全局配置),根据全局配置和独立配置结合转换为Properties,然后在DruidDataSourceCreator类中根据这个配置创建druid连接池。

    2.3 如何集成众多连接池

    集成连接池配置项是通过DynamicDataSourceProperties配置类实现的,但是如何通过这些配置项生成真正的数据源连接池?让我们来看creator包。

    见名知意,可知支持哪些类型的数据源。

    在自动装配中配置DataSource的时候,new了一个DynamicRoutingDataSource,该类实现了InitializingBean接口,在bean初始化时做一些操作。

    @Slf4j
    public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
    
        private static final String UNDERLINE = "_";
        /**
         * 所有数据库
         */
        private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();
        /**
         * 分组数据库
         */
        private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap<>();
        @Setter
        private DynamicDataSourceProvider provider;
        @Setter
        private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
        @Setter
        private String primary = "master";
        @Setter
        private Boolean strict = false;
        @Setter
        private Boolean p6spy = false;
        @Setter
        private Boolean seata = false;
    
        @Override
        public DataSource determineDataSource() {
            return getDataSource(DynamicDataSourceContextHolder.peek());
        }
    
        private DataSource determinePrimaryDataSource() {
            log.debug("dynamic-datasource switch to the primary datasource");
            return groupDataSources.containsKey(primary) ? groupDataSources.get(primary).determineDataSource() : dataSourceMap.get(primary);
        }
    
        /**
         * 获取当前所有的数据源
         *
         * @return 当前所有数据源
         */
        public Map<String, DataSource> getCurrentDataSources() {
            return dataSourceMap;
        }
    
        /**
         * 获取的当前所有的分组数据源
         *
         * @return 当前所有的分组数据源
         */
        public Map<String, GroupDataSource> getCurrentGroupDataSources() {
            return groupDataSources;
        }
    
        /**
         * 获取数据源
         *
         * @param ds 数据源名称
         * @return 数据源
         */
        public DataSource getDataSource(String ds) {
            if (StringUtils.isEmpty(ds)) {
                return determinePrimaryDataSource();
            } else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
                log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
                return groupDataSources.get(ds).determineDataSource();
            } else if (dataSourceMap.containsKey(ds)) {
                log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
                return dataSourceMap.get(ds);
            }
            if (strict) {
                throw new RuntimeException("dynamic-datasource could not find a datasource named" + ds);
            }
            return determinePrimaryDataSource();
        }
    
        /**
         * 添加数据源
         *
         * @param ds         数据源名称
         * @param dataSource 数据源
         */
        public synchronized void addDataSource(String ds, DataSource dataSource) {
            DataSource oldDataSource = dataSourceMap.put(ds, dataSource);
            // 新数据源添加到分组
            this.addGroupDataSource(ds, dataSource);
            // 关闭老的数据源
            if (oldDataSource != null) {
                try {
                    closeDataSource(oldDataSource);
                } catch (Exception e) {
                    log.error("dynamic-datasource - remove the database named [{}]  failed", ds, e);
                }
            }
    
            log.info("dynamic-datasource - load a datasource named [{}] success", ds);
        }
    
        /**
         * 新数据源添加到分组
         *
         * @param ds         新数据源的名字
         * @param dataSource 新数据源
         */
        private void addGroupDataSource(String ds, DataSource dataSource) {
            if (ds.contains(UNDERLINE)) {
                String group = ds.split(UNDERLINE)[0];
                GroupDataSource groupDataSource = groupDataSources.get(group);
                if (groupDataSource == null) {
                    try {
                        groupDataSource = new GroupDataSource(group, strategy.getDeclaredConstructor().newInstance());
                        groupDataSources.put(group, groupDataSource);
                    } catch (Exception e) {
                        throw new RuntimeException("dynamic-datasource - add the datasource named " + ds + " error", e);
                    }
                }
                groupDataSource.addDatasource(ds, dataSource);
            }
        }
    
        /**
         * 删除数据源
         *
         * @param ds 数据源名称
         */
        public synchronized void removeDataSource(String ds) {
            if (!StringUtils.hasText(ds)) {
                throw new RuntimeException("remove parameter could not be empty");
            }
            if (primary.equals(ds)) {
                throw new RuntimeException("could not remove primary datasource");
            }
            if (dataSourceMap.containsKey(ds)) {
                DataSource dataSource = dataSourceMap.remove(ds);
                try {
                    closeDataSource(dataSource);
                } catch (Exception e) {
                    log.error("dynamic-datasource - remove the database named [{}]  failed", ds, e);
                }
    
                if (ds.contains(UNDERLINE)) {
                    String group = ds.split(UNDERLINE)[0];
                    if (groupDataSources.containsKey(group)) {
                        DataSource oldDataSource = groupDataSources.get(group).removeDatasource(ds);
                        if (oldDataSource == null) {
                            if (log.isWarnEnabled()) {
                                log.warn("fail for remove datasource from group. dataSource: {} ,group: {}", ds, group);
                            }
                        }
                    }
                }
                log.info("dynamic-datasource - remove the database named [{}] success", ds);
            } else {
                log.warn("dynamic-datasource - could not find a database named [{}]", ds);
            }
        }
    
        /**
         * 关闭数据源。
         * <pre>
         *    从3.2.0开启,如果是原生或使用 DataSourceCreator 创建的数据源会包装成ItemDataSource。
         *    ItemDataSource保留了最原始的数据源,其可直接关闭。
         *    如果不是DataSourceCreator创建的数据源则只有尝试解包装再关闭。
         * </pre>
         */
        private void closeDataSource(DataSource dataSource) throws Exception {
            if (dataSource instanceof ItemDataSource) {
                ((ItemDataSource) dataSource).close();
            } else {
                if (seata && dataSource instanceof DataSourceProxy) {
                    DataSourceProxy dataSourceProxy = (DataSourceProxy) dataSource;
                    dataSource = dataSourceProxy.getTargetDataSource();
                }
                if (p6spy && dataSource instanceof P6DataSource) {
                    Field realDataSourceField = P6DataSource.class.getDeclaredField("realDataSource");
                    realDataSourceField.setAccessible(true);
                    dataSource = (DataSource) realDataSourceField.get(dataSource);
                }
                Class<? extends DataSource> clazz = dataSource.getClass();
                Method closeMethod = clazz.getDeclaredMethod("close");
                closeMethod.invoke(dataSource);
            }
        }
    
        @Override
        public void destroy() throws Exception {
            log.info("dynamic-datasource start closing ....");
            for (Map.Entry<String, DataSource> item : dataSourceMap.entrySet()) {
                closeDataSource(item.getValue());
            }
            log.info("dynamic-datasource all closed success,bye");
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            // 检查开启了配置但没有相关依赖
            checkEnv();
            // 添加并分组数据源
            Map<String, DataSource> dataSources = provider.loadDataSources();
            for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
                addDataSource(dsItem.getKey(), dsItem.getValue());
            }
            // 检测默认数据源是否设置
            if (groupDataSources.containsKey(primary)) {
                log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), primary);
            } else if (dataSourceMap.containsKey(primary)) {
                log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), primary);
            } else {
                throw new RuntimeException("dynamic-datasource Please check the setting of primary");
            }
        }
    
        private void checkEnv() {
            if (p6spy) {
                try {
                    Class.forName("com.p6spy.engine.spy.P6DataSource");
                    log.info("dynamic-datasource detect P6SPY plugin and enabled it");
                } catch (Exception e) {
                    throw new RuntimeException("dynamic-datasource enabled P6SPY ,however without p6spy dependency", e);
                }
            }
            if (seata) {
                try {
                    Class.forName("io.seata.rm.datasource.DataSourceProxy");
                    log.info("dynamic-datasource detect ALIBABA SEATA and enabled it");
                } catch (Exception e) {
                    throw new RuntimeException("dynamic-datasource enabled ALIBABA SEATA,however without seata dependency", e);
                }
            }
        }
    }

    这个类就是核心动态数据源组件。它将DataSource维护在map里,这里重点看如何创建数据源连接池。它所做的操作就是:初始化时从provider获取创建好的数据源map,然后解析这个map对其分组。下面来看看这个provider里面是如何创建这个数据源map的。

        @Bean
        @ConditionalOnMissingBean
        public DynamicDataSourceProvider dynamicDataSourceProvider() {
            Map<String, DataSourceProperty> datasourceMap = properties.getDatasource();
            return new YmlDynamicDataSourceProvider(datasourceMap);
        }

    在自动装配中注入的这个bean,是通过yml读取配置文件(还有通过jdbc读取配置文件)生成的。
    通过跟踪provider.loadDataSources()方法,发现在createDataSourceMap()方法中调用的是dataSourceCreator.createDataSource(dataSourceProperty, publicKey)。进一步追踪可以发现,具体使用哪种类型的连接池,是在DynamicDataSourceCreatorAutoConfiguration自动配置类中完成的。

    @Slf4j
    @Configuration
    @AllArgsConstructor
    @EnableConfigurationProperties(DynamicDataSourceProperties.class)
    public class DynamicDataSourceCreatorAutoConfiguration {
    
        private static final int JNDI_ORDER = 1000;
        private static final int DRUID_ORDER = 2000;
        private static final int HIKARI_ORDER = 3000;
        private static final int DEFAULT_ORDER = 5000;
        private final DynamicDataSourceProperties properties;
    
        @Primary
        @Bean
        @ConditionalOnMissingBean
        public DefaultDataSourceCreator dataSourceCreator(List<DataSourceCreator> dataSourceCreators) {
            DefaultDataSourceCreator defaultDataSourceCreator = new DefaultDataSourceCreator();
            defaultDataSourceCreator.setProperties(properties);
            defaultDataSourceCreator.setDataSourceCreators(dataSourceCreators);
            return defaultDataSourceCreator;
        }
    
        @Bean
        @Order(DEFAULT_ORDER)
        @ConditionalOnMissingBean
        public BasicDataSourceCreator basicDataSourceCreator() {
            return new BasicDataSourceCreator();
        }
    
        @Bean
        @Order(JNDI_ORDER)
        @ConditionalOnMissingBean
        public JndiDataSourceCreator jndiDataSourceCreator() {
            return new JndiDataSourceCreator();
        }
    
        /**
         * 存在Druid数据源时, 加入创建器
         */
        @ConditionalOnClass(DruidDataSource.class)
        @Configuration
        public class DruidDataSourceCreatorConfiguration {
            @Bean
            @Order(DRUID_ORDER)
            @ConditionalOnMissingBean
            public DruidDataSourceCreator druidDataSourceCreator() {
                return new DruidDataSourceCreator(properties.getDruid());
            }
    
        }
    
        /**
         * 存在Hikari数据源时, 加入创建器
         */
        @ConditionalOnClass(HikariDataSource.class)
        @Configuration
        public class HikariDataSourceCreatorConfiguration {
            @Bean
            @Order(HIKARI_ORDER)
            @ConditionalOnMissingBean
            public HikariDataSourceCreator hikariDataSourceCreator() {
                return new HikariDataSourceCreator(properties.getHikari());
            }
        }
    
    }
    

    2.4 DS注解如何被拦截处理的

    注解拦截处理离不开AOP,这里介绍代码中如何使用AOP。

    我们还是从DynamicDataSourceAutoConfiguration入口配置类中dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor)方法入手,该方法注入了一个DynamicDataSourceAnnotationAdvisor类型的bean对象。

    在讲解这个advisor之前,这里多提一点AOP相关的。

    在 Spring AOP 中,有 3 个常用的概念:Advices 、 Pointcut 、 Advisor ,解释如下:
    Advice :一个 method 执行前或执行后的动作。
    Pointcut :根据 method 的名字或者正则表达式等方式,去拦截一个 method 。
    Advisor : Advice 和 Pointcut 组成的独立的单元,并且能够传给 proxy factory 对象。

    @Component
    //声明这是一个切面Bean
    @Aspect
    public class ServiceAspect {
        //配置切入点,该方法无方法体,主要为方便同类中其他方法使用此处配置的切入点
        @Pointcut("execution(* com.xxx.aop.service..*(..))")
        public void aspect() {
        }
    
        /*
         * 配置前置通知,使用在方法aspect()上注册的切入点
         * 同时接受JoinPoint切入点对象,可以没有该参数
         */
        @Before("aspect()")
        public void before(JoinPoint joinPoint) {
        }
    
        //配置后置通知,使用在方法aspect()上注册的切入点
        @After("aspect()")
        public void after(JoinPoint joinPoint) {
        }
    
        //配置环绕通知,使用在方法aspect()上注册的切入点
        @Around("aspect()")
        public void around(JoinPoint joinPoint) {
        }
    
        //配置后置返回通知,使用在方法aspect()上注册的切入点
        @AfterReturning("aspect()")
        public void afterReturn(JoinPoint joinPoint) {
        }
    
        //配置抛出异常后通知,使用在方法aspect()上注册的切入点
        @AfterThrowing(pointcut = "aspect()", throwing = "ex")
        public void afterThrow(JoinPoint joinPoint, Exception ex) {
        }
    }
    

    平时,我们可能使用这种AspectJ注解多一些,通过@Aspect注解的方式来声明切面,spring会通过我们的AspectJ注解(比如@Pointcut、@Before) 动态生成各个Advisor。

    Spring还提供了另一种切面----顾问(Advisor),其可以完成更为复杂的切面织入功能。我们可以通过直接继承AbstractPointcutAdvisor来提供切面逻辑,生成对应的Advisor实例,如下图:

    其中,最重要的就是getAdvicegetPointcut方法,可以简单认为advisor=advice+pointcut

    再回到DynamicDataSourceAutoConfiguration入口配置类中dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor)方法,跟进观察DynamicDataSourceAnnotationAdvisor类:

    public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
    
        // 通知
        private final Advice advice;
        // 切入点
        private final Pointcut pointcut;
    
        public DynamicDataSourceAnnotationAdvisor(@NonNull DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) {
            this.advice = dynamicDataSourceAnnotationInterceptor;
            this.pointcut = buildPointcut();
        }
    
        @Override
        public Pointcut getPointcut() {
            return this.pointcut;
        }
    
        @Override
        public Advice getAdvice() {
            return this.advice;
        }
    
        @Override
        public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
            if (this.advice instanceof BeanFactoryAware) {
                ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
            }
        }
    
        private Pointcut buildPointcut() {
            // 类级别
            Pointcut cpc = new AnnotationMatchingPointcut(DS.class, true);
            // 方法级别
            Pointcut mpc = new AnnotationMethodPoint(DS.class);
            // 合并类和方法上添加的注解,类上的注解会绑定到每个方法上。
            return new ComposablePointcut(cpc).union(mpc);
        }
    
        /**
         * In order to be compatible with the spring lower than 5.0
         */
        private static class AnnotationMethodPoint implements Pointcut {
    
            private final Class<? extends Annotation> annotationType;
    
            public AnnotationMethodPoint(Class<? extends Annotation> annotationType) {
                Assert.notNull(annotationType, "Annotation type must not be null");
                this.annotationType = annotationType;
            }
    
            @Override
            public ClassFilter getClassFilter() {
                return ClassFilter.TRUE;
            }
    
            @Override
            public MethodMatcher getMethodMatcher() {
                return new AnnotationMethodMatcher(annotationType);
            }
    
            private static class AnnotationMethodMatcher extends StaticMethodMatcher {
                private final Class<? extends Annotation> annotationType;
    
                public AnnotationMethodMatcher(Class<? extends Annotation> annotationType) {
                    this.annotationType = annotationType;
                }
    
                @Override
                public boolean matches(Method method, Class<?> targetClass) {
                    if (matchesMethod(method)) {
                        return true;
                    }
                    // Proxy classes never have annotations on their redeclared methods.
                    if (Proxy.isProxyClass(targetClass)) {
                        return false;
                    }
                    // The method may be on an interface, so let's check on the target class as well.
                    Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
                    return (specificMethod != method && matchesMethod(specificMethod));
                }
    
                private boolean matchesMethod(Method method) {
                    return AnnotatedElementUtils.hasAnnotation(method, this.annotationType);
                }
            }
        }
    }
    

    现在看下@DS注解的advisor实现,在buildPointcut方法里拦截了被@DS注解的方法或类,并且使用ComposablePointcut组合切入点,可以实现方法优先级大于类优先级的特性。DynamicDataSourceAnnotationAdvisor通过构造方法传过来的参数类型是DynamicDataSourceAnnotationInterceptor类,跟进观察该类:

    public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {
    
        /**
         * The identification of SPEL.
         */
        private static final String DYNAMIC_PREFIX = "#";
    
        private final DataSourceClassResolver dataSourceClassResolver;
        private final DsProcessor dsProcessor;
    
        public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) {
            dataSourceClassResolver = new DataSourceClassResolver(allowedPublicOnly);
            this.dsProcessor = dsProcessor;
        }
    
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            String dsKey = determineDatasourceKey(invocation);
            // 把获取到的数据源标识(如master)存入本地线程
            DynamicDataSourceContextHolder.push(dsKey);
            try {
                return invocation.proceed();
            } finally {
                DynamicDataSourceContextHolder.poll();
            }
        }
    
        private String determineDatasourceKey(MethodInvocation invocation) {
            String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
            // 如果DS注解内容是以#开头,则解析动态最终值;否则,直接返回。
            return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
        }
    }
    

    这是它的advice通知(也可以说是方法拦截器)执行的动作:在要切换数据源的方法执行前,将“切换的数据源”放入了holder里,等方法执行完后在finally中释放掉,完成当前数据源的切换。该类的determineDatasource()方法决定具体使用哪个数据源。

    2.5 多数据源动态切换及如何管理多数据源

    在DynamicDataSourceAnnotationInterceptor类中切换数据源的方法中,前后调用了DynamicDataSourceContextHolder.push()和poll()。跟进观察下DynamicDataSourceContextHolder方法:

    public final class DynamicDataSourceContextHolder {
    
        /**
         * 为什么要用链表存储(准确的是栈)
         * <pre>
         * 为了支持嵌套切换,如ABC三个service都是不同的数据源
         * 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。
         * 传统的只设置当前线程的方式不能满足此业务需求,必须使用栈,后进先出。
         * </pre>
         */
        private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
            @Override
            protected Deque<String> initialValue() {
                return new ArrayDeque<>();
            }
        };
    
        private DynamicDataSourceContextHolder() {
        }
    
        /**
         * 获得当前线程数据源
         *
         * @return 数据源名称
         */
        public static String peek() {
            return LOOKUP_KEY_HOLDER.get().peek();
        }
    
        /**
         * 设置当前线程数据源
         * <p>
         * 如非必要不要手动调用,调用后确保最终清除
         * </p>
         *
         * @param ds 数据源名称
         */
        public static void push(String ds) {
            LOOKUP_KEY_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds);
        }
    
        /**
         * 清空当前线程数据源
         * <p>
         * 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称
         * </p>
         */
        public static void poll() {
            Deque<String> deque = LOOKUP_KEY_HOLDER.get();
            deque.poll();
            if (deque.isEmpty()) {
                LOOKUP_KEY_HOLDER.remove();
            }
        }
    
        /**
         * 强制清空本地线程
         * <p>
         * 防止内存泄漏,如手动调用了push可调用此方法确保清除
         * </p>
         */
        public static void clear() {
            LOOKUP_KEY_HOLDER.remove();
        }
    }
    

    它使用栈处理当前数据源。使用了ArrayDeque这个线程不安全的双端队列来实现栈功能,比原生Stack性能好。使用栈数据结构,嵌套过程中进来push、出去就pop,实现了这个嵌套调用service的业务需求。

    下面来看切换数据源的核心类AbstractRoutingDataSource:

     该项目没有使用Spring的AbstractRoutingDataSource做多数据源动态切换,而是自定义实现了一个AbstractRoutingDataSource类,如下所示:

    public abstract class AbstractRoutingDataSource extends AbstractDataSource {
    
        /**
         * 由子类实现,决定最终数据源。
         *
         * @return 数据源
         */
        protected abstract DataSource determineDataSource();
    
        @Override
        public Connection getConnection() throws SQLException {
            String xid = TransactionContext.getXID();
            if (StringUtils.isEmpty(xid)) {
                return determineDataSource().getConnection();
            } else {
                String ds = DynamicDataSourceContextHolder.peek();
                ConnectionProxy connection = ConnectionFactory.getConnection(ds);
                return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
            }
        }
    
        @Override
        public Connection getConnection(String username, String password) throws SQLException {
            String xid = TransactionContext.getXID();
            if (StringUtils.isEmpty(xid)) {
                return determineDataSource().getConnection(username, password);
            } else {
                String ds = DynamicDataSourceContextHolder.peek();
                ConnectionProxy connection = ConnectionFactory.getConnection(ds);
                return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection(username, password))
                        : connection;
            }
        }
    
        private Connection getConnectionProxy(String ds, Connection connection) {
            ConnectionProxy connectionProxy = new ConnectionProxy(connection, ds);
            ConnectionFactory.putConnection(ds, connectionProxy);
            return connectionProxy;
        }
    
        @Override
        @SuppressWarnings("unchecked")
        public <T> T unwrap(Class<T> iface) throws SQLException {
            if (iface.isInstance(this)) {
                return (T) this;
            }
            return determineDataSource().unwrap(iface);
        }
    
        @Override
        public boolean isWrapperFor(Class<?> iface) throws SQLException {
            return (iface.isInstance(this) || determineDataSource().isWrapperFor(iface));
        }
    }
    

    该抽象类也是实现了DataSource接口的getConnection方法,现在来看下子类如何实现determineDataSource方法:

    @Slf4j
    public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
    
        private static final String UNDERLINE = "_";
        /**
         * 所有数据库
         */
        private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();
        /**
         * 分组数据库
         */
        private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap<>();
        @Setter
        private DynamicDataSourceProvider provider;
        @Setter
        private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
        @Setter
        private String primary = "master";
        @Setter
        private Boolean strict = false;
        @Setter
        private Boolean p6spy = false;
        @Setter
        private Boolean seata = false;
    
        @Override
        public DataSource determineDataSource() {
            return getDataSource(DynamicDataSourceContextHolder.peek());
        }
    
        private DataSource determinePrimaryDataSource() {
            log.debug("dynamic-datasource switch to the primary datasource");
            return groupDataSources.containsKey(primary) ? groupDataSources.get(primary).determineDataSource() : dataSourceMap.get(primary);
        }
    
        /**
         * 获取当前所有的数据源
         *
         * @return 当前所有数据源
         */
        public Map<String, DataSource> getCurrentDataSources() {
            return dataSourceMap;
        }
    
        /**
         * 获取的当前所有的分组数据源
         *
         * @return 当前所有的分组数据源
         */
        public Map<String, GroupDataSource> getCurrentGroupDataSources() {
            return groupDataSources;
        }
    
        /**
         * 获取数据源
         *
         * @param ds 数据源名称
         * @return 数据源
         */
        public DataSource getDataSource(String ds) {
            if (StringUtils.isEmpty(ds)) {
                return determinePrimaryDataSource();
            } else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
                log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
                return groupDataSources.get(ds).determineDataSource();
            } else if (dataSourceMap.containsKey(ds)) {
                log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
                return dataSourceMap.get(ds);
            }
            if (strict) {
                throw new RuntimeException("dynamic-datasource could not find a datasource named" + ds);
            }
            return determinePrimaryDataSource();
        }
    
        /**
         * 添加数据源
         *
         * @param ds         数据源名称
         * @param dataSource 数据源
         */
        public synchronized void addDataSource(String ds, DataSource dataSource) {
            DataSource oldDataSource = dataSourceMap.put(ds, dataSource);
            // 新数据源添加到分组
            this.addGroupDataSource(ds, dataSource);
            // 关闭老的数据源
            if (oldDataSource != null) {
                try {
                    closeDataSource(oldDataSource);
                } catch (Exception e) {
                    log.error("dynamic-datasource - remove the database named [{}]  failed", ds, e);
                }
            }
    
            log.info("dynamic-datasource - load a datasource named [{}] success", ds);
        }
    
        /**
         * 新数据源添加到分组
         *
         * @param ds         新数据源的名字
         * @param dataSource 新数据源
         */
        private void addGroupDataSource(String ds, DataSource dataSource) {
            if (ds.contains(UNDERLINE)) {
                String group = ds.split(UNDERLINE)[0];
                GroupDataSource groupDataSource = groupDataSources.get(group);
                if (groupDataSource == null) {
                    try {
                        groupDataSource = new GroupDataSource(group, strategy.getDeclaredConstructor().newInstance());
                        groupDataSources.put(group, groupDataSource);
                    } catch (Exception e) {
                        throw new RuntimeException("dynamic-datasource - add the datasource named " + ds + " error", e);
                    }
                }
                groupDataSource.addDatasource(ds, dataSource);
            }
        }
    
        /**
         * 删除数据源
         *
         * @param ds 数据源名称
         */
        public synchronized void removeDataSource(String ds) {
            if (!StringUtils.hasText(ds)) {
                throw new RuntimeException("remove parameter could not be empty");
            }
            if (primary.equals(ds)) {
                throw new RuntimeException("could not remove primary datasource");
            }
            if (dataSourceMap.containsKey(ds)) {
                DataSource dataSource = dataSourceMap.remove(ds);
                try {
                    closeDataSource(dataSource);
                } catch (Exception e) {
                    log.error("dynamic-datasource - remove the database named [{}]  failed", ds, e);
                }
    
                if (ds.contains(UNDERLINE)) {
                    String group = ds.split(UNDERLINE)[0];
                    if (groupDataSources.containsKey(group)) {
                        DataSource oldDataSource = groupDataSources.get(group).removeDatasource(ds);
                        if (oldDataSource == null) {
                            if (log.isWarnEnabled()) {
                                log.warn("fail for remove datasource from group. dataSource: {} ,group: {}", ds, group);
                            }
                        }
                    }
                }
                log.info("dynamic-datasource - remove the database named [{}] success", ds);
            } else {
                log.warn("dynamic-datasource - could not find a database named [{}]", ds);
            }
        }
    
        /**
         * 关闭数据源。
         * <pre>
         *    从3.2.0开启,如果是原生或使用 DataSourceCreator 创建的数据源会包装成ItemDataSource。
         *    ItemDataSource保留了最原始的数据源,其可直接关闭。
         *    如果不是DataSourceCreator创建的数据源则只有尝试解包装再关闭。
         * </pre>
         */
        private void closeDataSource(DataSource dataSource) throws Exception {
            if (dataSource instanceof ItemDataSource) {
                ((ItemDataSource) dataSource).close();
            } else {
                if (seata && dataSource instanceof DataSourceProxy) {
                    DataSourceProxy dataSourceProxy = (DataSourceProxy) dataSource;
                    dataSource = dataSourceProxy.getTargetDataSource();
                }
                if (p6spy && dataSource instanceof P6DataSource) {
                    Field realDataSourceField = P6DataSource.class.getDeclaredField("realDataSource");
                    realDataSourceField.setAccessible(true);
                    dataSource = (DataSource) realDataSourceField.get(dataSource);
                }
                Class<? extends DataSource> clazz = dataSource.getClass();
                Method closeMethod = clazz.getDeclaredMethod("close");
                closeMethod.invoke(dataSource);
            }
        }
    
        @Override
        public void destroy() throws Exception {
            log.info("dynamic-datasource start closing ....");
            for (Map.Entry<String, DataSource> item : dataSourceMap.entrySet()) {
                closeDataSource(item.getValue());
            }
            log.info("dynamic-datasource all closed success,bye");
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            // 检查开启了配置但没有相关依赖
            checkEnv();
            // 添加并分组数据源
            Map<String, DataSource> dataSources = provider.loadDataSources();
            for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
                addDataSource(dsItem.getKey(), dsItem.getValue());
            }
            // 检测默认数据源是否设置
            if (groupDataSources.containsKey(primary)) {
                log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), primary);
            } else if (dataSourceMap.containsKey(primary)) {
                log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), primary);
            } else {
                throw new RuntimeException("dynamic-datasource Please check the setting of primary");
            }
        }
    
        private void checkEnv() {
            if (p6spy) {
                try {
                    Class.forName("com.p6spy.engine.spy.P6DataSource");
                    log.info("dynamic-datasource detect P6SPY plugin and enabled it");
                } catch (Exception e) {
                    throw new RuntimeException("dynamic-datasource enabled P6SPY ,however without p6spy dependency", e);
                }
            }
            if (seata) {
                try {
                    Class.forName("io.seata.rm.datasource.DataSourceProxy");
                    log.info("dynamic-datasource detect ALIBABA SEATA and enabled it");
                } catch (Exception e) {
                    throw new RuntimeException("dynamic-datasource enabled ALIBABA SEATA,however without seata dependency", e);
                }
            }
        }
    }

    之前已经将creator生成的数据源连接池放入map中,现在从map中获取数据源即可,可以发现数据源组优先于单数据源

    2.6 组数据源的负载均衡怎么实现的

    跟进观察DynamicRoutingDataSource的getDataSource方法:

     else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
                log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
                return groupDataSources.get(ds).determineDataSource();
            }

    如果数据源组不为空,并且DS注解写的数据源组名存在,那么就会通过GroupDataSource类的

    determineDataSource方法在数据源组中选取一个数据源。

    @Data
    public class GroupDataSource {
    
        private String groupName;
        // 数据源切换策略
        private DynamicDataSourceStrategy dynamicDataSourceStrategy;
    
        private Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();
    
        public GroupDataSource(String groupName, DynamicDataSourceStrategy dynamicDataSourceStrategy) {
            this.groupName = groupName;
            this.dynamicDataSourceStrategy = dynamicDataSourceStrategy;
        }
    
        /**
         * add a new datasource to this group
         *
         * @param ds         the name of the datasource
         * @param dataSource datasource
         */
        public DataSource addDatasource(String ds, DataSource dataSource) {
            return dataSourceMap.put(ds, dataSource);
        }
    
        /**
         * @param ds the name of the datasource
         */
        public DataSource removeDatasource(String ds) {
            return dataSourceMap.remove(ds);
        }
        // 根据切换策略,获取一个数据源。
        public DataSource determineDataSource() {
            return dynamicDataSourceStrategy.determineDataSource(new ArrayList<>(dataSourceMap.values()));
        }
    
        public int size() {
            return dataSourceMap.size();
        }
    }

    GroupDataSource使用策略模式来决定一个数据源,目前实现的策略有两种:随机和轮询,默认是轮询。在DynamicDataSourceProperties属性中写了默认值,也可以通过配置文件配置。

    public class LoadBalanceDynamicDataSourceStrategy implements DynamicDataSourceStrategy {
    
        /**
         * 负载均衡计数器
         */
        private final AtomicInteger index = new AtomicInteger(0);
    
        @Override
        public DataSource determineDataSource(List<DataSource> dataSources) {
            return dataSources.get(Math.abs(index.getAndAdd(1) % dataSources.size()));
        }
    }

    如果想通过jdbc获取数据源,该项目有个自定义的抽象类AbstractJdbcDataSourceProvider,需要实现其executeStmt方法,即:从其他数据库查询出url、username、password等信息(就是在yml配置的属性),然后拼接成一个配置对象DataSourceProperty去调用createDataSourceMap方法。

    2.7 如何动态增减数据源

    这个也是很实用的功能,其实现还是通过DynamicRoutingDataSource这个核心动态数据源组件完成。

    @Slf4j
    public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
     省略
    /**
         * 删除数据源
         *
         * @param ds 数据源名称
         */
        public synchronized void removeDataSource(String ds) {
            if (!StringUtils.hasText(ds)) {
                throw new RuntimeException("remove parameter could not be empty");
            }
            if (primary.equals(ds)) {
                throw new RuntimeException("could not remove primary datasource");
            }
            if (dataSourceMap.containsKey(ds)) {
                DataSource dataSource = dataSourceMap.remove(ds);
                try {
                    closeDataSource(dataSource);
                } catch (Exception e) {
                    log.error("dynamic-datasource - remove the database named [{}]  failed", ds, e);
                }
    
                if (ds.contains(UNDERLINE)) {
                    String group = ds.split(UNDERLINE)[0];
                    if (groupDataSources.containsKey(group)) {
                        DataSource oldDataSource = groupDataSources.get(group).removeDatasource(ds);
                        if (oldDataSource == null) {
                            if (log.isWarnEnabled()) {
                                log.warn("fail for remove datasource from group. dataSource: {} ,group: {}", ds, group);
                            }
                        }
                    }
                }
                log.info("dynamic-datasource - remove the database named [{}] success", ds);
            } else {
                log.warn("dynamic-datasource - could not find a database named [{}]", ds);
            }
        }
    
     省略
    }

    可以发现,该项目预留了相关接口给开发者,以方便添加、删除数据库。

    添加数据源的步骤:
    1、注入DynamicRoutingDataSource和DataSourceCreator。
    2、通过数据源配置(url、username、password等)构建一个DataSourceProperty对象。
    3、通过dataSourceCreator,根据配置属性构建一个真实的DataSource。
    4、调用DynamicRoutingDataSource的addDataSource方法添加这个DataSource。
    同理,删除数据源的步骤:
    1、注入DynamicRoutingDataSource。
    2、调用DynamicRoutingDataSource的removeDataSource方法。

        @PostMapping("/add")
        @ApiOperation("通用添加数据源(推荐)")
        public Set<String> add(@Validated @RequestBody DataSourceDTO dto) {
            DataSourceProperty dataSourceProperty = new DataSourceProperty();
            BeanUtils.copyProperties(dto, dataSourceProperty);
            DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
            DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
            ds.addDataSource(dto.getPollName(), dataSource);
            return ds.getCurrentDataSources().keySet();
        }
    
        @DeleteMapping
        @ApiOperation("删除数据源")
        public String remove(String name) {
            DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
            ds.removeDataSource(name);
            return "删除成功";
        }

    三、总结

    通过阅读该项目源码,熟悉了spring aop、spring事务管理、spring boot自动配置等spring知识点,可以根据业务需求去进一步扩展这个starter。

    展开全文
  • <p>2021-03-20 10:12:45 [ RMI TCP Connection(6)-127.0.0.1:9070 ] - [ INFO ] com.alibaba.druid.pool.DruidDataSource.init(DruidDataSource.java:669) {dataSource-377} inited 2021-03-20 10:12:45 [ RMI...
  • DataSource配置

    千次阅读 2021-01-18 20:27:45
    一、JDBCJar依赖:org.springframework.bootspring-boot-starter-jdbcmysqlmysql-connector-java数据源的相关参数配置:DataSourcePropertiesapplication.properties:spring.datasource.url=jdbc:mysql://localhost...
  • DataSource dataSourcedataSource下方报红线 原先遇到的直接就忽略,不影响程序运行 这次是直接报错 报错信息: *************************** APPLICATION FAILED TO START *************************** ...
  • DataSource数据源

    千次阅读 2020-04-29 21:29:21
    目录1 基本概念1.1 数据源DataSource、连接池1.2 连接池思想2 数连接池分类2.1 Hikari连接池2.2 阿里druid 【前言】 我们在进行数据访问都需要配置数据源用来连接数据库。数据源又可以分为两大类:直连的数据源 ...
  • 第五章 DataSource操作 ​ KenDo所有组件与服务器进行连接通信,将ype服务器中的数据拿到数据源DataSource中,在通过DataSource类的将服务器的数据赋值给kendo其他组件。在kendo所有mvvm模式中与服务器连接的请求都...
  • HikariDataSource 配置详解

    千次阅读 2022-05-14 11:32:42
    spring.datasource.hikari.pool-name=KevinHikariPool #最大连接数,小于等于0会被重置为默认值10;大于零小于1会被重置为minimum-idle的值 spring.datasource.hikari.maximum-pool-size=12 #连接超时时间:毫秒,...
  • SpringBoot下DataSource连接配置

    千次阅读 2020-07-06 17:06:13
    spring.datasource.secondary.url=jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf-8 spring.datasource.secondary.username=test spring.datasource.secondary.password=123456 ...
  • HikariPool-1 - dataSource or dataSourceClassName or jdbcUrl is required.解决方案
  • springboot多数据源,DruidDataSource多数据源切换,不重启项目,修改数据库名解决方案
  • DataSource接口,一个被大多数程序员忽略的接口

    千次阅读 多人点赞 2021-09-12 16:45:50
    代码不多,文章不长,简要描述了下 DataSource 演进过程的故事 文章产出背景 最近这段时间一直忙着集团内部安全等保加密相关事项,初步决定使用 shardingsphere 来进行 因为项目众多,需要兼容的需求也随之而来,...
  • datasource配置

    千次阅读 2022-02-04 15:01:47
    datasource: type: com.alibaba.druid.pool.DruidDataSource #使用第三方要设置类型 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=...
  • DataSource 使用方法

    千次阅读 2021-02-25 19:40:45
    DataSource接口(javax.sql.DataSource)替代DriverManager获取Connection的方法,有以下好处:可以在部署时灵活更换Connection实现;可以更好的屏蔽数据库的相关性。以下以oracle为例说明。使用厂商DataSource数据库...
  • DataSource health check failed

    万次阅读 2020-06-05 11:26:54
    2020-06-05 10:48:56 WARN [-RMI TCP Connection(3)-192.168.0.152] [87] [org.springframework.boot.actuate.jdbc.DataSourceHealthIndicator] : DataSource health check failed org.springframework.dao....
  • SpringBoot默认HikariDataSource配置

    千次阅读 2021-05-10 13:04:37
    Spring Boot默认的数据源是HikariDataSource,配置方式 ,直接上配置代码: spring: application: name: test-cloud profiles: active: prod datasource: driver-class-name: com.mysql.jdbc.Driver url: ...
  • SpringBoot配置数据源DataSource

    万次阅读 2019-11-22 16:53:11
    SpringBoot配置数据源DataSource 2019-03-28 categories:资料 author:iigadmin SpringBoot配置数据源DataSource 使用Spring Boot默认数据源 1.1 引入POM依赖 在pom文件中直接依赖官方提供的spring-boot-start-jdbc...
  • SpringBoot中DataSource的配置属性

    千次阅读 2021-08-02 20:27:13
    spring.datasource.abandon-when-percentage-full 设定超时被废弃的连接占到多少比例时要被关闭或上报 spring.datasource.allow-pool-suspension 使用Hikari pool时,是否允许连接池暂停,默认为: fa..
  • 3、使用dynamic-datasource-spring-boot-starter做多数据源及源码分析 文章目录多数据源系列简介实操基本使用集成druid连接池源码分析整体结构总结 简介 前两篇博客介绍了用基本的方式做多数据源,可以应对一般的...
  • Spring配置Datasource获取DataSource连接

    千次阅读 2020-02-10 22:04:10
     DataSource dataSource = applicationContext.getBean("dataSource",DataSource.class);  Connection connection = dataSource.getConnection();  System.out.println(connection.isClosed());  ...
  • DataSource配置 在webapp下新建META-INF文件夹,在META-INF下新建context.xml文件: 根据自己的数据库信息更改context.xml: 我这是连接本地的mysql,将数据库名称改为你自己的。 <?xml version="1.0" encoding=...
  • datasource spring.dao.exceptiontranslation.enabled是否开启PersistenceExceptionTranslationPostProcessor,默认为true spring.datasource.abandon-when-percentage-full设定超时被废弃的连接占到多少...
  • 而dynamic-datasource-spring-boot-starter的文档要收费,就自己琢磨了一下,这里记录一下。 需要解决的核心类: DynamicRoutingDataSource /* * Copyright © 2018 organization baomidou * * Licensed ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 601,088
精华内容 240,435
关键字:

datasource