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

原创
2019/04/10 08:40
阅读数 0

点击上方"田守枝的技术博客",关注我

       

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


1 基础回顾

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

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接口

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类来进行增删改查,如下:

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

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

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

XMLMapperBuilder#bindMapperForNamespace

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

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

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

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方法时,都会创建一个新的动态代理类实例。例如有以下代码:

UserMapper mapper = sqlSession.getMapper(UserMapper.class);

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

DefaultSqlSession#getMapper

public class DefaultSqlSession implements SqlSession {  private Configuration configuration;...  @Override  public <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

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

@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

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

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的类型。这个逻辑可以用以下简化后的代码来表示:

String statementId = mapperInterface.getName() + "." + method.getName();MappedStatement ms = configuration.getMappedStatement(statementName);SqlCommandType type= ms.getSqlCommandType();

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

MapperMethod#execute

 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。 

识别二维码关注我

近期发表:

剖析Spring多数据源

Mysql分支选择:Percona Or MariaDB

异地多活场景下的数据同步之道

分布式事务概述

数据库中间件详解

本文分享自微信公众号 - 田守枝的技术博客(tianshouzhi_blog)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部