最近的几个bug-mybatis

接着上回,这回说说mybatis。

mybatis算是互联网公司和传统企业都非常普遍使用的一款ORM

之所以想重开一篇是写的时候发现想说的有点多,其实同一个报错可能引起的原因有很多,单列出error log然后报解决方法并不能很清晰的认识问题,而且我也希望在记录的时候不止是解决某个bug本身,而是把这个过程前因后果甚至收获记录下来,共享加共勉。

下面过程会涉及一些mybatis源码解读,想看bug描述和解决的直接拉到最后

好了,言归正传。

关键词:

mybatis sql
There is no getter for property named 'xxx' in 'class java.lang.String

前戏

前面说过最近在做一个通过MQ异步处理数据表同步的工作,业务应用端发送消息的内容是个json字符串,里面包含一条数据快照和对应操作的表名。
我这边就要根据表名找到表名对应的实体类,对应的dao层接口,和要执行的mapper.xml中设置的sql。甚至还有表的主键。阿西巴!

这些个乱七八糟的关系最好还是消费者启动的时候就加载到个map里,话说没有什么数据存储是一个map解决不了的,如果有,那就两个map!

通过表名找主键

其实表名对主键的关系还算好找,而且手段也有很多,先说下通过sql怎么找吧。

sql查询方式

sql查询方式的话其实就是执行一条sql语句呗。不过oracle和mysql的略有区别。

mysql

1
show keys from table where key_name = 'PRIMARY'

oracle就麻烦一些,需要联查

oracle

1
2
3
select COLUMN_NAME from user_cons_columns where constraint_name = 
(select constraint_name from user_constraints
where table_name = 'xxx' and constraint_type ='P');

结果可能多条,解析ResultSet获取即可。

jdbc获取

不过在代码里获取的话执行sql要建立数据库连接,其实建立连接之后驱动包就有供来获取主键的API了
Connection类中有一个getMetaData()方法,返回的是一个DatabaseMetaData接口,摘取部分方法如下:
DatabaseMetaData

可以看到这个这个接口里其实定义了很多对获取数据库元数据信息的操作,具体的逻辑由实现类来实现,其实对应的不同数据库的驱动包的类就是com.mysql.jdbc.DatabaseMetaDataOracleDatabaseMetaData这两个实现类。其中获取主键的方法就是定义在接口的getPrimaryKeys(String catalog,String schema,String table)
说下这个方法的几个参数含义吧:

  • catalog: 就是log名字,传空的话就不计log,这个参数一般传空值即可
  • schema: 这个字段对于mysql来说是数据库实例名,对于oracle来说是大写的用户名,这个参数其实也可以为空,但我们目的是根据表名找主键,所以没必要全实例扫描
  • table: 这个就是表名了,oracle要大写!

我们看mysql下的部分源码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
StringBuilder queryBuf = new StringBuilder("SHOW KEYS FROM ");
queryBuf.append(StringUtils.quoteIdentifier(table, DatabaseMetaData.this.quotedId, DatabaseMetaData.this.conn.getPedantic()));
queryBuf.append(" FROM ");
queryBuf.append(StringUtils.quoteIdentifier(catalogStr, DatabaseMetaData.this.quotedId, DatabaseMetaData.this.conn.getPedantic()));

rs = stmt.executeQuery(queryBuf.toString());

TreeMap<String, byte[][]> sortMap = new TreeMap<String, byte[][]>();

while (rs.next()) {
String keyType = rs.getString("Key_name");

if (keyType != null) {
if (keyType.equalsIgnoreCase("PRIMARY") || keyType.equalsIgnoreCase("PRI")) {
byte[][] tuple = new byte[6][];
tuple[0] = ((catalogStr == null) ? new byte[0] : s2b(catalogStr));
tuple[1] = null;
tuple[2] = s2b(table);

String columnName = rs.getString("Column_name");
tuple[3] = s2b(columnName);
tuple[4] = s2b(rs.getString("Seq_in_index"));
tuple[5] = s2b(keyType);
sortMap.put(columnName, tuple);
}
}
}

可以看到它的实现其实还是通过jdbc来执行一条show keys from table的查询。

Oracle的源码实现也类似:

