Mybatis源码分析——参数处理

Mybatis源码分析——参数处理

前言

在查看mybatis源码的时候,只看了处理接口参数那一块,却没有查看后期处理完接口参数的过程(同时也是处理通过SqlSession调用命名空间,如下图所示),通过看了深入了解MyBatis参数这篇文章,我才知道自己的思维是多么局限,这篇文章作者也是《MyBatis从入门到精通》的作者,真正的一位大牛,我重新整理了一下我的这篇博文。代码来源:目前最新稳定版3.4.6
这里写图片描述

通过接口调用

1.构造方法

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<Integer, String> names;
  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);
  }
  \\...
  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<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;
    }
  }

返回的Object param可以有三种情况:

  1. null: 入参为null没有时,参数转换为
  2. this参数 :没有使用@Param注解并且只有一个参数时
  3. 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> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.<T>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 <E> List<E> 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<Object> map = new StrictMap<Object>();
      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<Object>();
      map.put("array", object);
      return map;
    }
    return object;
  }

总结

根据源码,我们可以知道以下参数处理结果:
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报错 Parameter ‘0’ not found. Available parameters are [arg1, arg0, param1, param2]

并不是说在某些MyBatis版本不能直接使用#{0}要使用 #{arg0},而是因为没有开启Java8编译开关,而3.4.2以及后续版本的mybatis首先默认使用注解值,然后isUseActualParamName默认为true,没有注解将使用参数名称,参数名称必须在编译时开启编译开关才能保留,否则将只保留arg0,arg1,arg2…argn,而后面的names.size,也就是0,1,2,3并没有添加

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值