Mybatis中@Param注解详细使用和原理分析

对于目前市场上火爆的持久层框架MyBatis相信大家在工作中肯定是用得很多,但是你对其mapper接口代理对象和其方法上的@Param注解又了解多少呢?

废话不多说,接来下就给大家来分析下

MapperRegistry

MapperRegistry是用于注册和缓存当前框架中所有的mapper接口

public class MapperRegistry {
  //框架的配置对象
  private final Configuration config; 
  //存放已经注册的mapper接口和其代码对象的工厂
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();;

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    //从map中取出已经注册的mapper接口代理对象的工厂
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    //创建并返回mapper接口的代码对象
    return mapperProxyFactory.newInstance(sqlSession);
  }
  ...其他方法省略...
}

上述代码是SqlSession中调用getMapper(mapper接口)方法的底层,也就是说我们调用getMapper方法其底层是调用了MapperRegistry对象的getMapper方法,那么我们继续往下研究那就要去看MapperProxyFactory中的newInstance方法啦

MapperProxyFactory

MapperProxyFactory是mapper的代理工厂专门用于创建mapper接口的代理对象

public class MapperProxyFactory<T> {
  //代理的接口
  private final Class<T> mapperInterface;
  //缓存代理的方法
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
  
  public T newInstance(SqlSession sqlSession) {
    //创建一个MapperProxy对象,其内部封装了方法拿到接口的代码对象
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy); //调用方法返回接口的代理对象
  }

  protected T newInstance(MapperProxy<T> mapperProxy) {
    //底层使用JDK动态创建接口的代理对象
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }
  ...其他方法省略...
}

通过上述代码我们最终看到了mapper接口的代理对象其底层使用的是JDK的动态代理技术创建并返回代理对象,最终接口中所有的方法都会由mapperProxy对象中的invoke方法来实现,当前类的字段MapperMethod对象至关总要,后期代理对象的invoke所执行的方法,最终是会调用到其对象的execute方法

MapperProxy

MapperProxy类实现了JDK动态代理的InvocationHandler接口,最后将由该对象的invoke方法来完成真正的方法执行

public class MapperProxy<T> implements InvocationHandler, Serializable {
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //判断是否是Object类中的方法,如equals/hashCode/toString等方法
    if (Object.class.equals(method.getDeclaringClass())) {
      try { //原封不动调用Object类中方法作为代理对象方法的默认实现
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    //接口中其他的方法则调用之前方法缓冲器的处理策略
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }
  ...其他方法省略...
}

MapperMethod

MapperMethod类是处理mapper接口中方法的真正处理器,该类内部的execute明确了代理的方法参数要怎么处理,查询得到的结果怎么封装然后返回

public class MapperMethod {
  //对执行的SQL标签的封装,包含SQL类型和任务的完整ID等信息
  private final SqlCommand command; 
  //代理方法的签名,其内部封装了一系列操作,如方法多参数时对@Param注解的处理
  private final MethodSignature method; 

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) { //针对DML/DQL操作执行
      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;
  }
}

MethodSignature类中的convertArgsToSqlCommandParam方法处理接口中的参数怎么转换成能用于执行SQL任务的参数,以下是底层执行的getNamedParams方法

public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
      //接口方法没有参数,返回null
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) { 
      //接口方法上只有1个参数则返回唯一的那个对象
      return args[names.firstKey()];
    } else {
      //接口方法上不止一个参数,就会把所有的参数封装到Map中然后返回
      final Map<String, Object> param = new ParamMap<Object>();
      int i = 0;
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        //把@Param注解中的value作为key,对应变量的值作为value
        param.put(entry.getValue(), args[entry.getKey()]);
        //自动生成key (param1, param2, ...)
        final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
        //为了不覆盖@Param注解设置的key
        if (!names.containsValue(genericParamName)) {
           param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
}

结合一个实际的例子来说明下:
mapper接口
假如调用方法时传入的实际参数是username=逍遥,password=123,那么在调用时我们可以发现接口中的方法超过1个的所以方法会执行最后的else代码
执行和数据分析

作者:梁开权,叩丁狼教育讲师。

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值