Mybatis源码解析(三) —— Mapper代理类的生成

  在本系列第一篇文章已经讲述过在Mybatis-Spring项目中,是通过 MapperFactoryBean 的 getObject()方法来获取到Mapper的代理类并注入到Spring容器中的。在学习本章之前我们先提出以下几点问题:

  • 1、 Mapper接口是如何被加载 到 Configuration 中的?

  • 2、 Mapper代理类是如何生成的?

  • 3、 Mapper代理类是如何实现接口方法的?

   本章内容就是围绕着上面三个问题进行解析,那么带着问题去看源码吧!

一、加载 Mapper接口

  针对Mybatis项目,Mapper的配置加载是从XmlConfigBuilder.mapperElement()方法中触发的。我们来看下源码:

  private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// 通过 package 形式加载 ,内部其实也是获取到 package 路径下的所有class再通过 class 形式加载
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
// 加载 Mapper.xml 的
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
// 加载 Mapper.xml 的
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// 通过 class 形式加载
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}

从上面源码看,加载Mapper接口有2种形式: 一种是根据设置的 package 找到路径下面所有的class并通过configuration.addMapper() 加载。 另一种是根据指定设置的Mapper接口路径直接通过 configuration.addMapper()加载class。所以加载Mapper接口最终都是 configuration.addMapper() 来加载的。

  而针对Mybatis-Spring项目则是 获取 MapperScannerConfigurer 的 basePackage 参数,并通过 ClassPathMapperScanner 扫描到 设置的 basePackage 路径下的所有class ,并得到 BeanDefinition ,后面的情况在第一篇文章已经讲过,最终是得到了 MapperFactoryBean ,并且还到了 MapperFactoryBean 内部的 checkDaoConfig() 方法加载Mapper接口的内容,那么我们再次回顾下这个方法的源码:


@Override
protected void checkDaoConfig() {
super.checkDaoConfig(); notNull(this.mapperInterface, "Property 'mapperInterface' is required"); Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
// 最终都是通过 addMapper() 方法加载的。
configuration.addMapper(this.mapperInterface);
} catch (Throwable t) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", t);
throw new IllegalArgumentException(t);
} finally {
ErrorContext.instance().reset();
}
}
}

   从源码中我们发现 内部其实还是通过 configuration.addMapper() 加载的。可能有些同学会问,checkDaoConfig()什么时候被调用的,这个可以追溯到 MapperFactoryBean 的继承关系图,可以发现实现了 InitializingBean接口, 而 checkDaoConfig() 就是 通过afterPropertiesSet() 调用的。所以在MapperFactoryBean 初始化创建的时候就会调用checkDaoConfig(),即 加载Mapper接口。

   根据上面的分析,我们可以发现 configuration.addMapper() 是实现加载Mapper接口的最核心的方法,那么我们就来好好分析下这个方法内部实现源码:


public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}

  其内部是通过 委托 mapperRegistry 来就行加载的,那继续往下看:


public <T> void addMapper(Class<T> type) {
// 1、 判断是否为接口
if (type.isInterface()) {
// 2、 判断是否已加载
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 3、 将 Mapper的class作为 MapperProxyFactory(生成Mapper代理对象的工厂类) 的构参,并保存到 knownMappers 中。
knownMappers.put(type, new MapperProxyFactory<T>(type));
// 4、 解析 Class对象中,包含的所有mybatis框架中定义的注解,并生成Cache、ResultMap、MappedStatement。
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}

   根据源码可以把整个加载流程分4个步骤:

  • 1、 判断是否为接口

  • 2、 判断是否已加载

  • 3、 将 Mapper的class作为 MapperProxyFactory(生成Mapper代理对象的工厂类) 的构参,并保存到 knownMappers 中。

  • 4、 解析 Class对象中,包含的所有mybatis框架中定义的注解,并生成Cache、ResultMap、MappedStatement。

   其中第三步是加载Mapper的核心,也就是同创建了一个生成Mapper代理对象的工厂对象,并将其放到map,等需要创建Mapper代理对象的是再通过获取map中的工厂对象即可。 关于第四步,就是最近几年比较流行的通过注解编写SQl形式的解析方法。我们知道mybatis支持xml和注解形式的Sql编写。所以 MapperAnnotationBuilder 就是解析注解形式,根解析xml一样, 最终也会生成 ResultMap、MappedStatement对象封装到 configuration 中。关于它是如何解析的,有兴趣的同学可以看啊可能源码,这里不在描述。

二、 Mapper动态代理对象(MapperProxy)的创建

   通过之前的文章,我们知道 MapperFactoryBean 实现了 FactoryBean,也就是说在Spring 根据BeanDefinition 加载Bean的时候会调用 MapperFactoryBean.getObject() 获取真实的Bean并注入到容器中。不用想,getObject()获取到的一定是Mapper接口的代理实现类 MapperProxy,那么我们来一步步分析是如何创建 MapperProxy


public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}

   getObject()方法是通过 getSqlSession().getMapper() 获取到 MapperProxy 的,相信大家对这个不陌生吧。至于这里的SqlSession 其实是 SqlSessionTemplate ,这个之前也讲过,所以继续查看:


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

   最终还是通过 configuration().getMapper() 获取到 MapperProxy。继续查看:


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

   毫不意外的肯定是委托 mapperRegistry.getMapper() 来获取,继续查看:


