前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >源码剖析 Mybatis 映射器(Mapper)工作原理

源码剖析 Mybatis 映射器(Mapper)工作原理

作者头像
田守枝
修改2019-07-10 16:08:57
5.8K1
修改2019-07-10 16:08:57
举报
文章被收录于专栏:田守枝的技术博客

Mybatis可以说是目前国内使用最广泛的ORM框架。最原始的使用方式下,我们将sql写在xml配置文件中,通过SqlSession,根据statementId来唯一指定要执行的sql。从Mybatis 3.0之后,我们可以通过一个Mapper映射接口来完成相同的功能。你是否思考过,Mapper映射接口内部是如何完成这样的功能的。本文从源码的角度,深入分析mybatis 映射器接口的工作原理。

1 基础回顾

在最原始的情况下,我们需要使用SqlSession类,通过namespace.id方式来定位一个sql,如:

代码语言:javascript
复制
String namespace="com.tianshouzhi.mybatis.UserMapper";sqlSession.insert(namespace+".insert",user);sqlSession.selectOne(namespace+".selectById",1);sqlSession.update(namespace+".update",user);sqlSession.delete(namespace+".deleteById",1);

从Mybatis 3.0开始,引入了Mapper映射器接口,我们可以直接通过一个接口来引用需要使用的sql。只要这个接口满足以下条件,即可以引用xml配置文件中的sql:

  • 接口的全路径就是映射文件的namespace属性值
  • 接口中定义的方法,方法名与映射文件中<insert>、<select>、<delete>、<update>等元素的id属性值相同

例如,定义一个UserMapper接口

代码语言:javascript
复制
package com.tianshouzhi.mybatis;public interface UserMapper {    public int insert(User user);    public User selectById(int id);    public int updateById(User user);    public int deleteById(int id);}

之后我们就可以直接使用UserMapper类来进行增删改查,如下:

代码语言:javascript
复制
User user=...SqlSession  sqlSession = sqlSessionFactory.openSession();try{    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);    int insertCount = userMapper.insert(user);    user=userMapper.selectById(1);    userMapper.updateById(user);    userMapper.deleteById(1);} finally {    sqlSession.close();}

这里的原理很简单:

当接口方法执行时,首先通过反射拿到当前接口的全路径当做namespace,然后把执行的方法名当成id,拼接成namespace.id,最后在xml映射文件中寻找对应的sql

在匹配上某个sql之后,底层实际上还是利用SqlSession的相关方法来进行操作,只不过这个过程对于用户来说屏蔽了。

另外,mybatis还会自动的根据Mapper接口方法的返回值类型,选择调用SqlSession的不同方法。例如:

  • 返回一个对象,则调用selectOne方法;
  • 返回List,则调用selectList;
  • 返回Map,则调用selectMap。

你可能会好奇,Mapper映射接口,是如何完成这些功能的。在接下来的内容中,笔者将从源码角度来分析Mybatis内部是如何使用JDK动态代理机制来完成这些功能,我们带着几个问题开始源码分析之旅:

  • SQL与Mapper接口的绑定关系是如何建立的?
  • 动态代理类是按照什么逻辑生成的?
  • 动态代理类是如何对方法进行拦截并处理的?

2 SQL与Mapper接口的绑定关系是如何建立的?

这个过程在mybatis初始化阶段,解析xml配置文件的时候就确定了。具体逻辑是,当解析一个xml配置文件时,会尝试根据<mapper namespace="....">的namespace属性值,判断classpath下有没有这样一个接口的全路径与namespace属性值完全相同,如果有,则建立二者之间的映射关系。

关解析代码位于XMLMapperBuilder的 parse方法中:

XMLMapperBuilder#parse

