在查看mybatis源码的时候,只看了处理接口参数那一块,却没有查看后期处理完接口参数的过程(同时也是处理通过SqlSession调用命名空间,如下图所示),通过看了深入了解MyBatis参数这篇文章,我才知道自己的思维是多么局限,这篇文章作者也是《MyBatis从入门到精通》的作者,真正的一位大牛,我重新整理了一下我的这篇博文。代码来源:目前最新稳定版3.4.6
MapperMethod进行实例化,构造方法中调用静态内部类MethodSignature构造方法
public MapperMethod(Class> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
MethodSignature构造方法中调用ParamNameResolver构造方法。
public MethodSignature(Configuration configuration, Class> mapperInterface, Method method) {
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
if (resolvedReturnType instanceof Class>) {
this.returnType = (Class>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
this.returnType = (Class>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
this.returnType = method.getReturnType();
}
//... 省略部分代码
this.paramNameResolver = new ParamNameResolver(configuration, method);
}
ParamNameResolver代码介绍:
1. 先遍历所有不是RowBounds或者ResultHandler子类的参数
2. 先判断参数中是否存在@Param注解,存在的话hasParamAnnotation设置true
3. 将有@Param注解的参数的值赋予name
4. 没有注解的将会判断isUseActualParamName是否为true[1],如果不设置java编译保留参数的话,将默认为arg0,arg1..argn,设置的话为参数本身名称。
5. 如果是false,将得到当前names的大小,设为其值0,1,2,3,4…n。最后将参数位置和名字保存在SortedMap->names中。
举例:
public User getUserByNameAndDep(@Param(“name”)String name,String dep);
处理完之后names为:
如果是3.4.1以及之后版本,isUseActualParamName设置为true,且编译开关打开
0->name
1->dep
如果是3.4.1以及之后版本,isUseActualParamName设置为true,且编译开关未打开
0->name
1->arg1
如果是3.4.1以及之后版本,isUseActualParamName设置为false,或者是3.4.1之前版本
0->name
1->1
注意:索引位置为key,名字相同也不出错,后续的程序将会覆盖,比如:
public User getUserByNameAndDep(@Param(“dep”)String name,String dep);
如果是3.4.1以及之后版本,isUseActualParamName设置为true,且编译开关打开
0->dep
1->dep
\\...
private final SortedMap names;
public ParamNameResolver(Configuration config, Method method) {
final Class>[] paramTypes = method.getParameterTypes();
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap map = new TreeMap();
int paramCount = paramAnnotations.length;
// get names from @Param annotations
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
if (isSpecialParameter(paramTypes[paramIndex])) {
// skip special parameters
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
if (name == null) {
// @Param was not specified.
if (config.isUseActualParamName()) {
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// use the parameter index as the name ("0", "1", ...)
// gcode issue #71
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}
\\...
private static boolean isSpecialParameter(Class> clazz) {
return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz);
}
MapperMethod.execute方法中执行Object param = method.convertArgsToSqlCommandParam(args);
convertArgsToSqlCommandParam方法中调用ParamNameResolver中的getNamedParams方法:
1. 方法会判断参数是否为null,或者之前构造函数返回的位置和名称map大小是否为空,为空则返回null。
2. 然后判断参数是否存在注解以及参数个数是否为1,为1直接返回这个参数。
3. 之后param会将添加names中的所有key和value,不过位置互换,前面名称,后面参数,相同名称意味着后面的将覆盖前面的。
4. 最后param再判断param是否存在param**n**,不存在则加上GENERIC_NAME_PREFIX + String.valueOf(i + 1),也就是param1, param2, …和参数。
例如:之前的
0->dep
1->dep
最后param为
dep=Tech
param1=yiibai
param2=Tech
后面的第二个参数覆盖了第一个,若sql语句中使用{dep},将不是所加注解的第一个参数,而是第二个。
\\... 省略代码
private static final String GENERIC_NAME_PREFIX = "param";
\\...
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
final Map param = new ParamMap
返回的Object param可以有三种情况:
null
: 入参为null
或没有
时,参数转换为 参数
:没有使用@Param注解
并且只有一个
参数时Map
:使用了@Param注解或有多个参数时
,将参数转换为Map类型,并且还根据参数顺序选择性的存储了key为param1,param2…的参数。Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
然后调用使用命名空间的方法。
下面是一些org.apache.ibatis.session.defaults.DefaultSqlSession的一些方法,用来证明我在大牛文章中看到的内容。
不管是selectOne还是selectMap方法,归根结底都是通过selectList进行查询的,不管是delete还是insert方法,都是通过update方法操作的。在selectList和update中所有参数的都进行了统一的处理。
@Override
public T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
@Override
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
@Override
public int delete(String statement) {
return update(statement, null);
}
@Override
public int delete(String statement, Object parameter) {
return update(statement, parameter);
}
selectList和update他们都使用了wrapCollection这个方法转化参数
@Override
public List selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
@Override
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
wrapCollection判定了参数的类型:
1.null
将直接返回为null
2.map
(1):意味着只有一个参数,且没有注解(否则前面已经变为map)。如果对象是将Collection子类,将创建一个map对象,存入键为”collection”的键值对,如果还是list,将存入键为”list”的键值对,并返回。如果对象是数组,将创建一个map对象,存入键为”array”的键值对,并返回。
(2):如果是map,将直接返回map。
3.object
只有一个的且没有注解的简单数据类型或者实体类
private Object wrapCollection(final Object object) {
if (object instanceof Collection) {
StrictMap
根据源码,我们可以知道以下参数处理结果:
1.null
参数为null,或者没有参数
mapper.getAllUserByID();
mapper.getUserByID(null);
session.selectOne("mybatis.mybatis.model.User.GetUserByID", null);
2.map
(1):意味着只有一个参数,且没有注解(否则前面已经变为map)。如果对象是将Collection子类,将创建一个map对象,存入键为”collection”的键值对,如果还是list,将存入键为”list”的键值对,并返回。如果对象是数组,将创建一个map对象,存入键为”array”的键值对,并返回。
mapper.getUserByIds(list);
mapper.getUserByIds(set);
mapper.getUserByIds(array);
mapper.getUserByNameAndDep("小明", "开发组");
(2):如果是map,将直接返回map。
3.其他object
只有一个的且没有注解的参数,比如简单数据类型或者实体类
mapper.getUserByID(1);
mapper.insert(user);
session.selectOne("mybatis.mybatis.model.User.GetUserByID", 1);
[1] isUseActualParamName:3.4.1开始有这个特性,默认为false,3.4.2以及之后版本默认true(org.apache.ibatis.builder.xml.XMLConfigBuilder.settingsElement()中可以看到configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"),false));
),Java8支持保留参数名称,如何开启可以看这个文章。
并不是说在某些MyBatis版本不能直接使用#{0}要使用 #{arg0},而是因为没有开启Java8编译开关,而3.4.2以及后续版本的mybatis首先默认使用注解值,然后isUseActualParamName默认为true,没有注解将使用参数名称,参数名称必须在编译时开启编译开关才能保留,否则将只保留arg0,arg1,arg2…argn,而后面的names.size,也就是0,1,2,3并没有添加