项目使用了mybatis,版本为mybatis-3.2.2.jar + mybatis-spring-1.2.0.jar。今天发现项目运行中出现Invalid bound statement (not found)异常。
mybatis在启动的时候会扫描mapper,把mapper类中的方法及对应的statement语句注册到Configuration对象中。既然出现了Invalid bound statement (not found)异常,说明在运行过程中没有找到对应mapper中的方法。我们使用的是注解方式,看了对应mapper的方法,注解都有了,没问题,关联了下源码,debug的时候发现,对应mapper中有部分方法是注册了的,而报错的方法没有被注册,怀疑是代码没编译,重新编译过后,问题还在,猜测可能是mybatis在注册的时候出错了,导致后续方法没有被注册。看了下源码,mybatis在Configuration类的getMapper方法中寻找statement,那肯定会有个地方在初始化的时候注册那些statement,在Configuration类中找到相似方法public <T> void addMapper(Class<T> type),追踪进去,这个方法调用了org.apache.ibatis.binding.MapperRegistry类的addMapper方法,代码如下:
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<T>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
追踪进入org.apache.ibatis.builder.annotation.MapperAnnotationBuilder类的parse方法
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
parseStatement(method);
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
可以看到在这边获取了Mapper接口的所有方法,解析方法上的注解,继续追踪,进入parseStatement方法
void parseStatement(Method method) {
Class<?> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method);
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {
Options options = method.getAnnotation(Options.class);
final String mappedStatementId = type.getName() + "." + method.getName();
Integer fetchSize = null;
Integer timeout = null;
StatementType statementType = StatementType.PREPARED;
ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
SqlCommandType sqlCommandType = getSqlCommandType(method);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = !isSelect;
boolean useCache = isSelect;
KeyGenerator keyGenerator;
String keyProperty = "id";
String keyColumn = null;
if (SqlCommandType.INSERT.equals(sqlCommandType)) {
// first check for SelectKey annotation - that overrides everything else
SelectKey selectKey = method.getAnnotation(SelectKey.class);
if (selectKey != null) {
keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
keyProperty = selectKey.keyProperty();
} else {
if (options == null) {
keyGenerator = configuration.isUseGeneratedKeys() ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
} else {
keyGenerator = options.useGeneratedKeys() ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
keyProperty = options.keyProperty();
keyColumn = options.keyColumn();
}
}
} else {
keyGenerator = new NoKeyGenerator();
}
if (options != null) {
flushCache = options.flushCache();
useCache = options.useCache();
fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
timeout = options.timeout() > -1 ? options.timeout() : null;
statementType = options.statementType();
resultSetType = options.resultSetType();
}
String resultMapId = null;
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
resultMapId = resultMapAnnotation.value();
} else if (isSelect) {
resultMapId = parseResultMap(method);
}
assistant.addMappedStatement(
mappedStatementId,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
null, // ParameterMapID
parameterTypeClass,
resultMapId, // ResultMapID
getReturnType(method),
resultSetType,
flushCache,
useCache,
false, // TODO issue #577
keyGenerator,
keyProperty,
keyColumn,
null,
languageDriver);
}
}
方法第三行,获取sqlSource,debug到这边的时候有出错(不是报 Invalid bound statement (not found)的犯法),去看了下报错的方法,原来报错的方法使用的是@UpdateProvider注解,但是属性type对应的类中没有method指定的方法,所以在执行代码
getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
时报了,但是项目启动的时候没错误日志,正常启动成功了,再看了下代码
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
try {
Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
if (sqlAnnotationType != null) {
if (sqlProviderAnnotationType != null) {
throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
}
Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
} else if (sqlProviderAnnotationType != null) {
Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation);
}
return null;
} catch (Exception e) {
throw new BuilderException("Could not find value method on SQL annotation. Cause: " + e, e);
}
}
getSqlSourceFromAnnotations方法中会抛出BuilderException,而在之前的parse方法中捕获的是IncompleteElementException
for (Method method : methods) {
try {
parseStatement(method);
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
IncompleteElementException是BuilderException的子类,所以这边并不会被捕获,会继续往上抛,导致循环退出,mapper中的后续方法跳过注册,因此在代码执行的时候没有找到对应的statement。
唉,真是坑爹啊。