public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 1、 从 knownMappers 中获取到 MapperProxyFactory
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null)
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
try {
// 2、 通过 mapperProxyFactory.newInstance() 创建 MapperProxy
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}

   也毫不意外的是 从 knownMappers 中获取到 MapperProxyFactory ,再 通过 mapperProxyFactory.newInstance() 创建 MapperProxy。 继续查看 mapperProxyFactory.newInstance() 内部实现:


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

   从上面源码可以清晰的看到 Proxy.newProxyInstance() 指定了被代理的类是 mapperInterface,其代理类是 mapperProxy,所以最终动态创建出 mapperInterface 的动态代理类 MapperProxy@xxxx (动态代理类名)

三、 MapperProxy 接口方法的实现

  通过上面的解析,我们明确了 MapperProxy 代理是通过JDK动态生成,但接口方法是如何实现的呢? 这里就得看 MapperProxy 源码:


public class MapperProxy<T> implements InvocationHandler, Serializable { private static final long serialVersionUID = -6424540398559729838L;
// 注意: 这个 SqlSession 实际上是 SqlSessionTemplate
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;
} public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 如果调用的方法是 Object 种定义的方法,直接执行
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
// 接口方法的调用都是通过 MapperMethod 来执行的
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
} // Mapper接口的每个方法 都会生成一个 MapperMethod 对象,并通过 methodCache 来维护它们之间的关系
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;
} }

  从上面源码中,我们可以发现,接口方法的实现其实是通过 MapperMethod 来实现的,且 Mapper接口的每个方法 都会生成一个 MapperMethod 对象,并通过 methodCache 来维护它们之间的关系,而 methodCache 是通过 MapperProxyFactory 传递下来的。

MapperMethod

   MapperMethod 实现接口方法的入口是 execute()方法,我们来看下其内部源码:

  // 实际上都是调用 SqlSession的方法实现
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
// 判断 索要执行的方法类型
if (SqlCommandType.INSERT == command.getType()) {
// 参数转换
Object param = method.convertArgsToSqlCommandParam(args);
// 执行insert
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
// 执行 update
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
// 执行 delete
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
// 执行 select
if (method.returnsVoid() && method.hasResultHandler()) {
// 没有返回
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 返回 List
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 返回 Map
result = executeForMap(sqlSession, args);
} else {
// 返回 一个对象
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
} else {
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;
}

   根据源码我们可以发现其实内部都是 委托 SqlSession 的方法实现的,但它是如何区别什么时候调用哪个 SqlSession 的方法呢?这个就不得不说 MapperMethod 内部维护的 SqlCommand 对象,我们查看 SqlCommand 的构造方法:


public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) throws BindingException {
// 补全 方法的全名称路径 即 com.xxx.selectByName
String statementName = mapperInterface.getName() + "." + method.getName();
MappedStatement ms = null;
// 从 configuration 中获取到 MappedStatement 对象
if (configuration.hasStatement(statementName)) {
ms = configuration.getMappedStatement(statementName);
} else if (!mapperInterface.equals(method.getDeclaringClass().getName())) { // issue #35
String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
if (configuration.hasStatement(parentStatementName)) {
ms = configuration.getMappedStatement(parentStatementName);
}
}
if (ms == null) {
throw new BindingException("Invalid bound statement (not found): " + statementName);
}
// 从 MappedStatement 中获取到方法名 (注意: 节点中的id属性包含命名空间)
name = ms.getId();
// 从 MappedStatement 中获取到 方法的节点标签,即 select|insert|update|delete
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}

   我们通过分析知道 ,SqlCommand 其实是通过从 MappedStatement 中获取到 方法名,以及所要执行的SQl命令类型(select|insert|update|delete)。 这里我们可以明确的发现从configuration 中 获取 MappedStatement 是通过 全称路径的方法去获取的,即 com.xxx.selectByName 这种,调用SqlSession的方法虽然是从 MappedStatement 中获取 id (注意: 节点中的id属性包含命名空间),但实质上都是 com.xxx.selectByName。但我们可以通过这里看出mybatis的Mapper接口方法是不可以重载的。

四、 个人总结

  • 1、 Configuration 维护了一个 MapperRegistry 对象,该对象主要作用就是加载Mapper接口和获取MapperProxy。

  • 2、 MapperRegistry 维护了一个key为 mapper接口class对象,value为 MapperProxyFactory 的map

  • 3、 MapperProxy 是 通过 MapperProxyFactory 创建的。

  • 4、 MapperProxy 实现 Mapper接口方法是委托 MapperMethod 执行的。

  • 5、 MapperMethod 执行接口方法时是通过 SqlCommand 来判断要执行的具体 SQL节点,并且最终委托 SqlSession执行。

  • 6、 SqlCommand 内部的 信息是通过从 MappedStatement 中获取的。

         如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!

