MyBatis源码分析1 参数映射分析
首先我们拿出之前的代码,在如图位置打上断点,开始调试
我们规定了一个mapper接口,而调用了mapper接口的getEmpByIdAndLastName,我们并没有实现这个接口,这是因为Mybatis会为这个接口创建一个代理对象,最终都是代理对象去调用实现。
在执行这个方法之前,代码会先来到MapperProxy.class中的invoke方法
-
public class MapperProxy<T> implements InvocationHandler, Serializable
我们先来看看MapperProxy这个类,它实现了InvocationHandler这个接口,这个接口就是为了实现动态代理,使用这个接口,就必须实现invoke方法,
-
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
-
{
-
try
-
{
-
if (Object.class.equals(method.getDeclaringClass())) {
-
return method.invoke(this, args);
-
}
-
if (isDefaultMethod(method)) {
-
return invokeDefaultMethod(proxy, method, args);
-
}
-
}
-
catch (Throwable t)
-
{
-
throw ExceptionUtil.unwrapThrowable(t);
-
}
-
MapperMethod mapperMethod = cachedMapperMethod(method);
-
return mapperMethod.execute(this.sqlSession, args);
-
}
我们当前执行的方法是getEmpByIdAndLastName,此方法第二个参数,就是这个。
接来分析第5行:
这个代理对象是由我们EmployeeMapper接口创建的,因此是用有该接口的方法,但同时也拥有Object类中的方法,我们无需对这些方法进行增强,因此放行,这就是第一个if的作用。如果不是这种Object下的方法我们会来到第16行,先将这个方法包装成MapperMethod,然后执行17行代码,我们可以尝试在17行添加上断点,这是进入方法之前的参数为
-
public Object execute(SqlSession sqlSession, Object[] args) {
-
Object result;
-
switch (command.getType()) {
-
case INSERT: {
-
Object param = method.convertArgsToSqlCommandParam(args);
-
result = rowCountResult(sqlSession.insert(command.getName(), param));
-
break;
-
}
-
case UPDATE: {
-
Object param = method.convertArgsToSqlCommandParam(args);
-
result = rowCountResult(sqlSession.update(command.getName(), param));
-
break;
-
}
-
case DELETE: {
-
Object param = method.convertArgsToSqlCommandParam(args);
-
result = rowCountResult(sqlSession.delete(command.getName(), param));
-
break;
-
}
-
case SELECT:
-
if (method.returnsVoid() && method.hasResultHandler()) {
-
executeWithResultHandler(sqlSession, args);
-
result = null;
-
} else if (method.returnsMany()) {
-
result = executeForMany(sqlSession, args);
-
} else if (method.returnsMap()) {
-
result = executeForMap(sqlSession, args);
-
} else if (method.returnsCursor()) {
-
result = executeForCursor(sqlSession, args);
-
} else {
-
Object param = method.convertArgsToSqlCommandParam(args);
-
result = sqlSession.selectOne(command.getName(), param);
-
}
-
break;
-
case FLUSH:
-
result = sqlSession.flushStatements();
-
break;
-
default:
-
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;
-
}
在执行之前这个方法做出了一个判断,当是插入时做何动作,当时更新时做了何动作,增删改都相同,每个在调用之前,都会调用method.convertArgsToSqlCommandParam(args);先将我们传入的参数转化为SQL命令可用的参数,返回一个Object对象,接着看一下select,我们属于最后一个,返回单个对象,它也同样的执行method.convertArgsToSqlCommandParam(args);接下来调用的依旧是底层的sqlSession.selectOne也就是我们第一章所提到的查找的方案一,只不过这里的param是我们传递方法中的几个零散的参数转换过来的,而第一种方法是我们直接写的,接下来看看这个转换是如何实现的:
-
public Object convertArgsToSqlCommandParam(Object[] args) {
-
return paramNameResolver.getNamedParams(args);
-
}
发现该方法调用了paramNameResolver下的getNamedParams方法,我们再观察一下这个方法是做什么的:
-
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<String, Object> param = new ParamMap<Object>();
-
int i = 0;
-
for (Map.Entry<Integer, String> entry : names.entrySet()) {
-
param.put(entry.getValue(), args[entry.getKey()]);
-
// add generic param names (param1, param2, ...)
-
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
-
// ensure not to overwrite parameter named with @Param
-
if (!names.containsValue(genericParamName)) {
-
param.put(genericParamName, args[entry.getKey()]);
-
}
-
i++;
-
}
-
return param;
-
}
这里我们发现,这就是为什么我们在多个参数时,不做任何修改,就在映射文件写上属性会出错的原因,因为这里默认给的就是param1、param2…..(观察第10行的for循环)
我们着重分析一下这个方法:
name:
final int paramCount = names.size()
发现name中已经有值了,正是我们两个参数,那它是如何获取到这个值呢?在这句上我们再添加断点,
-
private final SortedMap<Integer, String> names;
-
-
private boolean hasParamAnnotation;
-
-
public ParamNameResolver(Configuration config, Method method) {
-
final Class<?>[] paramTypes = method.getParameterTypes();
-
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
-
final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
-
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);
-
}
在6、7行上它拿到了所有的参数和参数注解,在第11行开始标注参数索引,在第18行上有个很关键的动作,如果当前参数的注解是Param,那么hasParamAnnotation标位true,并且把值放在name中。如果没有标注,那么它将会选择(29行)使用map的长度作为name,而map是会随参数的增加而增加长度的。接下来最后(35行)保存了key:索引和value:name值,很显然,如果我们有Param的注解,这个name自然而然保存的是属性名,如果没有,会有两种操作,一种是看看配置文件中setting属性的isUseActualParamName属性是否为true(26行),当然如果是true,效果和我们使用Param注解将会是一个效果,这个必须在JAVA8之后使用,如果这个属性也没填写true,那么就使用当前map的长度,这样{0=id,1=lastName}就诞生了,如果我们此时有第三个参数我们没有写注解同时也没配置isUseActualParamName属性,那么他的key=2,值也将会是2 value=2,
回到getNamedParams上,看第3行,如果参数为空,直接返回,第二个判断(第5行),如果参数只有一个,并且没有Param注解,我们将会拿到map(names)第一个参数,但此时我们map(names)其实只有一个参数,
接下来的else(第7行)就是我们多参传值,通过names,而这个names在构造器阶段就已经创建好了
-
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<String, Object> param = new ParamMap<Object>();
-
int i = 0;
-
for (Map.Entry<Integer, String> entry : names.entrySet()) {
-
param.put(entry.getValue(), args[entry.getKey()]);
-
// add generic param names (param1, param2, ...)
-
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
-
// ensure not to overwrite parameter named with @Param
-
if (!names.containsValue(genericParamName)) {
-
param.put(genericParamName, args[entry.getKey()]);
-
}
-
i++;
-
}
-
return param;
-
}
-
}
-
}
遍历names集合,将每一个值放入param这个map中(第11行),它将names中的values作为key,而将args,就是我们最外面那层getEmpByIdAndLastName方法传进来的参数作为value这样就变成我们最终的{id=args[0],lastName=args[1]},也就是{id=4,lastName="Hello"}
假设我们有第三个参数,传入参数为[4,"Hello","ABC"],没有标注解,也没有在全局配置文件的setting中声明isUseActualParamName属性为true,那么就会出现以下现象
names集合为{0=id,1=lastName,2=2}
在param中则会变成{id=args[0],lastName=args[1],2=args[2]}, 也就是{id=4,lastName="Hello",2="ABC"}最后有一步
GENERIC_NAME_PREFIX就是"param",它会将所有变量都存放在刚刚那个param中,采用param1、param2….作为key进行命名。
这里有一个我现在还不明白的地方,刚刚在代码里很明显names={0=id,1=lastName,2=2},param为{id=args[0],lastName=args[1],2=args[2]},我们应该在<select>标签中却要这样书写
<select id="getEmpByIdAndLastName" resultType="com.figsprite.bean.Employee">
select id,last_name lastName,gender,email from tb_employee where id = #{id} and last_name = #{arg1}
</select>
貌似涉及到selectOne函数的实现,我日后再细看这部分代码