SpringBoot+Mybatis配置Druid多數據源

開篇以前,說一句題外話。多數據源和動態數據源的區別。html

  1. 多數據源,通常用於對接多個業務上獨立的數據庫(可能異構數據庫)。
  2. 動態數據源,通常用於大型應用對數據切分。

配置參考

如何配置多數據源,網上教程一大堆。可參考 SpringBoot+MyBatis多數據源最簡解決方案java

問題描述

在實際開發配置中發現,若是要啓用Druid的防火牆監控(WallFilter)和統計監控(StatFilter),多個異構數據源就會出錯,錯誤信息以下:mysql

com.alibaba.druid.sql.parser.ParserException: syntax error, error in....

跟蹤Druid的源碼,發現了問題。spring

// com.alibaba.druid.wall.WallFilter
  private WallCheckResult checkInternal(String sql) throws SQLException {
    WallCheckResult checkResult = provider.check(sql);
    List<Violation> violations = checkResult.getViolations();

    // ... 下面省略了 ...
  }

全部的檢查sql工做,都在checkInternal方法中完成,而provider對象在執行init初始化以後就再也沒有改變了。這也就致使異構數據庫的sql檢查sql

StatFilter也是相似問題。數據庫

// com.alibaba.druid.filter.stat.StatFilter#createSqlStat(StatementProxy, String)
  public JdbcSqlStat createSqlStat(StatementProxy statement, String sql) {
    // ...省略
    String dbType = this.dbType;
    if (dbType == null) {
      dbType = dataSource.getDbType();
    }
    // ...省略//
  }

解決方案

重寫WallFilter

import com.alibaba.druid.filter.FilterChain;
import com.alibaba.druid.proxy.jdbc.CallableStatementProxy;
import com.alibaba.druid.proxy.jdbc.ConnectionProxy;
import com.alibaba.druid.proxy.jdbc.DataSourceProxy;
import com.alibaba.druid.proxy.jdbc.PreparedStatementProxy;
import com.alibaba.druid.util.JdbcUtils;
import com.alibaba.druid.wall.WallConfig;
import com.alibaba.druid.wall.WallFilter;
import com.alibaba.druid.wall.WallProvider;
import com.alibaba.druid.wall.spi.DB2WallProvider;
import com.alibaba.druid.wall.spi.MySqlWallProvider;
import com.alibaba.druid.wall.spi.OracleWallProvider;
import com.alibaba.druid.wall.spi.PGWallProvider;
import com.alibaba.druid.wall.spi.SQLServerWallProvider;

import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 自定義Druid防火牆過濾器
 * <p>使用多類型數據源時,因共用WallProvider解析器,致使判斷數據源類型出錯</p>
 * @author BBF
 * @see com.alibaba.druid.wall.WallFilter
 */
public class FrameWallFilter extends WallFilter {

  /**
   * 用線程安全的ConcurrentHashMap存儲WallProvider對象
   */
  private final Map<String, WallProvider> providerMap = new ConcurrentHashMap<>(8);

