MyBatis 中 Mapper 接口的使用原理

MyBatis 3 推荐使用 Mapper 接口的方式来执行 xml 配置中的 SQL,用起来很方便,也很灵活。在方便之余,想了解一下这是如何实现的,之前也大致知道是通过 JDK 的动态代理做到的,但这次想知道细节。

东西越多就越复杂,所以就以一个简单的仅依赖 MyBatis 3.4.0 的 CRUD 来逐步了解 Mapper 接口的调用。

通常是通过 xml 配置文件来创建SqlSessionFactory对象,然后再获取SqlSession对象,接着获取自定义的 Mapper 接口的代理对象,最后调用接口方法,示例如下:

/**
*
* @author xi
* @date 2018/10/01 14:12
*/
public class Demo {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream is = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
Role role = roleMapper.getRole(1L);
System.out.println(role);
}
}

如何解析配置文件,创建工厂,获取会话皆不是本次关注的重点,直接看下面这行即可:

RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);

获取自定义 Mapper 代理对象的方法位于:org.apache.ibatis.session.SqlSession#getMapper,还是个范型方法

  /**
* Retrieves a mapper.
* @param <T> the mapper type
* @param type Mapper interface class
* @return a mapper bound to this SqlSession
*/
<T> T getMapper(Class<T> type);

实现该方法的子类有:DefaultSqlSessionSqlSessionManager,这里关注默认实现即可:org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper

  @Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}

这里面出现了 Configuration 对象,简单来说就是包含了 xml 配置解析内容的对象,同样它也不是现在关注的重点,继续往下跟进:org.apache.ibatis.session.Configuration#getMapper

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}

这里出现了MapperRegistry对象,它是解析 Mapper.xml 中的内容(mapper标签中的namespace就包含了 Mapper 接口的全限定名称)得来的,含有一个 HashMap 类型的成员变量org.apache.ibatis.binding.MapperRegistry#knownMappers,key 是 Mapper 接口的Class对象,value 是org.apache.ibatis.binding.MapperProxyFactory,从名称就可以看出是用来创建 Mapper 接口的代理对象的工厂,后面会用到。

具体这个knownMappers是怎么填充的,详见org.apache.ibatis.binding.MapperRegistry#addMapper方法,暂时不管,先往下走:org.apache.ibatis.binding.MapperRegistry#getMapper

  @SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
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 接口的类型,从knownMappers中拿到对应的工厂,然后创建代理对象,继续跟进:org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)

  public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}

这里又出现了一个MapperProxy对象,理解起来是一个代理对象,打开一看它实现了java.lang.reflect.InvocationHandler接口,这是挂羊头卖狗肉哇。

先不看狗肉了,继续跟进:org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.binding.MapperProxy<T>)

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

在这,看到了熟悉的java.lang.reflect.Proxy,这里的mapperInterface是创建工厂时传入的 Mapper 接口。真正的 Mapper 接口的代理对象此时才产生,是真羊头。

这不是既熟悉又陌生的 JDK 动态代理么,说它熟悉,因为前面的狗肉mapperProxy是一个InvocationHandler对象,它拦截了所有对代理对象接口方法的调用。说它陌生是因为之前使用 JDK 动态代理时会有接口的具体实现子类,这里没看到。因为 MyBatis 只需要拦截接口方法,找到方法对应的 SQL 去执行就可以了,没必要多此一举加上实现子类,这也正是 MyBatis 方便的地方。接下来看看org.apache.ibatis.binding.MapperProxy

/**
* @author Clinton Begin
* @author Eduardo Macarron
*/
public class MapperProxy<T> implements InvocationHandler, Serializable { private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache; public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 判断 method 是不是 Object 类的方法,如 hashCode()、toString()
if (Object.class.equals(method.getDeclaringClass())) {
try {// 如果是,则调用当前 MapperProxy 对象的这些方法
// 跟 Mapper 接口的代理对象没关系
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
// 到这了,说明调用的是接口中的方法,具体的执行就不是本次关注的重点了
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
} // 对 MapperMethod 做了缓存,这个 methodCache 是个 ConcurrentHashMap,在 MapperProxyFactory 中创建的
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
} }

具体说明都在代码注释里面,没啥好说的了。

总结

  1. 通过 JDK 动态代理模式,创建 Mapper 接口的代理对象,拦截对接口方法的调用;
  2. Mapper 接口中不能使用重载,具体原因参见org.apache.ibatis.binding.MapperMethod.SqlCommand#SqlCommand,MyBatis 是通过mapperInterface.getName() + "." + method.getName()去获取 xml 中解析出来的 SQL 的,具体可能还要看一下org.apache.ibatis.session.Configuration#mappedStatements