代码语言:javascript
复制
public void parse() {  if (!configuration.isResourceLoaded(resource)) {    configurationElement(parser.evalNode("/mapper"));    configuration.addLoadedResource(resource);   //根据namespace属性值,尝试绑定对应的Mapper接口    bindMapperForNamespace();  }  ...}

bindMapperForNamespace方法名,既可以看出来,其作用正是将Mapper映射器接口绑定到某个xml文件的namespace属性值。具体逻辑如下:

XMLMapperBuilder#bindMapperForNamespace

代码语言:javascript
复制
private void bindMapperForNamespace() {  //1 获得mapper元素的namespace属性值   String namespace = builderAssistant.getCurrentNamespace();  if (namespace != null) {    Class<?> boundType = null;    try {      //2、通过反射,尝试以namespace属性值为全路径,加载对应Mapper接口的Class对象      boundType = Resources.classForName(namespace);    } catch (ClassNotFoundException e) {      //3、如果没有对应的Mapper接口,将会抛出ClassNotFoundException      // 意味着没有对应的Mapper接口,不需要绑定    }    if (boundType != null) {      if (!configuration.hasMapper(boundType)) {        configuration.addLoadedResource("namespace:" + namespace);        //4、如果存在这个Mapper,将其添加到Configuration类中        configuration.addMapper(boundType);      }    }  }}

从上述源码的第4步中,调用了Configuration的addMapper方法,来维护需要生成动态代理类的Mapper接口。此外,Configuration还提供了一个getMapper方法,这个方法返回的就是Mapper接口的JDK动态代理类。 相关源码如下所示:

org.apache.ibatis.session.Configuration

代码语言:javascript
复制
public class Configuration {...protected MapperRegistry mapperRegistry = new MapperRegistry(this);...public <T> void addMapper(Class<T> type) {  mapperRegistry.addMapper(type);}public <T> T getMapper(Class<T> type, SqlSession sqlSession) {  return mapperRegistry.getMapper(type, sqlSession);}...}

可以看到,Configuration类实际上将addMapper和getMapper委派给了MapperRegistry来执行:

  • addMapper方法会针对这个Mapper接口生成一个MapperProxyFactory工厂类。
  • getMapper方法,会通MapperProxyFactory工厂类,返回一个Mapper接口的动态代理类。

相关源码如下:

MapperRegistry#addMapper

代码语言:javascript
复制
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();public <T> void addMapper(Class<T> type) {  //1 判断传入的type是否是一个接口,如果不是,则忽略。意味着Mapper必须是接口类型。  if (type.isInterface()) {    //2、判断Mapper之前是否已经注册过,如果注册过就抛出异常    if (hasMapper(type)) {      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");    }    boolean loadCompleted = false;    try {      //3、针对Mapper接口的Class对象,生成一个MapperProxyFactoy工厂类,用于之后为这个Mapper接口生成动态代理类      //同时,将Class和MapperProxyFactoy的映射关系放入一个HashMap中,之后根据Class,就可以找到对应的工厂类      knownMappers.put(type, new MapperProxyFactory<T>(type));      //4、解析Mapper映射器接口方法上的注解,如@Select、@Insert等,并进行注册,这里不赘述      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);      parser.parse();      loadCompleted = true;    } finally {      if (!loadCompleted) {        knownMappers.remove(type);      }    }  }}

MapperRegistry还提供了一个getMapper方法,用于根据指定Mapper接口,返回其动态代理类。如下:

MapperRegistry#getMapper

代码语言:javascript
复制
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {    //1、首先根据type参数,找到对应的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工厂类,创建这个Mapper接口动态代理      return mapperProxyFactory.newInstance(sqlSession);    } catch (Exception e) {      throw new BindingException("Error getting mapper instance. Cause: " + e, e);    }  }

注意第2步,在通过MapperProxyFactory创建代理类的时候,把SqlSession当做了参数。这是因为,动态代理类的内部实际上还是需要通过SqlSession来进行增删改查。

3 动态代理类是何时生成的?

每次当我们调用sqlSession的getMapper方法时,都会创建一个新的动态代理类实例。例如有以下代码:

代码语言:javascript
复制
UserMapper mapper = sqlSession.getMapper(UserMapper.class);

这里返回的实际上就是一个动态代理类。其内部实现如下所示:

DefaultSqlSession#getMapper

代码语言:javascript
复制
public class DefaultSqlSession implements SqlSession {  private Configuration configuration;...  @Override  public <T> T getMapper(Class<T> type) {    return configuration.<T>getMapper(type, this);  }...  }

这里我们看到了SqlSession将将getMapper方法委给了Configuration对象执行。前面已经分析过,在xml解析的时候,就会将Mapper映射接口添加到Configuration内部维护的MapperRegistry中,显然,Configuration的getMapper方法,会继续委派给MapperRegistry来执行。

前面已经看到,MapperRegistry内部是通过已注册MapperProxyFactory的newInstance方法来创建代理,因此这里接着就要分析newInstance方法。

MapperProxyFactory

代码语言:javascript
复制
public class MapperProxyFactory<T> {  private final Class<T> mapperInterface;  ...  public MapperProxyFactory(Class<T> mapperInterface) {    this.mapperInterface = mapperInterface;  }  ...  //1、首先根据sqlSession创建一个MapperProxy对象,MapperProxy实现了JDK动态代理中的InvocationHandler接口  public T newInstance(SqlSession sqlSession) {    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);    return newInstance(mapperProxy);  }  //2、利用JDK提供的Proxy类,创建动态代理。    protected T newInstance(MapperProxy<T> mapperProxy) {    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface },    mapperProxy);  }}

可以看到,MapperProxyFactory的newInstance(sqlSession)方法中,首先会创建一个MapperProxy对象,然后将其当做参数传递给newInstance(mapperProxy)方法,这个方法内部通过JDK提供的Proxy.newProxyInstance方法生成动态代理类。

在JDK动态代理机制中,对方法的拦截是通过回调InvocationHandler接口的invoke方法实现的。在这里,MapperProxy类实现了InvocationHandler接口的invoke方法,因此我们只要从这个方法入手进行分析,既可以得出代理逻辑:

MapperProxy#invoke

代码语言:javascript
复制
@Override  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {    //1 如果当前执行的方法,是在Object类中定义的,    //如:equals、hashcode、toString等,    //无须对方法进行代理,直接反射执行。    if (Object.class.equals(method.getDeclaringClass())) {      try {        return method.invoke(this, args);      } catch (Throwable t) {        throw ExceptionUtil.unwrapThrowable(t);      }    }    //2、将Mapper接口当前被调用的方法Method包装成一个MapperMethod对象    final MapperMethod mapperMethod = cachedMapperMethod(method);    //3 调用MapperMethod的execute方法进行l拦截。    return mapperMethod.execute(sqlSession, args);  }

可以看出,对接口方法的核心代理逻辑,显然是位于MapperMethod类execute方法中。之前提到根据Mapper接口全路径+方法名,找到对应的namespace.id,以及根据Mapper方法返回值的不同,执行SqlSession的不同方法,如selectList、selectMap等,都是在这里实现的。

4 MapperMethod如何对Mapper方法拦截的?

现在我们定位到,最终的拦截代码位于MapperMethod类的execute方法中,当把这个方法的代码分析完成,本文的内容也就分析完成了。

从MapperMethod的构造方法开始看起:

org.apache.ibatis.binding.MapperMethod

代码语言:javascript
复制
public class MapperMethod {  private final SqlCommand command;  private final MethodSignature method;  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {    this.command = new SqlCommand(config, mapperInterface, method);    this.method = new MethodSignature(config, mapperInterface, method);  } ...}

在MapperMethod的构造方法中,给SqlCommandMethodSignature两个类型的成员变量进行了赋值,这两个类都是MapperMethod的内部类。

这里不对SqlCommand源码继续展开分析,主要关注在构造SqlCommand对象的时候,传入了3个参数:

  • mapperInterface:Mapper映射器接口的class对象
  • method:当前调用的Mapper映射器接口的方法对象
  • config:表示mybatis配置解析后的对象(前面我们已经看到过)

通过这3个参数,SqlCommand可以为我们提供以下信息:

1 唯一定位当前被调用的Mapper接口的方法,对应的要执行的sql

这个很容易做到,有了mapperInterface,以及method。就可以通过以下方式,来拼接出namespace.id

代码语言:javascript
复制
String statementId = mapperInterface.getName() + "." + methodName;

SqlCommand提供了一个getName方法,返回这个namespace.id。这也是为什么,要求Mapper映射接口,要与xml映射文件namespace属性值相同,方法名与<insert>、<select>等xml元素的id属性值相同的原因。

2 确定要执行的sql的类型

如INSERT、UPDATE、DELETE、SELECT等。因为底层还是通过SqlSession来执行,因此必须知道要执行的sql的类型,选择调用SqlSession的不同方法,如insert、delete、update、selectOne、selectList等。

在第一步确定了要执行的sql的statementId之后,我们可以通过Configuration类来获得这个statementId对应的MappedStatement对象。mybatis在解析xml的过程中,会将<insert>、<select>等xml元素都封装成一个MappedStatement对象,其提供了一个getSqlCommandType()方法,表示这个sql的类型。这个逻辑可以用以下简化后的代码来表示:

代码语言:javascript
复制
String statementId = mapperInterface.getName() + "." + method.getName();MappedStatement ms = configuration.getMappedStatement(statementName);SqlCommandType type= ms.getSqlCommandType();

有了这两个信息之后,我们来看MapperMethod的execute方法是如何执行的?

MapperMethod#execute

代码语言:javascript
复制
 public Object execute(SqlSession sqlSession, Object[] args) {    Object result;    //根据SqlCommand的不同类型,调用sqlSession不同的方法     //1、执行sqlSession.insert    if (SqlCommandType.INSERT == command.getType()) {      Object param = method.convertArgsToSqlCommandParam(args);      result = rowCountResult(sqlSession.insert(command.getName(), param));    //2、执行sqlSession.update    } else if (SqlCommandType.UPDATE == command.getType()) {      Object param = method.convertArgsToSqlCommandParam(args);      result = rowCountResult(sqlSession.update(command.getName(), param));    //3、执行sqlSession.delete    } else if (SqlCommandType.DELETE == command.getType()) {      Object param = method.convertArgsToSqlCommandParam(args);      result = rowCountResult(sqlSession.delete(command.getName(), param));    //4、对于select,根据Mapper接口方法的返回值类型,选择调用SqlSession的不同方法    } else if (SqlCommandType.SELECT == command.getType()) {      if (method.returnsVoid() && method.hasResultHandler()) {        executeWithResultHandler(sqlSession, args);        result = null;      //4.1 如果方法的返回值是一个集合,调用selectList方法      } else if (method.returnsMany()) {        result = executeForMany(sqlSession, args);      //4.2 如果方法的返回值是一个Map,调用selectMap方法      } else if (method.returnsMap()) {        result = executeForMap(sqlSession, args);      //4.3 如果方法的返回值是Cursor,调用selectCursor方法      } else if (method.returnsCursor()) {        result = executeForCursor(sqlSession, args);      //4.4 否则调用sqlSession.selectOne方法       } else {        Object param = method.convertArgsToSqlCommandParam(args);        result = sqlSession.selectOne(command.getName(), param);      }    } else if (SqlCommandType.FLUSH == command.getType()) {        result = sqlSession.flushStatements();    } 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;  }

至此,我们已经基本上从源码层面已经深入的分析了Mybatis的Mapper映射接口的内部工作原理,简单总结就是一句话:通过JDK动态代理,根据映射器接口+当前要执行的方法,确定要执行的sql,对sql的类型进行处理,最后还是委派给SqlSession来完成。

需要注意的是:这里的源码分析进行了一定程度上的简化,建议读者还是需要自行阅读源码,加深理解。

另外,本文我们仅仅讨论了单独使用Mybatis时,Mapper映射器接口是如何工作的。在实际开发中,通常Mybatis是与Spring整合的,我们可以在service层通过@Autowired注解,直接注入一个Mapper。这里的核心要点是,如何将Mybatis的Mapper接口变成spring 上下文中的一个bean,只有这样才能支持自动注入。在下一篇文章,笔者将深入分析mybatis-spring的源码,深入剖析MapperScannerConfigurer的内部实现原理,是如何将Mapper接口转换为spring中的bean。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-04-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 田守枝的技术博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 基础回顾
  • 2 SQL与Mapper接口的绑定关系是如何建立的?
  • 3 动态代理类是何时生成的?
  • 4 MapperMethod如何对Mapper方法拦截的?
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档