写点什么

MyBatis3 源码解析 (4) 参数解析

作者:
  • 2022 年 2 月 16 日
  • 本文字数:4974 字

    阅读完需:约 16 分钟

简介

上篇文章中探索了查询语句的执行过程,下面我们接着来看看其中的查询参数的解析细节,是如何工作的

参数的解析

在日常的开发中,常见的参数有如下几种:


  • 1.直接传入: func(Object param1, Object param2, ...)

  • 2.放入 Map 中进行传入:func(Map<String, Object> param)

  • 3.类传入: func(Object param)


上面的请求是如何对应的呢,下面让我们带着疑问跟着源码走一走

参数解析前的相关准备工作

在上篇中,在 ParamNameResolver 有获取参数列表的代码,大体上从 names 中遍历获取的,这里就涉及到 names 的初始化的相关代码,如下:


public class ParamNameResolver {    public static final String GENERIC_NAME_PREFIX = "param";    private final boolean useActualParamName;    private final SortedMap<Integer, String> names;    private boolean hasParamAnnotation;
// 初始化话的时候,将相关的name已初始化好 public ParamNameResolver(Configuration config, Method method) { this.useActualParamName = config.isUseActualParamName(); Class<?>[] paramTypes = method.getParameterTypes(); Annotation[][] paramAnnotations = method.getParameterAnnotations(); SortedMap<Integer, String> map = new TreeMap(); int paramCount = paramAnnotations.length;
for(int paramIndex = 0; paramIndex < paramCount; ++paramIndex) { if (!isSpecialParameter(paramTypes[paramIndex])) { String name = null; Annotation[] var9 = paramAnnotations[paramIndex]; int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) { Annotation annotation = var9[var11]; if (annotation instanceof Param) { this.hasParamAnnotation = true; name = ((Param)annotation).value(); break; } }
if (name == null) { if (this.useActualParamName) { name = this.getActualParamName(method, paramIndex); }
if (name == null) { name = String.valueOf(map.size()); } }
map.put(paramIndex, name); } }
this.names = Collections.unmodifiableSortedMap(map); }}
复制代码


在上面的代码中,names 在类构造函数中已经生成好了,后面获取值的时候直接用即可


而在 ParamNameResolver 的构造函数中,通过初步跟踪代码,是直接读取的接口函数参数获取得到的参数,也就是在情况 3 中传入类,是当做一个参数,后面这个类会一直传递下去


ParamNameResolver 在 MapperMethod 中就已经初始化好了


public class MapperProxy<T> implements InvocationHandler, Serializable {    private MapperProxy.MapperMethodInvoker cachedInvoker(Method method) throws Throwable {        try {            return (MapperProxy.MapperMethodInvoker)MapUtil.computeIfAbsent(this.methodCache, method, (m) -> {                if (m.isDefault()) {         ......                } else {                    return new MapperProxy.PlainMethodInvoker(new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration()));                }            });        } catch (RuntimeException var4) {            Throwable cause = var4.getCause();            throw (Throwable)(cause == null ? var4 : cause);        }    }}
public class MapperMethod { public static class MethodSignature { ...... public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) { ...... this.paramNameResolver = new ParamNameResolver(configuration, method); } }}
复制代码


跟踪代码下来,首先进行相关的初始化工作,而后在进行参数的解析获取

参数值初步获取

初始化完成后,在代码语句执行前,会获取参数值列表,下面是具体的处理逻辑:


public class ParamNameResolver {    public Object getNamedParams(Object[] args) {  // 获取参数的数量        int paramCount = this.names.size();        if (args != null && paramCount != 0) {      // 没有参数声明并且参数数量为1            if (!this.hasParamAnnotation && paramCount == 1) {                Object value = args[(Integer)this.names.firstKey()];                return wrapToMapIfCollection(value, this.useActualParamName ? (String)this.names.get(0) : null);            } else {                Map<String, Object> param = new ParamMap();                int i = 0;
// 遍历name获得参数列表 for(Iterator var5 = this.names.entrySet().iterator(); var5.hasNext(); ++i) { Entry<Integer, String> entry = (Entry)var5.next(); param.put(entry.getValue(), args[(Integer)entry.getKey()]); String genericParamName = "param" + (i + 1); if (!this.names.containsValue(genericParamName)) { param.put(genericParamName, args[(Integer)entry.getKey()]); } }
return param; } } else { return null; } }}
复制代码