最后的最后,古话说的好:遇事不决,先开大龙(看源码)(逃

MyBatis 中 Mapper 接口的使用原理的更多相关文章

  1. mybatis中mapper接口的参数设置几种方法

    方法一:忽略parameterType,加@param("xxx")注解 在mapper接口中加上@param("xxx")注解,则在配置文件中直接用即可 Li ...

  2. 逆向工程生成的mybatis中mapper文件。mapper接口,实例化成对象

    逆向工程生成的mybatis中mapper文件中,*mapper文件只是接口,而不是类文件.但是却可以通过spring的容器获得实例. 例如: //1.获得mapper代理对象,从spring容器获得 ...

  3. mybatis从mapper接口跳转到相应的xml文件的eclipse插件

    mybatis从mapper接口跳转到相应的xml文件的eclipse插件 前提条件 开发软件 eclipse 使用框架 mybatis 为了方便阅读源码,项目使用mybatis的时候,方便从mapp ...

  4. Mybatis的Mapper接口方法不能重载

    今天给项目的数据字典查询添加通用方法,发现里边已经有了一个查询所有数据字典的方法 List<Dict> selectDictList(); 但我想设置的方法是根据数据字典的code查询出所 ...

  5. 5.7 Liquibase:与具体数据库独立的追踪、管理和应用数据库Scheme变化的工具。-mybatis-generator将数据库表反向生成对应的实体类及基于mybatis的mapper接口和xml映射文件(类似代码生成器)

    一. liquibase 使用说明 功能概述:通过xml文件规范化维护数据库表结构及初始化数据. 1.配置不同环境下的数据库信息 (1)创建不同环境的数据库. (2)在resource/liquiba ...

  6. IntelliJ IDEA中Mapper接口通过@Autowired注入报错的正确解决方式

    转载请注明来源:四个空格 » IntelliJ IDEA中Mapper接口通过@Autowired注入报错的正确解决方式: 环境 ideaIU-2018.3.4.win: 错误提示: Could no ...

  7. Mybatis中dao接口和mapper 的加载过程

    这里考虑的是mybatis和spring整合的场景 1.在系统启动的时候,会去执行配置文件中有关扫描mybatis接口的配置:通过MapperScannerConfigurer扫描接口生成spring ...

  8. Mybatis的mapper接口在Spring中实例化过程

    在spring中使用mybatis时一般有下面的配置 <bean id="mapperScannerConfigurer" class="org.mybatis.s ...

  9. Mybatis的mapper接口接受的参数类型

    最近项目用到了Mybatis,学一下记下来. Mybatis的Mapper文件中的select.insert.update.delete元素中有一个parameterType属性,用于对应的mappe ...

随机推荐

  1. 花 1 小时,开源设计 LoRa 继电器开关

    提示1:锐米所有 LoRa 产品严格遵循国标标准的 LoRaWAN 协议. 提示2:您可以免费复制,修改和商用本项目,请注明锐米原创. 提示3:如果您有其他 LoRa 需求或建议,欢迎联系锐米 sup ...

  2. Windwos日志分析

    Windows日志分析工具 查看系统日志方法: 在“开始”菜单上,依次指向“所有程序”.“管理工具”,然后单击“事件查看器” 按 "Window+R",输入 ”eventvwr.m ...

  3. css实现文字过长显示省略号的方法

    <div class="title">当对象内文本溢出时显示省略标记</div> 这是一个例子,其实我们只需要显示如下长度: css实现网页中文字过长截取. ...

  4. GNU make doc - 函数总结

    $(value variable) 使用variable未展开状态的值 FOO = $(PATH) all: $(warning $(FOO)) $(warning $(value FOO)) #ou ...

  5. 基于Struts2开发校园二手购物商城源码

    开发环境: Windows操作系统开发工具: MyEclipse+Jdk+Tomcat+MySQL数据库 次项目分为管理员和普通用户两种角色 运行效果图 源码及原文链接:https://javadao ...

  6. Android Intent用法总结

    Android中提供了Intent机制来协助应用间的交互与通讯,Intent负责对应用中一次操作的动作.动作涉及数据.附加数据进行描述,Android则根据此Intent的描述,负责找到对应的组件,将 ...

  7. GHM论文笔记(CVPR2019)

    目录 作者要解决的问题 Focal loss(CVPR2017) Focal loss的解决方案 Focal loss的不足 设计思路 梯度与样本的关系 梯度分布计算方法:将0-1的梯度切bin,计算 ...

  8. CSS3结构类选择器补充

    :empty 没有子元素(包括文本节点)的元素 :not  否定选择器 <!DOCTYPE html> <html lang="en" manifest=&quo ...

  9. 框架里增加.env文件的作用

    在实际开发中我们常常遇到这样的问题,就是开发地点不固定,这就造成了我们需要频繁的更改数据库配置,给开发工作造成了麻烦,.env环境文件的出现解决了这个麻烦,我们只需要在不同的工作地点配置好.env文件 ...

  10. Radmin Server v3.5.1 汉化破解绿色版 第四版

    下载:https://pan.baidu.com/s/1skOXffJ 使用方法:1.运行“安装.bat”,安装过程静默,安装后无托盘图标,不创建任何快捷方式.2.运行“设置.bat”,进入 radm ...