mybatis 参数解析

写在前面

关于 mybatis参数,mybatis文档中介绍的比较少,是否方法的所有参数都需要加@Param注解,如果不加又该怎么获取?

这里我们不妨想一下,如果我们需要一个属性#{user.name},我们怎么去获取?


假装思考


其实是需要先从user的父级对象中获取user对象,再从user对象中获取name对象,那么有什么数据结构或者对象可以满足这种需求呢,其实就是Map和POJO能够满足这种需求。那其实mybatis要做的事情就是组装成POJO或者Map,其中map的key就是属性的名称。

给参数命名

  • 如果是多个参数,则得到一个Map<String, Object>(key=参数的名称,value=参数)
  • 如果是单个参数得到参数本身
  • 如果是集合参数,处理集合

初始化参数的名称

这发生在初始化ParamNameResolver,得到一个Map<Integer, String>(key=参数索引,value=参数名称),名称获取逻辑如下:

org.apache.ibatis.reflection.ParamNameResolver#ParamNameResolver

public ParamNameResolver(Configuration config, Method method) {
    final Class<?>[] paramTypes = method.getParameterTypes();
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<>();
    int paramCount = paramAnnotations.length;
    // get names from @Param annotations
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
        // 1.如果为特殊参数(RowBounds(分页对象) 和 ResultHandler(结果处理)),则不会记入mapper的实际参数
        if (isSpecialParameter(paramTypes[paramIndex])) {
            // skip special parameters
            continue;
        }
        String name = null;
        for (Annotation annotation : paramAnnotations[paramIndex]) {
            // 如果有注解Param,则名称为param属性值
            if (annotation instanceof Param) {
                hasParamAnnotation = true;
                name = ((Param) annotation).value();
                break;
            }
        }
        if (name == null) {
            // @Param was not specified.
            /* 如果配置了useActualParamName=true的话,则取实际参数的名称,而实际参数的名称取决于 jvm运行参数 -parameter,
            如果启动没设置该值,则参数名为arg+索引,(关于useActualParamName参数的解释:允许使用方法签名中的名称作为语句参数名称。
            */
            if (config.isUseActualParamName()) {
                name = getActualParamName(method, paramIndex);
            }
            if (name == null) {
                // use the parameter index as the name ("0", "1", ...)
                // gcode issue #71
                // 否则,该参数的名称为0,1....n
                name = String.valueOf(map.size());
            }
        }
        map.put(paramIndex, name);
    }
    names = Collections.unmodifiableSortedMap(map);
}

getNamedParams获取参数对象

上面说,names为一个Map<String, String> 的map,key=参数的索引,value=参数的名称,getNamedParams就是将参数与名称映射,得到一个map<String,Object>(key=参数名,value=参数)或者一个实际对象(当有效参数(不包括RowBounds和ResultHandler)只有一个时,并且没有使用Param注解)

org.apache.ibatis.reflection.ParamNameResolver#getNamedParams

public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    // 没有参数
    if (args == null || paramCount == 0) {
        return null;
    // 只有一个参数,且没有Param注解时,返回对象本身
    } else if (!hasParamAnnotation && paramCount == 1) {
        return args[names.firstKey()];
    } else {
        final Map<String, Object> param = new ParamMap<>();
        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;
    }
}

插曲
在最开始看的时候,有些疑惑ParamNameResolver 和 getNamedParams 为什么分开。这是因为ParamNameResolver是方法一对一的不会因为依次查询而更改,是可以跟着方法一起被缓存的,而不需要每次执行都初始化一个ParamNameResolver

集合和数组特殊处理

在执行查询之前,还调用了org.apache.ibatis.session.defaults.DefaultSqlSession#wrapCollection方法,来处理集合和数组对象(在上面方法(getNamedParams)中,如果只有一个参数,且没有注解,会返回该对象,如果正好是集合,数组,那么此时会是集合和数组),参数设置如下:

如果object instanceof Collection,name=collection,p
如果object instanceof List,name=list
如果object != null && object.getClass().isArray(),name=array

org.apache.ibatis.session.defaults.DefaultSqlSession#wrapCollection