  /**
   * 獲取WallProvider
   * @param dataSource 數據源
   * @return WallProvider
   */
  private WallProvider getProvider(DataSourceProxy dataSource) {
    String dbType;
    if (dataSource.getDbType() != null) {
      dbType = dataSource.getDbType();
    } else {
      dbType = JdbcUtils.getDbType(dataSource.getRawJdbcUrl(), "");
    }
    WallProvider provider;
    if (JdbcUtils.MYSQL.equals(dbType) || JdbcUtils.MARIADB.equals(dbType) || JdbcUtils.H2.equals(dbType)) {
      provider = providerMap.get(JdbcUtils.MYSQL);
      if (provider == null) {
        provider = new MySqlWallProvider(new WallConfig(MySqlWallProvider.DEFAULT_CONFIG_DIR));
        provider.setName(dataSource.getName());
        providerMap.put(JdbcUtils.MYSQL, provider);
      }
    } else if (JdbcUtils.ORACLE.equals(dbType) || JdbcUtils.ALI_ORACLE.equals(dbType)) {
      provider = providerMap.get(JdbcUtils.ORACLE);
      if (provider == null) {
        provider = new OracleWallProvider(new WallConfig(OracleWallProvider.DEFAULT_CONFIG_DIR));
        provider.setName(dataSource.getName());
        providerMap.put(JdbcUtils.ORACLE, provider);
      }
    } else if (JdbcUtils.SQL_SERVER.equals(dbType) || JdbcUtils.JTDS.equals(dbType)) {
      provider = providerMap.get(JdbcUtils.SQL_SERVER);
      if (provider == null) {
        provider = new SQLServerWallProvider(new WallConfig(SQLServerWallProvider.DEFAULT_CONFIG_DIR));
        provider.setName(dataSource.getName());
        providerMap.put(JdbcUtils.SQL_SERVER, provider);
      }
    } else if (JdbcUtils.POSTGRESQL.equals(dbType) || JdbcUtils.ENTERPRISEDB.equals(dbType)) {
      provider = providerMap.get(JdbcUtils.POSTGRESQL);
      if (provider == null) {
        provider = new PGWallProvider(new WallConfig(PGWallProvider.DEFAULT_CONFIG_DIR));
        provider.setName(dataSource.getName());
        providerMap.put(JdbcUtils.POSTGRESQL, provider);
      }
    } else if (JdbcUtils.DB2.equals(dbType)) {
      provider = providerMap.get(JdbcUtils.DB2);
      if (provider == null) {
        provider = new DB2WallProvider(new WallConfig(DB2WallProvider.DEFAULT_CONFIG_DIR));
        provider.setName(dataSource.getName());
        providerMap.put(JdbcUtils.DB2, provider);
      }
    } else {
      throw new IllegalStateException("dbType not support : " + dbType);
    }
    return provider;
  }

  /**
   * 利用反射來更新父類私有變量provider
   * @param connection ConnectionProxy
   */
  private void setProvider(ConnectionProxy connection) {
    for (Class<?> cls = this.getClass(); cls != Object.class; cls = cls.getSuperclass()) {
      try {
        Field field = cls.getDeclaredField("provider");
        field.setAccessible(true);
        field.set(this, getProvider(connection.getDirectDataSource()));
      } catch (Exception e) {
        // Field不在當前類定義,繼續向上轉型
      }
    }
  }

  @Override
  public PreparedStatementProxy connection_prepareStatement(FilterChain chain,
                                                            ConnectionProxy
                                                                connection,
                                                            String sql) throws SQLException {
    this.setProvider(connection);
    return super.connection_prepareStatement(chain, connection, sql);
  }

  @Override
  public PreparedStatementProxy connection_prepareStatement(FilterChain chain,
                                                            ConnectionProxy connection,
                                                            String sql,
                                                            int autoGeneratedKeys) throws SQLException {
    this.setProvider(connection);
    return super.connection_prepareStatement(chain, connection, sql, autoGeneratedKeys);
  }

  @Override
  public PreparedStatementProxy connection_prepareStatement(FilterChain chain,
                                                            ConnectionProxy connection,
                                                            String sql,
                                                            int resultSetType,
                                                            int resultSetConcurrency)
      throws SQLException {
    this.setProvider(connection);
    return super.connection_prepareStatement(chain, connection, sql,
        resultSetType, resultSetConcurrency);
  }

  @Override
  public PreparedStatementProxy connection_prepareStatement(FilterChain chain,
                                                            ConnectionProxy connection,
                                                            String sql,
                                                            int resultSetType,
                                                            int resultSetConcurrency,
                                                            int resultSetHoldability) throws SQLException {
    this.setProvider(connection);
    return super.connection_prepareStatement(chain, connection, sql,
        resultSetType, resultSetConcurrency, resultSetHoldability);
  }

  @Override
  public PreparedStatementProxy connection_prepareStatement(FilterChain chain,
                                                            ConnectionProxy connection,
                                                            String sql,
                                                            int[] columnIndexes) throws SQLException {
    this.setProvider(connection);
    return super.connection_prepareStatement(chain, connection, sql, columnIndexes);
  }

  @Override
  public PreparedStatementProxy connection_prepareStatement(FilterChain chain,
                                                            ConnectionProxy connection,
                                                            String sql,
                                                            String[] columnNames) throws SQLException {
    this.setProvider(connection);
    return super.connection_prepareStatement(chain, connection, sql, columnNames);
  }

  @Override
  public CallableStatementProxy connection_prepareCall(FilterChain chain,
                                                       ConnectionProxy connection,
                                                       String sql) throws SQLException {
    this.setProvider(connection);
    return super.connection_prepareCall(chain, connection, sql);
  }

