由于项目中需要使用mysql的强制索引,于是乎就在mapper.xml中添加一个,结果运行后报错:
Caused by: com.kxtx.security.data.exception.DataPermissionException: SQL 语句解析失败,请检查:SELECT id, order_no, order_type, waybill_id, waybill_no, extend_waybill_id FROM TMS_ORDER FORCE INDEX (idx_to_exwaybill_id) WHERE extend_waybill_id IN (?) AND order_type = ? at com.kxtx.security.data.handler.HandlerUtil.handlerSql(HandlerUtil.java:34) ~[gillion-web-quick-2.1.6.17.RELEASE-pg.jar:na] at com.kxtx.security.data.DataPermissionInterceptor.intercept(DataPermissionInterceptor.java:89) ~[gillion-web-quick-2.1.6.17.RELEASE-pg.jar:na] at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:57) ~[mybatis-3.1.1.jar:3.1.1] at com.sun.proxy.$Proxy212.prepare(Unknown Source) ~[na:na] at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:70) ~[mybatis-3.1.1.jar:3.1.1] at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:56) ~[mybatis-3.1.1.jar:3.1.1] at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:267) ~[mybatis-3.1.1.jar:3.1.1] at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:141) ~[mybatis-3.1.1.jar:3.1.1] at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:105) ~[mybatis-3.1.1.jar:3.1.1] at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:81) ~[mybatis-3.1.1.jar:3.1.1] at sun.reflect.GeneratedMethodAccessor139.invoke(Unknown Source) ~[na:na] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_151] at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_151] at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:59) ~[mybatis-3.1.1.jar:3.1.1] ... 124 common frames omitted
这里分享一个快速定位及解决bug的经验:
- 首先,尽可能的认真读故障现场迹象,了解症状
- 对正在症状有一个初步的判断,不要急着立马度娘,要有自己的判断(低价代码不严谨问题 or API用法问题 or 高级用法不支持问题)
- 可以运用排除法,定位属于哪一类,很显然它属于高级用法不支持问题(之前一直是好好的)
- 需要到该组件或类库的社区中找答案
这里有一点要知道:jdbc的PreparedStatement,它本质上是不解析sql的,所谓客户端预编译是假的,其实还是通过服务端预编译(mysql 引擎)完成的。
如果对mybatis源码足够了解的同学,可能知道sql的执行链中有个自定义的plugin(DataPermissionInterceptor)引起的,继续追踪,看它干了什么呢
/**
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="plugins">
<list>
<!--数据权限拦截-->
<bean class="com.kxtx.security.data.DataPermissionInterceptor">
<property name="excludes">
<list>
<value>*NoAcl*</value>
</list>
</property>
</bean>
</list>
</property>
*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})
public class DataPermissionInterceptor implements Interceptor {
private Logger LOGGER = LoggerFactory.getLogger(DataPermissionInterceptor.class);
private boolean showSql = false;
//排除不进行权限控制的Mapper方法,使用正则来匹配
private List<String> excludes = Lists.newArrayList();
@Override
public Object intercept(Invocation invocation) throws InvocationTargetException, IllegalAccessException, DataPermissionException {
if (invocation.getTarget() instanceof RoutingStatementHandler) {
RoutingStatementHandler statementHandler = (RoutingStatementHandler) invocation.getTarget();
MetaObject metaStatementHandler = MetaObject.forObject(statementHandler);
BoundSql boundSql = statementHandler.getBoundSql();
MappedStatement mappedStatement = loadMapperStatement(statementHandler);
String mapperId = mappedStatement.getId();
if (isNeedPermission(mapperId)) {
String sql = boundSql.getSql();
List<ParametersNode> parametersNodes = loadParamsNode(boundSql);
sql = HandlerUtil.handlerSql(sql, parametersNodes);
metaStatementHandler.setValue("delegate.boundSql.sql", sql);
if (showSql) {
LOGGER.info("Current execute SQL is: \n" + boundSql.getSql());
}
}
}
return invocation.proceed();
}
/**
* 检查是否需要权限验证
*
* @param mapperId
*
* @return
*/
private boolean isNeedPermission(String mapperId) {
for (String exclude : excludes) {
Pattern pattern = Pattern.compile(exclude);
Matcher matcher = pattern.matcher(mapperId);
if (matcher.matches()) {
return false;
}
}
return true;
}
private List<ParametersNode> loadParamsNode(BoundSql boundSql) {
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
List<ParametersNode> parametersNodes = Lists.newArrayList();
if (parameterMappings.size() > 0) {
if (boundSql.getParameterObject() instanceof String) {
String name = parameterMappings.get(0).getProperty();
Object value = boundSql.getParameterObject();
parametersNodes.add(new ParametersNode(name, value));
}
else {
for (ParameterMapping parameterMapping : parameterMappings) {
String name = parameterMapping.getProperty();
Object value = boundSql.getAdditionalParameter(name);
if (value == null) {
Object objectValue = boundSql.getAdditionalParameter("_parameter");
if (objectValue != null && !(objectValue instanceof Map) && !(objectValue instanceof List)) {
try {
value = FieldUtils.getField(objectValue.getClass(), name, true).get(objectValue);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
ParametersNode node = new ParametersNode(name, value);
parametersNodes.add(node);
}
}
}
return parametersNodes;
}
private MappedStatement loadMapperStatement(RoutingStatementHandler routingStatementHandler) throws DataPermissionException {
Field field = FieldUtils.getField(routingStatementHandler.getClass(), "mappedStatement", true);
try {
if (field == null) {
Field delegate = FieldUtils.getField(routingStatementHandler.getClass(), "delegate", true);
if (delegate != null) {
StatementHandler delegateHandler = (StatementHandler) delegate.get(routingStatementHandler);
field = FieldUtils.getField(delegateHandler.getClass(), "mappedStatement", true);
if (field != null) {
return (MappedStatement) field.get(delegateHandler);
}
}
}
return (MappedStatement) field.get(routingStatementHandler);
} catch (IllegalAccessException e) {
throw new DataPermissionException("无法获取对应的Mapper Statement");
}
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
public List<String> getExcludes() {
return excludes;
}
public void setExcludes(List<String> excludes) {
if (!excludes.contains("*NoAcl*")) {
excludes.add("*NoAcl*");
}
for (String exclude : excludes) {
exclude = exclude.replaceAll("\\*", "[\\\\w|.]*[\\\\w|.]");
this.excludes.add(exclude);
}
}
public boolean isShowSql() {
return showSql;
}
public void setShowSql(boolean showSql) {
this.showSql = showSql;
}
}
public class HandlerUtil {
private final static Logger LOGGER = LoggerFactory.getLogger(HandlerUtil.class);
/**
* 处理进行权限判断的SQL操作语句
* @param sql
* @return
* @throws com.gfa4j.security.data.exception.DataPermissionException
*/
public static String handlerSql(String sql,List<ParametersNode> nodes) throws DataPermissionException{
Statement statement = null;
try {
statement = new CCJSqlParserManager().parse(new StringReader(sql));
HandlerFactory.buildHandler(statement).handler(statement,nodes);
return statement.toString();
} catch (JSQLParserException e) {
LOGGER.error("SQL 语句解析失败,请检查:"+sql);
throw new DataPermissionException("SQL 语句解析失败,请检查:"+sql);
}
}
}
找到了,有个类库 jsqlparser 报错了,but?exception呢,异常被吃掉了(这样处理就很不专业,增加排查难度),真实的异常是:
net.sf.jsqlparser.JSQLParserException
at net.sf.jsqlparser.parser.CCJSqlParserManager.parse(CCJSqlParserManager.java:40)
at com.example.driver.TmsSqlParser_test.sqlParse(TmsSqlParser_test.java:94)
at com.example.driver.TmsSqlParser_test.go_hint(TmsSqlParser_test.java:17)
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:497)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
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:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Caused by: net.sf.jsqlparser.parser.ParseException: Encountered " "INDEX" "INDEX "" at line 1, column 36.
Was expecting one of:
<EOF>
"JOIN" ...
"LEFT" ...
"CROSS" ...
"FULL" ...
"WHERE" ...
"GROUP" ...
"INNER" ...
"OUTER" ...
"RIGHT" ...
"HAVING" ...
"NATURAL" ...
"START" ...
"CONNECT" ...
";" ...
"," ...
"LEFT" ...
"RIGHT" ...
"FULL" ...
"NATURAL" ...
"CROSS" ...
"OUTER" ...
"INNER" ...
"JOIN" ...
"," ...
"WHERE" ...
"START" ...
"CONNECT" ...
"GROUP" ...
"HAVING" ...
at net.sf.jsqlparser.parser.CCJSqlParser.generateParseException(CCJSqlParser.java:9257)
at net.sf.jsqlparser.parser.CCJSqlParser.jj_consume_token(CCJSqlParser.java:9130)
at net.sf.jsqlparser.parser.CCJSqlParser.Statement(CCJSqlParser.java:63)
at net.sf.jsqlparser.parser.CCJSqlParserManager.parse(CCJSqlParserManager.java:38)
... 29 more
Caused by: net.sf.jsqlparser.parser.ParseException: Encountered " "INDEX" "INDEX "" at line 1, column 36.
Was expecting one of:
<EOF>
"JOIN" ...
"LEFT" ...
"CROSS" ...
"FULL" ...
"WHERE" ...
"GROUP" ...
"INNER" ...
"OUTER" ...
"RIGHT" ...
"HAVING" ...
"NATURAL" ...
"START" ...
"CONNECT" ...
";" ...
"," ...
"LEFT" ...
"RIGHT" ...
"FULL" ...
"NATURAL" ...
"CROSS" ...
"OUTER" ...
"INNER" ...
"JOIN" ...
"," ...
"WHERE" ...
"START" ...
"CONNECT" ...
"GROUP" ...
"HAVING" ...
at net.sf.jsqlparser.parser.CCJSqlParser.generateParseException(CCJSqlParser.java:9257)
at net.sf.jsqlparser.parser.CCJSqlParser.jj_consume_token(CCJSqlParser.java:9130)
at net.sf.jsqlparser.parser.CCJSqlParser.Statement(CCJSqlParser.java:63)
at net.sf.jsqlparser.parser.CCJSqlParserManager.parse(CCJSqlParserManager.java:38)
看红色部分,到官方找答案:https://github.com/JSQLParser/JSqlParser/pull/429
看截图,在1.1之后的版本是支持的,确认下项目中使用的是0.9的版本。
最后,升级下版本,问题得到解决?升级之后发现,0.9的有些功能之前好的,1.1之后就报错了。
怎么破?
这里就要强调一点:所有的关键组件一定可插拔的,所有关键功能点一定是可监听的,类设计上可拓展性很多时候要具备一定的前瞻性!
那这个问题就简单了,在执行该插件是把该sql排查出去就可以了(把mapper.xml中的该方法的通配规则添加到excludes中)。