private Object wrapCollection(final Object object) {
    if (object instanceof Collection) {
        StrictMap<Object> map = new StrictMap<>();
        map.put("collection", object);
        if (object instanceof List) {
            map.put("list", object);
        }
        return map;
    } else if (object != null && object.getClass().isArray()) {
        StrictMap<Object> map = new StrictMap<>();
        map.put("array", object);
        return map;
    }
    return object;
}

解析参数

设置参数是发生在该方法(org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters),并在这时候从参数中解析取值。
这里需要说下ParameterMapping,它是在有参数匹配时,就会生成这么一个对象,表示匹配了一个参数

获取属性值逻辑:

org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters

ParameterMapping parameterMapping = parameterMappings.get(i);
Object value;
String propertyName = parameterMapping.getProperty();
// 如果是hasAdditionalParameter,getAdditionalParameter,这个参数在`后面`会再提到
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
    value = boundSql.getAdditionalParameter(propertyName);
// 如果是参数是null,value=null;
} else if (parameterObject == null) {
    value = null;
/* 如果参数满足:org.apache.ibatis.type.TypeHandlerRegistry#hasTypeHandler(java.lang.Class<?>)
(可简单理解为org.apache.ibatis.type.TypeHandlerRegistry#TYPE_HANDLER_MAP中包含的该类的handler,TYPE_HANDLER_MAP在配置中和对象初始化时增加值)*/
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
    value = parameterObject;
} else {
    /* 从org.apache.ibatis.reflection.MetaObject#getValue中获取,内部从org.apache.ibatis.reflection.wrapper.ObjectWrapper#get获取,
    可以简单理解为:如果参数有“.”,则根据字类生成一个metaObject递归调用getValue,最终调用是:objectWrapper.get(); */
    MetaObject metaObject = configuration.newMetaObject(parameterObject);
    value = metaObject.getValue(propertyName);
}

ObjectWrapper
ObjectWrapper默认提供了3个实现,在MetaObject 构造器中初始化

// 如果对象就是wrapper,取wrapper,
if (object instanceof ObjectWrapper) {
    this.objectWrapper = (ObjectWrapper) object;
// 如果objectWrapperFactory有这个类的wrapper,则从factory中获取,用户自定义wrapper
} else if (objectWrapperFactory.hasWrapperFor(object)) {
    this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object);
// 如果对象是Map的实例
} else if (object instanceof Map) {
    this.objectWrapper = new MapWrapper(this, (Map) object);
// 如果对象是集合
} else if (object instanceof Collection) {
    this.objectWrapper = new CollectionWrapper(this, (Collection) object);
} else {
    // 剩下的实体对象处理
    this.objectWrapper = new BeanWrapper(this, object);
}
ObjectWrapper处理类型底层结构
MapWrapper对象是map的实例持有map,map
CollectionWrapper集合对象不过内部实现基本全是抛错,所以也不能直接引用属性#{list.size}
BeanWrapper数组对象,普通对象持有一个MetaClass,内部是实体的所有get,set方法等,数组类是没有属性的get,set方法的,所以也不能直接引用#{array.length}

疑问:

  1. 为什么CollectionWrapper 内部全是抛错,array也没有属性可以获取,那foreach是怎么工作的呢?后面

在sql解析时,org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql 会重新解析sql,解析成select id, name from users where id in ( #{__frch_id_0}, #{__frch_id_1}) 类似的sql,并将对应参数的键值对设置到org.apache.ibatis.scripting.xmltags.DynamicContext#bindings中,在做参数设置时,已经没有foreach标签,只有简单参数

结论:

  1. 参数名称不建议使用索引值(param+有效参数索引或者arg+索引或者0,1…n-1),如果参数顺序更改了,容易出错
  2. 当一个参数时,可以不用@Param注解,如果是基本类型,String,xml中的变量定义和mapper参数名称可以不一致,但是最好一致,如果是POJO,不加注解可以不通过POJO名称直接访问POJO中字段。
  3. 多个属性时,都用@Param注解,不然名称无法获取(目前我们并没有加-paramters 参数),只能通过 1 中方式获取
  4. 使用array和list时,注意不要访问size()或者length这样的属性/方法

参考

mybatis 3.x源码深度解析与最佳实践(倒是没参考,不过佩服这篇幅,以后应该可以借鉴)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值