MyBatis SqlCommand+MethodSignature源码探究

系列文章目录

文章目录


一、从getMapper说起

UserMapper mapper = sqlSession.getMapper(UserMapper.class);当调用 Mapper 接口的方法时,动态代理会拦截这个调用,并根据方法签名和配置,将请求交给 MyBatis 的 SQL 执行器(SqlSession)来完成数据库操作,

复制代码
Invoker 是 MyBatis 内部用来封装方法调用逻辑的类,它负责:

解析方法参数
根据方法名找到对应的 SQL
执行 SQL 并处理结果
返回最终结果(如 List、Map、实体类等)

1.检查这个 method 是不是定义在 Object 类中的方法(比如 toString(), equals() 等)。
如果是,就直接调用该方法(因为这些方法不需要 MyBatis 处理)。
this 表示当前代理对象本身(虽然一般不会调用 Object 的方法,但这是为了防止错误处理)。
2.cachedInvoker(method):从缓存中获取一个 Invoker 对象,用于执行该方法。
c 复制代码
@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      }
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }


继续追踪这行源码:cachedInvoker(method).invoke(proxy, method, args, sqlSession);先看cachedInvoker这个代码里面做了什么。MapperMethodInvoker 有2种, 一种是PlainMethodInvoker, 一种DefaultMethodInvoker,有什么区别呢?

DefaultMethodInvoker是指当前代理对象(接口,比如UserMapper),执行的是不是里面的默认方法,是默认方法,返回DefaultMethodInvoker, 不是默认方法,返回PlainMethodInvoker

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      return MapUtil.computeIfAbsent(methodCache, method, m -> {
        // 方法不是接口中的默认方法,PlainMethodInvoker的底层会调用MapperMethod的execute方法,从而执行方法对应的SQL
        if (!m.isDefault()) {
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }

        // 方法如果是接口中的默认方法,那么方法已经有逻辑了,DefaultMethodInvoker是专门针对默认方法的MapperMethodInvoker,底层会利用反射执行默认方法的逻辑,不会执行SQL
        try {
          if (privateLookupInMethod == null) {
            return new DefaultMethodInvoker(getMethodHandleJava8(method));
          }
          return new DefaultMethodInvoker(getMethodHandleJava9(method));


接下来看一下PlainMethodInvoker里面的MapperMethod,代码如下:
  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }

二、SqlCommand:SQL解析+父子接口

bash 复制代码
SqlCommand:
解析该方法对应的 SQL 是 SELECT、INSERT、UPDATE 还是 DELETE。
获取该方法对应的 SQL 语句(从 XML 或注解中读取)。
确定 SQL 的 ID(即 namespace.methodName 的形式)。
例如:如果 UserMapper.selectUserById 方法对应 XML 中的 <select id="selectUserById"></select id="selectUserById">``,那么 SqlCommand` 就会记录这个信息。
c 复制代码
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      final String methodName = method.getName();
      final Class<?> declaringClass = method.getDeclaringClass();

      // 找到方法对应的MappedStatement,根据接口名,方法名,找到MappedStatement对象
      // 1、接口可以继承,子接口的代理对象可以调用父接口的方法
      // 2、一个接口可以继承多个接口,遍历每个父接口找到当前调用方法对应的MappedStatement对象
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);
     省略.....主要看如何得到ms,请看2.1

返回ms以后,就可以得到sql的很多信息,比如当前执行的命令是什么,select/update/delete/xx, 不管是解析xml标签出来的,还是解析方法上面注解出来的

2.1、父子接口MappedStatement源码解析

复制代码
public interface UserMapper{
  public UserInfo getUserInfo(int userId);
}

public interface AdminMapper extends UserMapper{}

Main:
 AdminMapper mapper = sqlSession.getMapper(AdminMapper.class);

这种使用就会涉及到父子接口
c 复制代码
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, Class<?> declaringClass,
        Configuration configuration) {
      String statementId = mapperInterface.getName() + "." + methodName;
//这个if语句比较容易理解,直接从configuration里面拿
      if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
      }
      // 什么时候不相等呢?接口可以继承,子接口的代理对象可以调用父接口的方法
      //请看2.1 开始部分代码示例
      if (mapperInterface.equals(declaringClass)) {
        return null;
      }
      // 一个接口可以继承多个接口,遍历每个父接口找到当前调用方法对应的MappedStatement对象
      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
          MappedStatement ms = resolveMappedStatement(superInterface, methodName, declaringClass, configuration);
          if (ms != null) {
            return ms;
        ......

三、MethodSignature方法源码

用于解析接口方法的签名,主要作用是:解析并保存当前方法的返回类型,参数信息,是否为集合,是否使用@MapKey等元数据信息,以便后续在sql映射中使用这些信息进行参数绑定和结果映射

c 复制代码
    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.returnsVoid = void.class.equals(this.returnType);
      this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
      this.returnsCursor = Cursor.class.equals(this.returnType);
      this.returnsOptional = Optional.class.equals(this.returnType);
      this.mapKey = getMapKey(method); // 获取方法上@MapKey指定的值
      this.returnsMap = this.mapKey != null; // 使用了@MapKey
      this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);  // // 获取方法中RowBounds类型参数的参数Index
      this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);  // 获取方法中ResultHandler类型参数的参数Index
      this.paramNameResolver = new ParamNameResolver(configuration, method);
    }

3.1. MapKey注解

平时比较少用

c 复制代码
  @Select("select * from user_info;")
  @MapKey("name")
  //MapKey注解里面name的值是UserInfo类中的某个属性
  //表示将查询结果封装到Map中,Map的key是UserInfo类中的name属性,value是UserInfo对象
  public Map<String, UserInfo> selectAllUser();

四、execute()

前面说了sqlcommand,MethodSignature等等,有了这两个,最终会执行下面这个方法PlainMethodInvoker.execute()

c 复制代码
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 SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          // 方法中有ResultHandler类型的参数,那就用该参数对应的ResultHandler对象来处理SQL的返回结果
          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 {
          // param要么是一个对象,要么是ParamMap, key就是参数的名字,value就是参数值
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
相关推荐
HeyZoeHey2 天前
Mybatis执行sql流程(一)
java·sql·mybatis
青川入梦2 天前
MyBatis极速通关上篇:Spring Boot环境搭建+用户管理实战
java·开发语言·mybatis
33255_40857_280592 天前
掌握分页艺术:MyBatis与MyBatis-Plus实战指南(10年Java亲授)
java·mybatis
勿在浮沙筑高台2 天前
无法获取实体类com.example.springdemo2.entity.po.UserPO对应的表名!
java·spring boot·mybatis
柯南二号3 天前
【Java后端】MyBatis-Plus 原理解析
java·开发语言·mybatis
Easocen3 天前
Mybatis学习笔记(五)
笔记·学习·mybatis
qq_三哥啊3 天前
【IDEA】设置Debug调试时调试器不进入特定类(Spring框架、Mybatis框架)
spring·intellij-idea·mybatis
柯南二号4 天前
【Java后端】Spring Boot 集成 MyBatis-Plus 全攻略
java·spring boot·mybatis
记忆不曾留4 天前
Mybatis 源码解读-SqlSession 会话源码和Executor SQL操作执行器源码
mybatis·二级缓存·sqlsession会话·executor执行器·一级缓存localcache