1
2
3
4
5
PreparedStatement var4 = this.connection.prepareStatement("SELECT NULL AS table_cat,\n       c.owner AS table_schem,\n       c.table_name,\n       c.column_name,\n       c.position AS key_seq,\n       c.constraint_name AS pk_name\nFROM all_cons_columns c, all_constraints k\nWHERE k.constraint_type = \'P\'\n  AND k.table_name = :1\n  AND k.owner like :2 escape \'/\'\n  AND k.constraint_name = c.constraint_name \n  AND k.table_name = c.table_name \n  AND k.owner = c.owner \nORDER BY column_name\n");
var4.setString(1, var3);
var4.setString(2, var2 == null?"%":var2);
OracleResultSet var5 = (OracleResultSet)var4.executeQuery();
var5.closeStatementOnClose();

刚开始看到接口里有直接获取主键的方法的时候还以为有什么黑科技。。没想到也是这么查的。

mybatis获取

不过既然我们用了mybatis框架对吧,就可以抄一下近路了。
想想,在mapper.xml的配置文件里面?是不是有个resultMap的节点已经有了主键信息了?
mybatis加载的时候已经把这些信息全部加载到org.apache.ibatis.session.Configuration里面。这个类包含了mybatis所有的配置信息,这个框架后面用到的全部配置项都是从这个类里面取得的,而且这个类也给我们提供了大多数的成员变量的get方法,使我们很容易得到想要的数据。我们要获取的信息是在其中的resultMaps属性中

1
protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");

resultMaps的信息如下:

key: com.jiedaibao.databus.dao.mapper.xxxMapper.resultMap
value: ResultMap

可以看到resultMaps其实就是个org.apache.ibatis.mapping.ResultMap的集合,它是存储所有mapper.xml文件定义的resultMap节点信息的集合,我们可以通过如下方式获取resultMaps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Resource
private SqlSessionFactory sqlSessionFactory;//初始化beanFactory的时候,Mybatis的配置文件会注册该bean

...

Collection<ResultMap> resultMaps = sqlSessionFactory.getConfiguration().getResultMaps();
Iterator<ResultMap> iterator = resultMaps.iterator();
while (iterator.hasNext()) {
Object object = iterator.next();
if (!(object instanceof ResultMap))
continue;
ResultMap resultMap = (ResultMap) object;
...
}

PS:resultMaps里面会多一条记录,现在还没明白是怎么加进来的,其结构如下:

1
2
key = "resultMap"
value=Configuration$StrictMap$Ambiguity

如果出现了上述记录,在迭代循环里面就要判断一下当前迭代 instanceof ResultMap。

OK,到这儿呢,我们就获取了ResultMap对象,可以看一下它里面都有哪些东西:

1
2
3
4
5
6
7
8
private String id;    //id,跟当前迭代的key相同,com.jiedaibao.databus.dao.mapper.xxxMapper.resultMap
private Class<?> type; //resultMap节点配置对应的type类,一般为对应的数据库表对象类
private List<ResultMapping> resultMappings; //所有列的信息
private List<ResultMapping> idResultMappings; //主键列的信息
private List<ResultMapping> constructorResultMappings; //构造函数所需参数信息
private List<ResultMapping> propertyResultMappings; //非构造函数所需参数信息
private Set<String> mappedColumns; //resultMap节点下所有的column节点的值
...

我们要的主键信息呢,就在idResultMappings中,它是ResultMapping类的集合,ResultMapping类下有两个属性propertycolumn,分别就对应xml文件中resultMap节点下的propertycolumn的值。

xml配置

我们知道Mybatis在配置xml中的sql的时候可以选择三种声明的编译类型,对应的是statementType的值:

  • PRAPARED: 预编译声明
    • 默认的是用PRAPARED,预编译,相当于java.sql.PreparedStatement 解析关键字符为#{}
  • STATEMENT: 非预编译声明
    • STATEMENT是非预编译声明,相当于java.sql.Statement,使用非预编译的时候,解析的关键字符为${},非预编译的时候可以传入动态的表名,Mybatis会把${}不经任何加工替换为参数的值,这里有个主意的地方就是,不会经过任何处理,也就是说不会根据字段类型的不同而添加上引号等,这些都需要提前自己根据不同字段类型加上引号处理。
      CALLABLE: 调用存储过程

OK,马上进入正题(阿西巴!这才开始进入正题?),当时我的xml配置如下:

1
2
3
<select id = "select" resultMap="resultMap" statementType="STATEMENT">
select * from table ${condition}
</select>

然后dao层接口如下:

1
List<Model> select(@Param("condition")String condition);

调用的时候传入的参数conditionwhere id = xxx(此处为方便测试,简略了查询条件,实际可能比这个要复杂的多)

