Druid多数据源下Sql防火墙导致异常的示例分析
本篇文章给大家分享的是有关Druid多数据源下Sql防火墙导致异常的示例分析,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。
问题原因
由于应用组件同时使用了两个数据源,Oracle和Mysql,而之前一直都是Oracle,近期引入了Mysql;
应用组件生产环境配置开启了Druid的Sql防火墙配置,配置项为:
spring.datasource.druid.filter.wall.enabled=true
,由于测试环境未开启,因此将问题带到了生产上面。
开启Sql防火墙后,在数据源初始化的时候会初始化一个 WallFilter .
@Bean @ConfigurationProperties("spring.datasource.druid.filter.wall") @ConditionalOnProperty( prefix = "spring.datasource.druid.filter.wall", name = {"enabled"} ) @ConditionalOnMissingBean public WallFilter wallFilter(WallConfig wallConfig) { WallFilter filter = new WallFilter(); filter.setConfig(wallConfig); return filter; }
问题表象
在近期版本上线后,一些跑了好几年的sql突然一夜之间报错了,守版本的弟兄遇到这种问题后,难免大惊失色,无奈之下,当晚只好回滚了版本。
下面我们来看下,已经跑了几年的sql当时报了什么错:
at com.alibaba.druid.wall.WallFilter.checkInternal(WallFilter.java:798)at com.alibaba.druid.wall.WallFilter.connection_prepareStatement(WallFilter.java:251)at com.alibaba.druid.filter.FilterChainImpl.connection_prepareStatement(FilterChainImpl.java:473)at com.alibaba.druid.filter.FilterAdapter.connection_prepareStatement(FilterAdapter.java:929)at com.alibaba.druid.filter.FilterEventAdapter.connection_prepareStatement(FilterEventAdapter.java:122)at com.alibaba.druid.filter.FilterChainImpl.connection_prepareStatement(FilterChainImpl.java:473)at com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl.prepareStatement(ConnectionProxyImpl.java:342)at com.alibaba.druid.pool.DruidPooledConnection.prepareStatement(DruidPooledConnection.java:350)at org.apache.ibatis.executor.statement.PreparedStatementHandler.instantiateStatement(PreparedStatementHandler.java:87)at org.apache.ibatis.executor.statement.BaseStatementHandler.prepare(BaseStatementHandler.java:88)at org.apache.ibatis.executor.statement.RoutingStatementHandler.prepare(RoutingStatementHandler.java:59)at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:85)at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java:49)at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117)at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76)at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:198)at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java:185)at sun.reflect.GeneratedMethodAccessor408.invoke(Unknown Source)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:433)... 121 common frames omittedCaused by: com.alibaba.druid.sql.parser.ParserException: syntax error, error in :' USING(select ? as o', expect USING, actual IDENTIFIER pos 61, line 2, column 39, token IDENTIFIER USINGat com.alibaba.druid.sql.parser.SQLParser.printError(SQLParser.java:284)at com.alibaba.druid.sql.parser.SQLParser.accept(SQLParser.java:292)at com.alibaba.druid.sql.parser.SQLStatementParser.parseMerge(SQLStatementParser.java:2879)at com.alibaba.druid.sql.parser.SQLStatementParser.parseStatementList(SQLStatementParser.java:225)at com.alibaba.druid.sql.parser.SQLStatementParser.parseStatementList(SQLStatementParser.java:81)at com.alibaba.druid.wall.WallProvider.checkInternal(WallProvider.java:622)at com.alibaba.druid.wall.WallProvider.check(WallProvider.java:576)at com.alibaba.druid.wall.WallFilter.checkInternal(WallFilter.java:785)... 141 common frames omitted
看到上面的异常堆栈,抛出的异常的意思是:对该SQL进行解析的时候期望接受的USING关键字,但实际上在关键字库里面确没有找到USING关键字,而是把USING当成了标识符IDENTIFIER.
问题分析
打开SQLStatementParser 及 SQLParser类源码发现有以下信息:
com.alibaba.druid.sql.parser.SQLStatementParser#parseMerge
关键问题在这里,为什么关键字的词里没有找到USING关键字? 通过debug发现在第一次访问数据的时候会调用com.alibaba.druid.pool.DruidDataSource#init 方法, 该方法里有这一句:
public void init() throws SQLException { if (inited) { return; } 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.connectionIdSeed.addAndGet(delta); this.statementIdSeed.addAndGet(delta); this.resultSetIdSeed.addAndGet(delta); this.transactionIdSeed.addAndGet(delta); } if (this.jdbcUrl != null) { this.jdbcUrl = this.jdbcUrl.trim(); initFromWrapDriverUrl(); } // 重点在这里 for (Filter filter : filters) { filter.init(this); }
这句话的意思是初始化配置的Filter,其中我们启用的WallFilter就是在这里初始化的。
接下来我们进入WallFilter 的 init方法:
@Override public synchronized void init(DataSourceProxy dataSource) { if (null == dataSource) { LOG.error("dataSource should not be null"); return; } if (this.dbType == null || this.dbType.trim().length() == 0) { // dbType 取决于第一次调用此方法的数据源的类型 if (dataSource.getDbType() != null) { this.dbType = dataSource.getDbType(); } else { this.dbType = JdbcUtils.getDbType(dataSource.getRawJdbcUrl(), ""); } } if (dbType == null) { dbType = JdbcUtils.getDbType(dataSource.getUrl(), null); } if (JdbcUtils.MYSQL.equals(dbType) || // JdbcUtils.MARIADB.equals(dbType) || // JdbcUtils.H2.equals(dbType)) { if (config == null) { config = new WallConfig(MySqlWallProvider.DEFAULT_CONFIG_DIR); } // 初始化Mysql类型的防火墙 provider = new MySqlWallProvider(config); } else if (JdbcUtils.ORACLE.equals(dbType) || JdbcUtils.ALI_ORACLE.equals(dbType)) { if (config == null) { config = new WallConfig(OracleWallProvider.DEFAULT_CONFIG_DIR); }
我们发现此方法加了 synchronized,并且里面有一个关键的属性 dbType。
由于WallFilter只有一个实例,此处的dbType是成员变量,代码中判断为null时才从当前dataSource中获取;
那么问题来了,我们现在有两个 DruidDataSource
实例,那么在初始化时就会调用WallFilter的初始化,但是尽管调用多次 WallFilter
的 init
方法,但是从上面代码得知dbType仅取决于第一次 DruidDataSource
调用WallFilter 的 init 方法时 dataSource.getDbType()
的值。
分析到这里,你看出什么问题了?
是的,如果当我们第一个访问的数据源是Mysql时,那么不管后续的DruidDataSource多次调用WallFilter 的 init 方法,dbType永远不会改变。
所以导致,我们用的永远都是 MySqlWallProvider
,也就是说我们用的SQL防火墙是Mysql类型的,所以后续对SQL的语法解析也用到了MySqlLexer 词法解析器,该解析器的关键字库里面是没有USING这个关键字的,所以会报错 USING 语法错误。
解决方案
开始我也说了,本次问题是部分实例报sql 异常,这又是为什么呢?
这是因为,这和你第一次访问的数据有关,如果第一次访问的是Mysql库,那后面在访问Oracle时候就会有这个问题。
如何解决这个问题呢?这里给出两种方案。
可以把
spring.datasource.druid.filter.wall.enabled=true
这个配置设置成false 或去掉该配置,这也是最简单快捷的方式;对WallFilter和StatFilter 两个类进行拓展和相关逻辑优化,需要编程,未实施。
以上就是Druid多数据源下Sql防火墙导致异常的示例分析,小编相信有部分知识点可能是我们日常工作会见到或用到的。希望你能通过这篇文章学到更多知识。更多详情敬请关注行业资讯频道。