本文由博客一文多发平台 OpenWrite 发布!

Mybatis源码解析(三) —— Mapper代理类的生成的更多相关文章

  1. Mybatis源码解析5—— 接口代理

    本篇文章,可乐将为大家介绍通过接口代理的方式去执行SQL操作.话不多说,直接上图: 其实无论哪种方式,我们最终是需要找到对应的 SQL 语句,接口代理的方式就是通过 [包名.方法名] 的方式,去找到 ...

  2. Mybatis源码解析,一步一步从浅入深(六):映射代理类的获取

    在文章:Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码中我们提到了两个问题: 1,为什么在以前的代码流程中从来没有addMapper,而这里却有getMapper? 2,UserDao ...

  3. mybatis源码-解析配置文件(四)之配置文件Mapper解析

    在 mybatis源码-解析配置文件(三)之配置文件Configuration解析 中, 讲解了 Configuration 是如何解析的. 其中, mappers作为configuration节点的 ...

  4. mybatis源码-解析配置文件(三)之配置文件Configuration解析

    目录 1. 简介 1.1 系列内容 1.2 适合对象 1.3 本文内容 2. 配置文件 2.1 mysql.properties 2.2 mybatis-config.xml 3. Configura ...

  5. Mybatis源码解析,一步一步从浅入深(三):实例化xml配置解析器(XMLConfigBuilder)

    在上一篇文章:Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码 ,中我们看到 代码:XMLConfigBuilder parser = new XMLConfigBuilder(read ...

  6. Mybatis源码解析,一步一步从浅入深(五):mapper节点的解析

    在上一篇文章Mybatis源码解析,一步一步从浅入深(四):将configuration.xml的解析到Configuration对象实例中我们谈到了properties,settings,envir ...

  7. Mybatis源码解析3——核心类SqlSessionFactory,看完我悟了

    这是昨晚的武汉,晚上九点钟拍的,疫情又一次来袭,曾经熙熙攘攘的夜市也变得冷冷清清,但比前几周要好很多了.希望大家都能保护好自己,保护好身边的人,生活不可能像你想象的那么好,但也不会像你想象的那么糟. ...

  8. mybatis源码-解析配置文件(四-1)之配置文件Mapper解析(cache)

    目录 1. 简介 2. 解析 3 StrictMap 3.1 区别HashMap:键必须为String 3.2 区别HashMap:多了成员变量 name 3.3 区别HashMap:key 的处理多 ...

  9. Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码

    在文章:Mybatis源码解析,一步一步从浅入深(一):创建准备工程,中我们为了解析mybatis源码创建了一个mybatis的简单工程(源码已上传github,链接在文章末尾),并实现了一个查询功能 ...

随机推荐

  1. vue - 基础(1)

    Vue基本用法 在学习Vue的基本用法之前,我们先简单的了解一些es6的语法 let: 特点:1.局部作用域 2.不会存在变量提升 3.变量不能重复声明 const: 特点:1.局部作用域 2.不会存 ...

  2. VMware遇到的一连串问题

    之前正常运行的VMware,再次打开提示“VMware Workstation pro 无法在 windows 上运行”, 百度一波,原来是微软的更新程序引起的问题,只要将最近的一次更新程序卸载然后重 ...

  3. 复制excel表中的数据

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. 协程,yield,i多路复用,复习

    课程回顾: 线程池 队列:同一进程内的队列 先进先出 后进先出 优先级队列 线程池中的回调函数是谁在调用? 线程池中的回调函数是子线程调用的,和父线程没有关系 进程池中的会点函数是父进程调用的,和子进 ...

  5. proc介绍,free命令查看内存

    proc介绍 https://www.cnblogs.com/dongzhuangdian/p/11366910.html https://blog.csdn.net/majianting/artic ...

  6. Java实现递归阶乘

    public class Factorial{ public static void main(String[] args){ for (int i = -5; i <= 5; i++) { S ...

  7. K8s预选策略和优选函数简介

    调度器选择策略: 预选策略(Predicate) 1. 根据运行Pod的资源限制来排除不符合要求的Node 2. 根据运行Pod时,是否要求共享宿主机的网络名称空间来判断,如: 某Pod启动要共享宿主 ...

  8. NDCG、AUC介绍

    https://blog.csdn.net/u014313009/article/details/38944687 SIGIR的一篇推荐算法论文中提到用NDCG和AUC作为比较效果的指标,之前没了解过 ...

  9. ipv6转ipv4 NAT64与DNS64基本原理概述

    原文: https://blog.csdn.net/zhangjie1989/article/details/51464251 1.NAT64与 DNS64背景 在 IPv6网络的发展过程中,面临最大 ...

  10. 每日一问:你了解 Java 虚拟机结构么?

    对于从事 C/C++ 程序员开发的小伙伴来说,在内存管理领域非常头疼,因为他们总是需要对每一个 new 操作去写配对的 delete/free 代码.而对于我们 Android 乃至 Java 程序员 ...