问题

起因

正常来说这样的配置调用的时候不该出现问题,Mybatis在解析Sql的时候应该解析成:
select * from table where id = xxx
但实际执行的时候报错:

1
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException:There is no getter for property named 'condition' in 'class java.lang.String'

这是什么情况?明明传入了参数。

后来想到可能是我调用的问题,因为我不是直接通过注入的dao层bean来调用方法的,而是通过反射。
没错,是反射:

1
2
3
4
5
Proxy proxy = (Proxy) applicationContext.getBean(daoName);
//获取对象中的查询方法
Method method = proxy.getClass().getMethod(methodName, new class[]{String.class});
//执行方法返回对象
Object object = Proxy.getInvocationHandler(proxy).invoke(proxy, method, new Object[]{condition});

daoName是注入后dao的bean id,methodName是dao接口下的方法名,condition即传入的条件。

源码解析

我们来分析一下Mybatis生成sql语句的源码逻辑:
Mybatis执行sql语句是通过生成一个org.apache.ibatis.binding.MapperProxy的代理对象来调用方法的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//如果方法是在object类下则直接调用
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
//生成MapperMethod对象并且进行缓存
final MapperMethod mapperMethod = cachedMapperMethod(method);
//执行dao接口中的方法,即执行sql逻辑
return mapperMethod.execute(sqlSession, args);
}

private MapperMethod cachedMapperMethod(Method method) {
//先从缓存里面取
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
//缓存没取到的话就new一个
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
//把new的mapperMethod写入缓存
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}

org.apache.ibatis.binding.MapperMethod类是Mybatis缓存接口方法的一个处理类,看看它的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
//Mapper方法构造函数
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
//执行sql的类型,主要是根据xml配置中sql部分配置的增删改查标签来确定
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, method);
}

