Mybatis如何通过接口实现sql执行原理解析

作者:myboy 时间:2022-11-30 11:31:26 

使用过 mybatis 框架的小伙伴们都知道,mybatis 是个半 orm 框架,通过写 mapper 接口就能自动实现数据库的增删改查,但是对其中的原理一知半解,接下来就让我们深入框架的底层一探究竟

1、环境搭建

首先引入 mybatis 的依赖,在 resources 目录下创建 mybatis 核心配置文件 mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
       PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
       "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

<!-- 环境、事务工厂、数据源 -->
   <environments default="dev">
       <environment id="dev">
           <transactionManager type="JDBC"/>
           <dataSource type="UNPOOLED">
               <property name="driver" value="org.apache.derby.jdbc.EmbeddedDriver"/>
               <property name="url" value="jdbc:derby:db-user;create=true"/>
           </dataSource>
       </environment>
   </environments>

<!-- 指定 mapper 接口-->
   <mappers>
       <mapper class="com.myboy.demo.mapper.user.UserMapper"/>
   </mappers>

</configuration>

在 com.myboy.demo.mapper.user 包下新建一个接口 UserMapper

public interface UserMapper {
   UserEntity getById(Long id);
   void insertOne(@Param("id") Long id, @Param("name") String name, @Param("json") List<String> json);
}

在 resources 的 com.myboy.demo.mapper.user 包下创建 UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
       PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
       "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.myboy.demo.mapper.user.UserMapper">

<select id="getById" resultType="com.myboy.demo.db.entity.UserEntity">
       select * from demo_user where id = #{id}
   </select>

<insert id="insertOne">
       insert into demo_user (id, name, json) values (#{id}, #{name}, #{json})
   </insert>
</mapper>

创建 main 方法测试

try(InputStream in = Resources.getResourceAsStream("com/myboy/demo/sqlsession/mybatis-config.xml")){
   SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
   sqlSession = sqlSessionFactory.openSession();
   # 拿到代理类对象
   UserMapper mapper = sqlSession.getMapper(UserMapper.class);
   # 执行方法
   UserEntity userEntity = mapper.getById(2L);
   System.out.println(userEntity);
   sqlSession.close();
}catch (Exception e){
   e.printStackTrace();
}

2、 * 类的生成

🤔 通过上面的示例,我们需要思考两个问题:

  • mybatis 如何生成 mapper 的 * 类?

  • 通过 sqlSession.getMapper 获取到的 * 类是什么内容?

通过查看源码,sqlSession.getMapper() 底层调用的是 mapperRegistry 的 getMapper 方法

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
   // sqlSessionFactory build 的时候,就已经扫描了所有的 mapper 接口,并生成了一个 MapperProxyFactory 对象
   // 这里根据 mapper 接口类获取 MapperProxyFactory 对象,这个对象可以用于生成 mapper 的代理对象
   final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
   if (mapperProxyFactory == null) {
     throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
   }
   try {
     // 创建代理对象
     return mapperProxyFactory.newInstance(sqlSession);
   } catch (Exception e) {
     throw new BindingException("Error getting mapper instance. Cause: " + e, e);
   }
 }

代码注释已经写的很清楚,每个 mapper 接口在解析时会对应生成一个 MapperProxyFactory,保存到 knownMappers 中,mapper 接口的实现类(也就是 * 类)通过这个 MapperProxyFactory 生成,mapperProxyFactory.newInstance(sqlSession) 代码如下:

/**
* 根据 sqlSession 创建 mapper 的 * 对象
* @param sqlSession sqlSession
* @return 代理类
*/
public T newInstance(SqlSession sqlSession) {
   // 创建 MapperProxy 对象,这个对象实现 InvocationHandler 接口,里面封装类 mapper * 方法的执行的核心逻辑
   final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
   return newInstance(mapperProxy);
}