  @Override
  public CallableStatementProxy connection_prepareCall(FilterChain chain,
                                                       ConnectionProxy connection,
                                                       String sql,
                                                       int resultSetType,
                                                       int resultSetConcurrency) throws SQLException {
    this.setProvider(connection);
    return super.connection_prepareCall(chain, connection, sql,
        resultSetType, resultSetConcurrency);
  }

  @Override
  public CallableStatementProxy connection_prepareCall(FilterChain chain,
                                                       ConnectionProxy connection,
                                                       String sql,
                                                       int resultSetType,
                                                       int resultSetConcurrency,
                                                       int resultSetHoldability) throws SQLException {
    this.setProvider(connection);
    return super.connection_prepareCall(chain, connection, sql,
        resultSetType, resultSetConcurrency, resultSetHoldability);
  }

}

重寫StatFilter

import com.alibaba.druid.filter.stat.StatFilter;
import com.alibaba.druid.proxy.jdbc.StatementProxy;
import com.alibaba.druid.stat.JdbcSqlStat;

/**
 * 自定義Druid統計監控過濾器
 * <p>使用多類型數據源時,因沒有及時清空dbType,致使判斷數據源類型出錯</p>
 * @author BBF
 * @see com.alibaba.druid.filter.stat.StatFilter#createSqlStat(StatementProxy, String)
 */
public class FrameStatFilter extends StatFilter {
  @Override
  public JdbcSqlStat createSqlStat(StatementProxy statement, String sql) {
    super.setDbType(null);
    return super.createSqlStat(statement, sql);
  }
}

配置過濾器的Bean

若是存在多個同類Bean候選時,被@Primary標誌的Bean優先。
另外兩個註解@ConfigurationProperties@ConditionalOnProperty是配置文件的前綴和有特定屬性值時生效express

/**
   * 自定義Druid防火牆過濾器Bean
   * @param wallConfig 防火牆過濾器配置Bean
   * @return WallFilter
   * @see com.alibaba.druid.spring.boot.autoconfigure.stat.DruidFilterConfiguration#wallFilter
   */
  @Bean("wallFilter")
  @ConfigurationProperties("spring.datasource.druid.filter.wall")
  @ConditionalOnProperty(prefix = "spring.datasource.druid.filter.wall", name = {"enabled"})
  @Primary
  public WallFilter wallFilter(@Qualifier("wallConfig") WallConfig wallConfig) {
    WallFilter filter = new FrameWallFilter();
    filter.setConfig(wallConfig);
    return filter;
  }

  /**
   * 自定義Druid統計監控過濾器Bean
   * @return StatFilter
   * @see com.alibaba.druid.spring.boot.autoconfigure.stat.DruidFilterConfiguration#statFilter
   */
  @Bean("statFilter")
  @ConfigurationProperties("spring.datasource.druid.filter.stat")
  @ConditionalOnProperty(prefix = "spring.datasource.druid.filter.stat", name = {"enabled"}
  )
  @Primary
  public StatFilter statFilter() {
    return new FrameStatFilter();
  }

附錄

數據源配置類

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.alibaba.druid.util.JdbcUtils;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

import javax.sql.DataSource;

/**
 * 配置從數據源
 * @author BBF
 */
@Configuration
@MapperScan(basePackages = MysqlDataSourceConfig.PACKAGE,
    sqlSessionTemplateRef = MysqlDataSourceConfig.SESSION_NAME)
public class MysqlDataSourceConfig {
  /**
   * Dao類所在的包
   */
  public static final String PACKAGE = "com.bbf.frame.service.dao";

  /**
   * mapper.xml所在目錄
   */
  private static final String MAPPER_LOCATION = "classpath:/mapperMysql/*Mapper.xml";

  /**
   * mybatis的配置文件路徑
   */
  private static final String CONFIG_LOCATION = "classpath:/config/mybatis-config.xml";

  /**
   * bean的名稱
   */
  private static final String DATASOURCE_NAME = "mysqlDataSource";
  private static final String FACTORY_NAME = "mysqlSqlSessionFactory";
  public static final String SESSION_NAME = "mysqlSqlSessionTemplate";