//执行dao接口中的方法,即执行sql逻辑
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//判断增删改查类型,然后根据类型执行相应的增删改查方法
if (SqlCommandType.INSERT == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
//是否查询多个值
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
//是否查询结果是map
result = executeForMap(sqlSession, args);
} else {
//转换执行方法所需参数,args的值在上文中的话就是" where id = xxx "
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
} else {
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
* " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}

/**
* 方法签名信息的匿名类
*/

public static class MethodSignature {
private final boolean returnsMany;
private final boolean returnsMap;
private final boolean returnsVoid;
private final Class<?> returnType;
private final String mapKey;
private final Integer resultHandlerIndex;
private final Integer rowBoundsIndex;
private final SortedMap<Integer, String> params;
private final boolean hasNamedParameters;

//构造函数
public MethodSignature(Configuration configuration, Method method) throws BindingException {
this.returnType = method.getReturnType();
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray());
this.mapKey = getMapKey(method);
this.returnsMap = (this.mapKey != null);
//是否已有指定名称的参数
this.hasNamedParameters = hasNamedParams(method);
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
this.params = Collections.unmodifiableSortedMap(getParams(method, this.hasNamedParameters));
}

//转换参数
public Object convertArgsToSqlCommandParam(Object[] args) {
final int paramCount = params.size();
if (args == null || paramCount == 0) {
return null;
//此处注意!上述场景在调用反射的时候hasNamedParameters = false;
} else if (!hasNamedParameters && paramCount == 1) {
return args[params.keySet().iterator().next()];
} else {
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : params.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// issue #71, add param names as param1, param2...but ensure backward compatibility
final String genericParamName = "param" + String.valueOf(i + 1);
if (!param.containsKey(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
}

所以看来上述转换参数的方法convertArgsToSqlCommandParam中的关键是这个hasNamedParameters的值,它是在上述构造函数中生成的,代码块儿为this.hasNamedParameters = hasNamedParams(method);:

1
2
3
4
5
6
7
8
9
10
11
12
13
private boolean hasNamedParams(Method method) {
boolean hasNamedParams = false;
final Object[][] paramAnnos = method.getParameterAnnotations();
for (Object[] paramAnno : paramAnnos) {
for (Object aParamAnno : paramAnno) {
if (aParamAnno instanceof Param) {
hasNamedParams = true;
break;
}
}
}
return hasNamedParams;
}

java.lang.reflect.Method类里面的getParameterAnnotations()

1
2
3
4
@Override
public Annotation[][] getParameterAnnotations() {
return sharedGetParameterAnnotations(parameterTypes, parameterAnnotations);
}

sharedGetParameterAnnotationsMethod父类Executable里面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
Annotation[][] sharedGetParameterAnnotations(Class<?>[] parameterTypes,
byte[] parameterAnnotations) {
int numParameters = parameterTypes.length;
if (parameterAnnotations == null)
return new Annotation[numParameters][0];
/** 转换有注解的参数 **/
Annotation[][] result = parseParameterAnnotations(parameterAnnotations);
/** result长度与参数长度不匹配的话做校验 **/
if (result.length != numParameters)
handleParameterNumberMismatch(result.length, numParameters);
return result;
}

上述代码用之前错误的配置的话传入的parameterAnnotations参数为空,而parameterTypes则是包含一个String类的class数组。所以此处result的值是空,那么前面hasNamedParameters的值就是false了。那么上述convertArgsToSqlCommandParam(args)这个方法的返回值就是where id = xxx了。
那么接下来就是执行result = sqlSession.selectOne(command.getName(), param)这句,层层调用下来,最后是调到org.apache.ibatis.session.defaults.DefaultSqlSession中如下代码:

1
2
3
4
5
6
7
8
9
10
11
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
return result;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

参数中statement就是方法全路径名,parameter就是上面那个param的值where id = xxx
第一句是获取MappedStatement对象,里面有一些配置信息。warpCollection(parameter)的话是组织一下参数,主要处理一下参数是List或者Array的情况,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
private Object wrapCollection(final Object object) {
if (object instanceof List) {
StrictMap<Object> map = new StrictMap<Object>();
map.put("list", object);
return map;
} else if (object != null && object.getClass().isArray()) {
StrictMap<Object> map = new StrictMap<Object>();
map.put("array", object);
return map;
}
return object;
}

因为我们的参数是String,所以返回的object就还是where id = xxxexecutor.query()下一步是类org.apache.ibatis.executor.CachingExecutor的如下方法:

1
2
3
4
5
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

上述中参数resultHandler是空。执行第一个方法org.apache.ibatis.mapping.MappedStatement里的ms.getBoundSql()

1
2
3
4
5
6
7
public BoundSql getBoundSql(Object parameterObject) {
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.size() <= 0) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
}

sqlSource.getBoundSql(parameterObject)执行的是类org.apache.ibatis.scripting.xmltags.DynamicSqlSource里的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public BoundSql getBoundSql(Object parameterObject) {
/** **/
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}

DynamicContext的部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DynamicContext {

public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";

static {
OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}

private final ContextMap bindings;
private final StringBuilder sqlBuilder = new StringBuilder();
private int uniqueNumber = 0;

public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
...
}

前面调用DynamicContext的构造函数主要是对成员变量bindings的处理,ContextMap继承自HashMap,这里处理完之后bindings的值是两个,_parameter : where id = xxx_databaseId : null
回到getBoundSql(Object parameterObject)中,下一步调用到了org.apache.ibatis.scripting.xmltags.MixedSqlNode里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  public class MixedSqlNode implements SqlNode {
private List<SqlNode> contents;

public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}

public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
}

上面这个List的成员变量contents,里面存的是解析xml文件的时候取出来的sql语句本体集合,上述情况就是select * from table ${condition}apply(context)这个方法是接口org.apache.ibatis.scripting.xmltags.SqlNode里的方法,此处实现调用的是类org.apache.ibatis.scripting.xmltags.TextSqlNode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class TextSqlNode implements SqlNode {
private String text;

public TextSqlNode(String text) {
this.text = text;
}

public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
}

public boolean apply(DynamicContext context) {
/** 获取解析sql中动态字段的前后缀和文本信息 **/
GenericTokenParser parser = createParser(new BindingTokenParser(context));
/** parser.parse(text)是进行sql解析,把sql中字段替换为相应参数的值 **/
context.appendSql(parser.parse(text));
return true;
}

private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}

private static class BindingTokenParser implements TokenHandler {

private DynamicContext context;

public BindingTokenParser(DynamicContext context) {
this.context = context;
}

public String handleToken(String content) {
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put("value", parameter);
}
Object value = OgnlCache.getValue(content, context.getBindings());
return (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
}
}

private static class DynamicCheckerTokenParser implements TokenHandler {

private boolean isDynamic;

public boolean isDynamic() {
return isDynamic;
}

public String handleToken(String content) {
this.isDynamic = true;
return null;
}
}
}

