開篇以前,說一句題外話。多數據源和動態數據源的區別。html
如何配置多數據源,網上教程一大堆。可參考 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(); } // ...省略// }
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); } }
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候選時,被@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>