protected T newInstance(MapperProxy<T> mapperProxy) {
   return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

代码一目了然,通过 jdk * 技术创建了 mapper 接口的代理对象,其 InvocationHandler 的实现是 MapperProxy,那么 mapper 接口中方法的执行,最终都会被 MapperProxy 增强

3、MapperProxy 增强 mapper 接口

MapperProxy 类实现了 InvocationHandler 接口,那么其核心方法必然是在其 invoke 方法内部

/**
* 所有 mapper 代理对象的方法的核心逻辑
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   try {
     // 如果执行的方法是 Object 类的方法,则直接反射执行
     if (Object.class.equals(method.getDeclaringClass())) {
       return method.invoke(this, args);
     } else {
       // 1、根据method创建方法执行器对象 MapperMethodInvoker,用于适配不同的方法执行过程
       // 2、执行方法逻辑
       return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
     }
   } catch (Throwable t) {
     throw ExceptionUtil.unwrapThrowable(t);
   }
}

3.1、cachedInvoker(method)

由于 jdk8 对接口增加了 default 关键字,使接口中的方法也可以有方法体,但是默认方法和普通方法的反射执行方式不同,需要用适配器适配一下才能统一执行,具体代码如下

/**
* 适配器模式,由于默认方法和普通方法反射执行的方式不同,所以用 MapperMethodInvoker 接口适配下
* DefaultMethodInvoker 用于执行默认方法
* PlainMethodInvoker 用于执行普通方法
*/
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
   try {
     return MapUtil.computeIfAbsent(methodCache, method, m -> {
       // 返回默认方法执行器 DefaultMethodInvoker
       if (m.isDefault()) {
         try {
           if (privateLookupInMethod == null) {
             return new DefaultMethodInvoker(getMethodHandleJava8(method));
           } else {
             return new DefaultMethodInvoker(getMethodHandleJava9(method));
           }
         } catch (IllegalAccessException | InstantiationException | InvocationTargetException
             | NoSuchMethodException e) {
           throw new RuntimeException(e);
         }
       }
       // 返回普通方法执行器,只有一个 invoke 执行方法,实际上就是调用 MapperMethod 的执行方法
       else {
         return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
       }
     });
   } catch (RuntimeException re) {
     Throwable cause = re.getCause();
     throw cause == null ? re : cause;
   }
}

如果判定执行的是接口的默认方法,则原始方法封装成 DefaultMethodInvoker,这个类的 invoke 方法就是利用反射调用原始方法,没什么好说的

如果是普通的接口方法,则将方法封装成封装成 MapperMethod,然后再将 MapperMethod 封装到 PlainMethodInvoker 中,PlainMethodInvoker 没什么好看的,底层的执行方法还是调用 MapperMethod 的执行方法,至于 MapperMethod,咱们放到下一章来看

3.2、MapperMethod

首先看下构造方法

public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
   // 通过这个 SqlCommand 可以拿到 sql 类型和sql 对应的 MappedStatement
   this.command = new SqlCommand(config, mapperInterface, method);
   // 包装了 mapper 接口的一个方法,可以拿到方法的信息,比如方法返回值类型、返回是否集合、返回是否为空
   this.method = new MethodSignature(config, mapperInterface, method);
}

代码里的注释写的很清楚了,MapperMethod 构造方法创建了两个对象 SqlCommand 和 MethodSignature

mapper 接口的执行核心逻辑在其 execute() 方法中:

/**
  * 执行 mapper 方法的核心逻辑
  * @param sqlSession sqlSession
  * @param args 方法入参数组
  * @return 接口方法返回值
  */
 public Object execute(SqlSession sqlSession, Object[] args) {
   Object result;
   switch (command.getType()) {
     case INSERT: {
       // 参数处理,单个参数直接返回,多个参数封装成 map
       Object param = method.convertArgsToSqlCommandParam(args);
       // 调用 sqlSession 的插入方法
       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()) {
         // 方法返回值为 void,但是参数里有 ResultHandler
         executeWithResultHandler(sqlSession, args);
         result = null;
       } else if (method.returnsMany()) {
         // 方法返回集合
         result = executeForMany(sqlSession, args);
       } else if (method.returnsMap()) {
         // 方法返回 map
         result = executeForMap(sqlSession, args);
       } else if (method.returnsCursor()) {
         // 方法返回指针
         result = executeForCursor(sqlSession, args);
       } else {
         // 方法返回单个对象
         // 将参数进行转换,如果是一个参数,则原样返回,如果多个参数,则返回map,key是参数name(@Param注解指定 或 arg0、arg1 或 param1、param2 ),value 是参数值
         Object param = method.convertArgsToSqlCommandParam(args);
         // selectOne 从数据库获取数据,封装成返回值类型,取出第一个
         result = sqlSession.selectOne(command.getName(), param);

// 如果返回值为空,并且返回值类型是 Optional,则将返回值用 Optional.ofNullable 包装
         if (method.returnsOptional()
             && (result == null || !method.getReturnType().equals(result.getClass()))) {
           result = Optional.ofNullable(result);
         }
       }
       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;
 }

代码逻辑很清晰,拿 Insert 方法来看,他只做了两件事

  • 参数转换

  • 调用 sqlSession 对应的 insert 方法

3.2.1、参数转换 method.convertArgsToSqlCommandParam(args)

在 mapper 接口中,假设我们定义了一个 user 的查询方法

List<User> find(@Param("name")String name, @Param("age")Integer age)

在我们的 mapper.xml 中,写出来的 sql 可以是这样的:

select * from user where name = #{name} and age > #{age}

当然不使用 @Param 注解也可以的,按参数顺序来

select * from user where name = #{arg0} and age > #{arg1}

select * from user where name = #{param1} and age > #{param2}

因此如果要通过占位符匹配到具体参数,就要将接口参数封装成 map 了,如下所示

{arg1=12, arg0="abc", param1="abc", param2=12}

{name="abc", age=12, param1="abc", param2=12}
复制代码

这里的这个 method.convertArgsToSqlCommandParam(args) 就是这个作用,当然只有一个参数的话就不用转成 map 了, 直接就能匹配

3.2.2、调用 sqlSession 的方法获取结果

真正要操作数据库还是要借助 sqlSession,因此很快就看到了 sqlSession.insert(command.getName(), param) 方法的执行,其第一个参数是 statement 的 id,就是 mpper.xml 中 namespace 和 insert 标签的 id的组合,如 com.myboy.demo.mapper.MoonAppMapper.getAppById,第二个参数就是上面转换过的参数,至于 sqlSession 内部处理逻辑,不在本章叙述范畴

sqlSession 方法执行完后的执行结果交给 rowCountResult 方法处理,这个方法很简单,就是将数据库返回的数据处理成接口返回类型,代码很简单,如下

private Object rowCountResult(int rowCount) {
   final Object result;
   if (method.returnsVoid()) {
     result = null;
   } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
     result = rowCount;
   } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
     result = (long) rowCount;
   } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
     result = rowCount > 0;
   } else {
     throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
   }
   return result;
 }

4、小结

到目前为止,我们已经搞清楚了通过 mapper 接口生成 * 对象,以及代理对象调用 sqlSession 操作数据库的逻辑,我总结出执行逻辑图如下:

Mybatis如何通过接口实现sql执行原理解析

来源:https://juejin.cn/post/7191854734406811685

标签:mybatis,sql执行,接口
0
投稿

猜你喜欢

  • SpringBoot异步任务使用方法详解

    2021-08-07 07:57:02
  • 浅谈C#多线程简单例子讲解

    2023-06-24 03:03:50
  • Java实现的读取资源文件工具类ResourcesUtil实例【可动态更改值的内容】

    2022-01-15 17:27:12
  • AndroidStudio报错Emulator:PANIC:Cannot find AVD system path. Please define ANDROID_SDK_ROOT(解决方案)

    2023-11-27 01:33:28
  • SpringBoot Entity中枚举类型详细使用介绍

    2023-11-11 00:30:52
  • java数据结构实现机器人行走

    2023-12-04 00:44:24
  • 双重检查锁定模式Java中的陷阱案例

    2023-11-13 22:11:02
  • Kotlin标准函数与静态方法应用详解

    2022-01-21 10:45:33
  • Android实现Camera2预览和拍照效果

    2021-09-30 20:03:42
  • 有关tomcat内存溢出的完美解决方法

    2023-09-18 09:02:25
  • 对spring task和线程池的深入研究

    2022-08-08 10:32:38
  • C#实现回文检测的方法

    2022-12-26 22:31:23
  • Java使用数组实现ArrayList的动态扩容的方法

    2023-03-23 11:24:39
  • android自动生成dimens适配文件的图文教程详解(无需Java工具类)

    2023-07-17 12:12:30
  • C#实现字符串倒序的写法

    2023-02-24 04:07:23
  • Android使用android-wheel实现省市县三级联动

    2022-10-07 21:01:00
  • Android实现摇一摇功能

    2023-07-23 20:21:11
  • C#实现数据包加密与解密实例详解

    2022-05-14 18:44:50
  • C#实现加密与解密详解

    2023-08-11 16:54:42
  • java实现文件的断点续传

    2023-11-23 09:11:00
  • asp之家 软件编程 m.aspxhome.com