  @Bean(DATASOURCE_NAME)
  @ConfigurationProperties("datasource.druid.mysql")
  public DataSource dataSourceTwo() {
    DruidDataSource ds= DruidDataSourceBuilder.create().build();
    ds.setDbType(JdbcUtils.MYSQL);
    return ds;
  }

  /**
   * Mybatis的SQL會話工廠
   * @param dataSource 數據源
   * @return SqlSessionFactory
   * @throws Exception 建立SqlSessionFactory發生異常
   */
  @Bean(name = FACTORY_NAME)
  public SqlSessionFactory sqlSessionFactory(@Qualifier(DATASOURCE_NAME) DataSource dataSource) throws Exception {
    final SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSource);
    ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
    sqlSessionFactoryBean.setMapperLocations(resolver.getResources(MAPPER_LOCATION));
    sqlSessionFactoryBean.setConfigLocation(resolver.getResource(CONFIG_LOCATION));
    return sqlSessionFactoryBean.getObject();
  }

  @Bean(SESSION_NAME)
  public SqlSessionTemplate sqlSessionTemplate(@Qualifier(FACTORY_NAME) SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionTemplate(sqlSessionFactory);
  }
}

配置文件

爲了其它數據源配置的相對獨立性,單獨保存爲一個文件mysql.properties
在入口類上,定義@PropertySource,本文在主數據源以外,又定義了兩個數據源。apache

@SpringBootApplication
@ImportResource(locations = {"classpath:config/conf.xml"})
@PropertySource(encoding = "UTF8", value = {"classpath:config/datasource/sqlserver.properties",
    "classpath:config/datasource/mysql.properties"})
public class Application {
  //內容略
}
############################################
# DataSource - druid    Mysql數據源
############################################
# 多數據源,涉及到異構數據庫,必須明確指定dbType,不然druid的WallFilter轉換SQL出錯
# 取值內容可參考 com.alibaba.druid.util.JdbcConstants
datasource.druid.mysql.db-type=mysql
datasource.druid.mysql.driver-class-name=com.mysql.jdbc.Driver
datasource.druid.mysql.url=jdbc:mysql://192.168.1.2:3306/bbf?characterEncoding=UTF-8
datasource.druid.mysql.username=root
datasource.druid.mysql.password=root

# 初始鏈接數
datasource.druid.mysql.initial-size=5
#最大鏈接池數量。default=8+
datasource.druid.mysql.max-active=20
# 獲取鏈接時最大等待時間,單位毫秒。
# 配置了maxWait以後,缺省啓用公平鎖,併發效率會有所降低。
# 若是須要能夠經過配置useUnfairLock屬性爲true使用非公平鎖
datasource.druid.mysql.max-wait=60000
# 開啓池的prepared statement池功能,PSCache對支持遊標的數據庫性能提高巨大
# 若是用Oracle, 則把poolPreparedStatements配置爲true, mysql 5.5以後建議true
datasource.druid.mysql.pool-prepared-statements=true
# 要啓用PSCache,必須配置大於0,當大於0時,poolPreparedStatements自動觸發修改成true。
# 在Druid中,會存在Oracle下PSCache佔用內存過多的問題,能夠把這個數據配置大一些,好比100。默認=-1
datasource.druid.mysql.max-open-prepared-statements=100
# 用來檢測鏈接是否有效的sql,要求是一個查詢語句,經常使用select 'x'。
# 若是validationQuery爲null,testOnBorrow,testOnBorrow,testOnReturn,testWhileIdle都不會起做用。這個能夠不配置
datasource.druid.mysql.validation-query=SELECT 'V';
# 單位:秒,檢測鏈接是否有效的超時時間。底層調用jdbc Statement對象的void setQueryTimeout(int seconds)方法
# mysql實現的不是很合理,不建議在mysql下配置此參數
datasource.druid.mysql.validation-query-timeout=1000
# 是否在從池中取出鏈接前進行檢驗。若是檢驗失敗,則從池中去除鏈接並嘗試取出另外一個
# 注意: 設置爲true後,validation-query參數必須設置
datasource.druid.mysql.test-on-borrow=false
# 是否在歸還鏈接池前進行檢驗
# 注意: 設置爲true後,validation-query參數必須設置
datasource.druid.mysql.test-on-return=false
# 建議配置爲true,不影響性能,而且保證安全性。
# 申請鏈接的時候檢測,若是空閒時間大於timeBetweenEvictionRunsMillis,
# 執行validationQuery檢測鏈接是否有效,validation-query參數必須設置。default=false
datasource.druid.mysql.test-while-idle=true
# 鏈接池中的minIdle數據之內的鏈接,空閒時間超過minEvictableIdleTimeMillis,則會執行keepAlive操做。default=false
datasource.druid.mysql.keep-alive=true
#配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 default=1分鐘
#有兩個含義:
# (1)Destroy線程會檢測鏈接的間隔時間,若是鏈接空閒時間大於等於minEvictableIdleTimeMillis則關閉物理鏈接
# (2)testWhileIdle的判斷依據,詳細看testWhileIdle屬性的說明
datasource.druid.mysql.time-between-eviction-runs-millis=60000
#池中的鏈接保持空閒而不被驅逐的最小時間,單位是毫秒
datasource.druid.mysql.min-evictable-idle-time-millis=100000
datasource.druid.mysql.max-evictable-idle-time-millis=200000
#合併多個DruidDataSource的監控數據
datasource.druid.mysql.use-global-data-source-stat=false