我们可以看到apply方法第一句是取得执行sql的动态字段的前后缀信息和内容,这里就是${", "},然后下面那个parser.parse(text)的方法是真正解析sql,把动态字段替换为相应的值,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public String parse(String text) {
StringBuilder builder = new StringBuilder();
if (text != null && text.length() > 0) {
char[] src = text.toCharArray();
int offset = 0;
int start = text.indexOf(openToken, offset);
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// the variable is escaped. remove the backslash.
builder.append(src, offset, start - 1).append(openToken);
offset = start + openToken.length();
} else {
int end = text.indexOf(closeToken, start);
if (end == -1) {
builder.append(src, offset, src.length - offset);
offset = src.length;
} else {
builder.append(src, offset, start - offset);
offset = start + openToken.length();
String content = new String(src, offset, end - offset);
builder.append(handler.handleToken(content));//异常的地方!
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
}
return builder.toString();
}

上述方法呢就是解析sql里动态字段的部分了,其实就是找出前后缀标志符号的offset,然后把里面的值替换为前面传入的对应的参数对象中。handler.handleToken(content)方法的实现在前面那个TextSqlNode类里。我们再贴出代码块来看一下:

1
2
3
4
5
6
7
8
9
10
public String handleToken(String content) {
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put("value", parameter);
}
Object value = OgnlCache.getValue(content, context.getBindings());//异常代码
return (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
}

其中参数content是传入动态字段标志位符号内的参数,在上述sql语句select * from table ${condition}中,content=“condition”,接下来调到了org.apache.ibatis.scripting.xmltags.OgnlCache这个类里的方法,作用是从context.getBindings()中取得content对应的值,我们前面分析过context.getBindings()的值如下:

1
2
_parameter -> where id = xxx
_databaseId -> null

可以看到这里面根本找不到“condition”相关信息,所以就抛出了异常:

1
2
3
org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database. Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'condition' in 'class java.lang.String'
### Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'condition' in 'class java.lang.String'

其实分析知道最后关键是DynamicContext类里bindings的值,异常的点找到了,那么要怎么解决呢?之前对Mybatis并不算很熟,所以这里我采取的是对错对比法,前面是因为反射调用Dao层方法引起了,那么我们正常走一遍调用流程,来看一下有什么不同。

首先是前面MapperMethod$MethodSignature类中的hasNamedParameters的值为true,那么在下述方法的调用中就会走另一个分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Object convertArgsToSqlCommandParam(Object[] args) {
final int paramCount = params.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasNamedParameters && paramCount == 1) {
return args[params.keySet().iterator().next()];
} else {
//hasNamedParameters此处为true,就会走这个分支
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : params.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// issue #71, add param names as param1, param2...but ensure backward compatibility
final String genericParamName = "param" + String.valueOf(i + 1);
if (!param.containsKey(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}

上面这段else的分支主要是对注解参数进行组装,可以看到上述代码每个注解参数都会有两条记录,分别是以注解名为key的值和一条param[1,2...n+1]为key的值。上述中返回的param就是:

1
2
condition -> where id = xxx
param1 -> where id = xxx

那么其实我们取值的时候实际通过注解名或者param[1,2...n+1]都可以取到。

这里就是关键了,前面通过反射调用的时候返回的param是字符串,而这里正常调用的时候是个Map

那么之后在DynamicContext构造函数里面成员变量bindings的值就是如下所示:

1
2
3
4
5
6
7
8
0 = {HashMap}
key = "_parameter"
value = {MapperMethod$ParamMap} size = 2
0 = "condition" -> " where id = xxx "
1 = "param1" -> " where id = xxx "
1 = {HashMap}
key = "_databaseId"
value = null

如此的话后面在handler.handleToken(content)方法里面就能根据动态参数condition找到相应的值进行替换。

sql语句生成的源码分析至此结束。

解决

通过分析发现通过反射调用的话注解@Param("condition")并不会生效,只是作为一个参数传入Mybatis。
那么这个问题如何解决呢,有取巧的方法就是把代码中的动态参数替换为_parameter,由于在构造bindings的时候Mybatis内置的参数就是这个,所以如下更改能奏效:

1
2
3
<select id = "select" resultMap="resultMap" statementType="STATEMENT">
select * from table ${_parameter}
</select>

但其实这种非预编译的方式并不是太推荐,这样做一呢,是需要在框架之前显示的进行防止sql注入的编码,也无法预编译。二呢是不能进行预编译。

暂且先分析到这里。