在上面处理中,如果参数列表中唯一只有一个类参数,那这个参数也算是一个参数,会直接返回类,比如在示例中会直接返回:Person(id=1,name=1),后面获取参数值填充时,会使用类 get 方法获取值,这个在下面会接着分析


而且注意在 param 中,会存入两个东西,一个 argx,一个是 paramx,感觉是和 ${}和 #{}有关,这个后面再分析,param 在示例中会如下:


#如果不加@Param注解param:- arg0: 1- arg1: 1- param0: 1- param1: 1
#如果加@Param注解param:- id: 1- name: 1- param0: 1- param1: 1
复制代码


在这里就得到后面要用的参数了,这里需要注意了,如果是单个参数,那就是直接返回对应的值;如果是多个参数,那就会放到一个 map 中,这个 map 中的 key 是非常关键的,因为构造 preStatement 是根据名称从里面取值的,后面会有相关代码

PreStatement 的生成

咋上面得到参数后,并不是直接使用,而在在 PreStatement 生成的时候用于传入的,关键的代码如下:


public class SimpleExecutor extends BaseExecutor {    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {        Connection connection = this.getConnection(statementLog);        Statement stmt = handler.prepare(connection, this.transaction.getTimeout());  // 这里会进行参数的传入        handler.parameterize(stmt);        return stmt;    }}
public class DefaultParameterHandler implements ParameterHandler { public void setParameters(PreparedStatement ps) { ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId()); List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings(); if (parameterMappings != null) { for(int i = 0; i < parameterMappings.size(); ++i) { ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { String propertyName = parameterMapping.getProperty(); Object value; if (this.boundSql.hasAdditionalParameter(propertyName)) { value = this.boundSql.getAdditionalParameter(propertyName); } else if (this.parameterObject == null) { value = null; } else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) { value = this.parameterObject; } else { // 上面的三个先不管,下面就是获取参数的具体的逻辑 // 如果是类,会通过一些处理调用对应的get方法 // 如果多个之间传递的参数,在上面会放入一个map,之间从map中获取即可 MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject); value = metaObject.getValue(propertyName); }
// 和参数拦截器相关的,后面再解析,先放过 TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { jdbcType = this.configuration.getJdbcTypeForNull(); }
try { // 设置相关的值 typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (SQLException | TypeException var10) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var10, var10); } } } }
}}
复制代码


从上面大致代码可以看到,在正常情况下,参数的设置都是通过名称取获取的,之间传入或者单个传入的情况比较简单


  • 多个参数的情况下,会将所有的参数放入 map 中,后面根据名称去获取

  • 单个参数类的情况下,会调用类的 get 方法对应获取


那如果混合类型,比如下面的情况:


public interface PersonMapper {    @Select("Select id, name from Person where id=#{id} and name=#{person.name}")    @Results(value = {            @Result(property = "id", column = "id"),            @Result(property="name", column = "name"),    })    Person getPersonByMul(@Param("person") Person person, @Param("id") Integer id);}
复制代码


我们一直根据下,从下面的代码中得到,如果是类,会递归去获取


public class MetaObject {    public Object getValue(String name) {        PropertyTokenizer prop = new PropertyTokenizer(name);        if (prop.hasNext()) {            MetaObject metaValue = this.metaObjectForProperty(prop.getIndexedName());      // 如果是类,后面会再次调用getVale获取            return metaValue == SystemMetaObject.NULL_META_OBJECT ? null : metaValue.getValue(prop.getChildren());        } else {            return this.objectWrapper.get(prop);        }    }}
复制代码

总结

通过本篇的探索,我们大致了解了 MyBatis3 的参数获取解析原理


  • 1.在初始化阶段,获取接口函数的参数列表,初始化 names,用于后面获取参数值

  • 2.获取参数值,分单个或者多个参数的情况,而且根据参数的类型,有不同的处理方法

  • 单个参数:直接返回

  • 多个参数:放入 Map 中返回,后面根据 key 进行获取

  • 参数类型是类:后面调用类的 get 防护获取对应的值

  • 参数类型是 Map:后面直接调用 get 方法

  • 3.PreStatement 生成,生成的时候对应的值从上面得到的参数值获取

发布于: 刚刚阅读数: 2
用户头像

关注

还未添加个人签名 2018.09.09 加入

代码是门手艺活,也是门艺术活

评论

发布
暂无评论
MyBatis3源码解析(4)参数解析