事務配置

這個因人而異,我是更喜歡xml方式配置事務。api

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns = "http://www.springframework.org/schema/beans"
       xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx = "http://www.springframework.org/schema/tx"
       xmlns:aop = "http://www.springframework.org/schema/aop"
       xsi:schemaLocation = "
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
  <!-- 對dataSource 數據源進行事務管理 -->
  <!-- 注意,此處ref的值在DataSourceConfig中配置了 -->
  <bean id = "mysqlTransactionManager"
        class = "org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name = "dataSource" ref = "mysqlDataSource"/>
  </bean>
  <!-- 聲明式事物 -->
  <tx:advice id = "mysqlTxAdvice" transaction-manager = "mysqlTransactionManager">
    <tx:attributes>
      <!-- find,query,list,search,get 開頭的方法,開啓只讀,提升數據庫訪問性能 -->
      <tx:method name = "find*" propagation = "NOT_SUPPORTED" read-only = "true"/>
      <tx:method name = "get*" propagation = "NOT_SUPPORTED" read-only = "true"/>
      <tx:method name = "list*" propagation = "NOT_SUPPORTED" read-only = "true"/>
      <tx:method name = "query*" propagation = "NOT_SUPPORTED" read-only = "true"/>
      <tx:method name = "search*" propagation = "NOT_SUPPORTED" read-only = "true"/>
      <!-- 對save,insert,update,delete 開頭的方法進行事務管理,只要有異常就回滾 -->
      <tx:method name = "create*" propagation = "REQUIRES_NEW"
                 rollback-for = "java.lang.Throwable"/>
      <tx:method name = "delete*" propagation = "REQUIRES_NEW"
                 rollback-for = "java.lang.Throwable"/>
      <tx:method name = "do*" propagation = "REQUIRES_NEW" rollback-for = "java.lang.Throwable"/>
      <tx:method name = "insert*" propagation = "REQUIRES_NEW"
                 rollback-for = "java.lang.Throwable"/>
      <tx:method name = "save*" propagation = "REQUIRES_NEW" rollback-for = "java.lang.Throwable"/>
      <tx:method name = "update*" propagation = "REQUIRES_NEW"
                 rollback-for = "java.lang.Throwable"/>
      <!-- 對其餘方法 使用默認的事務管理 -->
      <tx:method name = "*" propagation = "SUPPORTS"/>
    </tx:attributes>
  </tx:advice>
  <!-- 事務 AOP 配置 -->
  <aop:config proxy-target-class = "true">
    <aop:pointcut id = "mysqlInterceptorPointCuts"
                  expression = "execution( * com.bbf.frame.service.api.impl.*Impl.*(..))"/>
    <aop:advisor advice-ref = "mysqlTxAdvice" pointcut-ref = "mysqlInterceptorPointCuts"/>
  </aop:config>
</beans>

將多個xml,import到一個xml中,目的是減小複雜度。入口類加入註解@ImportResource(locations = {"classpath:config/conf.xml"})安全

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns = "http://www.springframework.org/schema/beans"
       xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation = "http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
  <import resource = "datasource/master_transaction.xml"/>
  <import resource = "datasource/sqlserver_transaction.xml"/>
  <import resource = "datasource/mysql_transaction.xml"/>
</beans>
相關文章
相關標籤/搜索