MyBatis核心源码解读

一、前言

每个基于MyaBatis的应用都是以"一个"SqlSessionFactory实例为核心和基础的,而SqlSessionFactory是由SqlSessionFactoryBuilder创建的,而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先定制的 Configuration 的实例构建出 SqlSessionFactory 的实例。

二、MyBatis内部的几个类

1、SqlSessionFactoryBuilder

创建 SqlSessionFactory 实例。

2、SqlSessionFactory

SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,不能销毁或重新创建另一个SqlSessionFactory实例。 SqlSessionFactory 在应用运行期间不要重复创建多次。它的作用就是开启一个SqlSession,用户直接与SqlSession打交道。

3、SqlSession

SqlSession 完全包含了面向数据库执行 SQL 命令所需的所有方法。可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。

try (SqlSession session = sqlSessionFactory.openSession()) {
  Author author = (Author) session.selectOne("org.mybatis.example.AuthorMapper.selectAutor", 1);
}

更简洁的方式 ——使用接口的形式,可以执行更清晰和类型安全的代码。

try (SqlSession session = sqlSessionFactory.openSession()) {
  AuthorMapper mapper = session.getMapper(AuthorMapper.class);
  Author author = mapper.selectBlog(101);
}

确保 SqlSession 关闭的标准模式:

// 接口SqlSession 继承了Closeable接口,可以使用Java7 的 try-with-resources语法来保证session的关闭
try (SqlSession session = sqlSessionFactory.openSession()) {
  // just do it 
}

4、Configuration
存储了MyBatis的所有配置项和解析后的信息,极为重要的一个类。MyBatis初始化时,将解析出的信息一股脑的扔给Configuration进行存储。执行sql语句或者其它操作时,从此类里取出相关的配置信息进行操作。
两个重要属性:

  • MapperRegistry mapperRegistry; 映射注册机实例。
  • Map mappedStatements,存储了 dao接口完全限定名+方法名 - MappedStatement实例的键值对。

5、XMLConfigBuilder
XML配置构建器,建造者模式。解析MyBatis的xml配置文件并以Document的形式保存下来,方便其内部解析方法解析出MyBatis的具体配置信息。

6、MapperRegistry
映射注册机,其属性 Map, MapperProxyFactory> knownMappers 存储了 Class对象 - 映射器代理工厂实例 的键值对,MyBatis初始化时存入,使用时取出

7、MapperProxyFactory
映射器代理工厂,映射器代理类的工厂方法,生产一个dao接口的代理实例提供给用户。

8、MapperProxy
映射器代理类,利用jdk动态代理返回给用户代理类实例,除Object通用方法(toString()、hashcode()等等)、接口默认方法(java8 新增默认方法)外,皆走代理方法,在代理方法内部执行数据库的crud。

9、MappedStatement
映射语句,存储了解析注解、xml后的sql语句相关信息。

10、MapperMethod
映射器方法。负责sql语句参数填充及语句的分类执行。
两个极为重要的属性:
SqlCommand一个内部类 封装了SQL标签的类型 insert delete update select
MethodSignature一个内部类 封装了方法的参数信息 返回类型信息等

一个execute方法,负责执行sql语句,并将结果处理好后返回给用户。

三、配置

可以通过XML文件方式和代码方式构建SqlSessionFactory,建议通过XML文件方式进行构建,简单、直观、明了


配置文件.png

四、使用

    // 配置文件路径
    String resource = "org/apache/ibatis/builder/MapperConfig.xml";
    /*
     * Resources作用:通过类加载器简化对资源访问的类。
     * 读取配置文件内容,返回流
     */
    InputStream inputStream = Resources.getResourceAsStream(resource);
    // 解析并构建出SqlSessionFactory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    // SqlSession实现了Closeable接口
    try(SqlSession sqlSession = sqlSessionFactory.openSession()) {
        // 实际上获取到的是一个通过代理对象
        AuthorMapper authorMapper = sqlSession.getMapper(AuthorMapper.class);
        // 执行查询sql,并进行结果集映射
        List list = author.getAuthors();
    }

上述代码,就是使用MyBatis查询数据库某个表,并返回结果列表的过程,增删改一样的道理。在返回结果集后,sqlSession调用close()方法,关闭这个session。

这套配置及使用没有引入Spring和MyBatis-Spring。关于MyBatis-Spring在这里不做过多解释,有兴趣可以自己看一下MyBatis-Spring的文档及源码,基本上和我们要把MyBatis融入到Spring的思路差不太多。

五、开始解读

1、SqlSessionFactoryBuilder().build(inputStream)

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream)

这行代码的意思是通过SqlSessionFactoryBuilder创建一个SqlSessionFactory,而build具体的内部创建细节如下:

public class SqlSessionFactoryBuilder {        
          /*
           * ① 在这里,内部调用了build(InputStream inputStream, String environment, Properties properties)方法
           */
          public SqlSessionFactory build(InputStream inputStream) {
            return build(inputStream, null, null);
          }

          /*
           * ②
           * 文件流、环境配置、属性,后两者都为null,有可能在MyBatis配置文件中有定义
           */
          public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
            try {
              /*
               * XML配置构建器 这样的写法棒棒哒,逻辑清晰明了,有点类似
               */
              XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
              /*
               * parser.parse() 返回MyBatis最重要的一个配置类 Configuration,作为DefaultSqlSessionFactory构造函数的参数传递进去,进行DefaultSqlSessionFactory的实例化
               */
              return build(parser.parse());
            } catch (Exception e) {
              throw ExceptionFactory.wrapException("Error building SqlSession.", e);
            } finally {
              ErrorContext.instance().reset();
              try {
                inputStream.close();
              } catch (IOException e) {
                // Intentionally ignore. Prefer previous error.
              }
            }
          }
        
          /*
           * 创建SqlSessionFactory
           */
          public SqlSessionFactory build(Configuration config) {
            return new DefaultSqlSessionFactory(config);
          }
        
        }

核心解析代码在XMLConfigBuilder.parse()方法里,由它解析出Configuration实例。
XMLConfigBuilder的parse代码如下:

XMLConfigBuilder:
    public Configuration parse() {
        // 只能解析一次 否则多次解析会打乱配置信息。而且多次解析本身就是一种错误
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }    
        // 将解析标识置为true,防止二次解析
        parsed = true;
        // 内部私有方法,进一步进行配置解析,解析目标节点为.....
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
      }

parseConfiguration(parser.evalNode("/configuration")); 内部私有方法,进一步进行配置解析。

XMLConfigBuilder:
      // 解析配置
      private void parseConfiguration(XNode root) {
        try {
    
          // #解析子节点properties
          // issue #117 read properties first
          // 这些属性都是可外部配置且可动态替换的,既可以在典型的 Java 属性文件中配置,亦可通过 properties 元素的子元素来传递
          propertiesElement(root.evalNode("properties"));
    
          // mybatis中很重要的配置
          Properties settings = settingsAsProperties(root.evalNode("settings"));
          loadCustomVfs(settings);
          loadCustomLogImpl(settings);
    
          // #解析子节点typeAliases 别名
          // 类型别名是为 Java 类型设置一个短的名字。 它只和 XML 配置有关,存在的意义仅在于用来减少类完全限定名的冗余。
          typeAliasesElement(root.evalNode("typeAliases"));
          // #解析子节点plugins 插件
          pluginElement(root.evalNode("plugins"));
          // #解析子节点objectFactory mybatis为结果创建对象时都会用到objectFactory
          objectFactoryElement(root.evalNode("objectFactory"));
          // #解析子节点objectWrapperFactory
          objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
          // 3.3.0之前不需要 有默认实现
          /*
            Reflector 这个类的用途主要是是通过反射获取目标类的 getter 方法及其返回值类型,setter 方法及其参数值类型等元信息。并将获取到的元信息缓存到相应的集合中,供后续使用
           */
          reflectorFactoryElement(root.evalNode("reflectorFactory"));
          settingsElement(settings);
          // #解析environments 可以配置多个运行环境,但是每个SqlSessionFactory 实例只能选择一个运行环境
          // read it after objectFactory and objectWrapperFactory issue #631
          environmentsElement(root.evalNode("environments"));
          // #解析databaseIdProvider MyBatis能够执行不同的语句取决于提供的数据库供应商。许多数据库供应商的支持是基于databaseId映射
          databaseIdProviderElement(root.evalNode("databaseIdProvider"));
          // #解析typeHandlers 当MyBatis设置参数到PreparedStatement 或者从ResultSet 结果集中取得值时,就会使用TypeHandler  来处理数据库类型与java 类型之间转换
          typeHandlerElement(root.evalNode("typeHandlers"));
          // #解析mappers 主要的crud操作都是在mappers中定义的,这个是主线,继续往下走
          mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
      }

在解析mapperElement方法之前,先看一下的配置方式:


  // ① 扫描该目录包下所有mapper接口 xml文件必须在同一目录
  
  // ② 使用相对classpath的路径
  
  // ③ 使用完全限定资源定位符(URL)
  
  // ④ 使用映射器接口实现类的完全限定名,xml文件必须在同一目录
  

mapperElement方法里,是这样解析的:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // attribute 目前只能是resource url class 其中之一
          if (resource != null && url == null && mapperClass == null) {
            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) {
            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) {
            // 返回对应Dao接口 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.");
          }
        }
      }
    }
  }

为了方便,我们选择resource == null && url == null && mapperClass != null这个条件分支。
Resouurces.classForName返回dao接口 Class对象,然后调用configuration.addMapper方法,configuration.addMapper里调用的是mapperRegistry.addMapper方法,加入到映射注册机。

MapperRegistry:
  
//将已经添加的映射都放入HashMap
private final Map, MapperProxyFactory> knownMappers = new HashMap, MapperProxyFactory>();

public  void addMapper(Class type) {
    //mapper必须是接口!才会添加
    if (type.isInterface()) {
      if (hasMapper(type)) {
        //如果重复添加了,报错
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // ①
        knownMappers.put(type, new MapperProxyFactory(type));
        // ②注解解析器,但同时也进行了同级目录下dao.xml的解析,如果没有xml文件,则去解析接口方法上的注解内容,如果二者皆有,报错 Mapped Statements collection already contains value for xx.xx.xx.xx
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        //如果加载过程中出现异常需要再将这个mapper从mybatis中删除,这种方式比较丑陋吧,难道是不得已而为之?
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

①创建映射器代理工厂,并加入到映射注册机的kownMappers

②在注解解析器的parse()方法里去探查同级目录下是否存在dao对应的xml文件。

加载sql信息时会有以下几种情况:

1)如果存在,则使用XMLMapperBuilder的parse方法,解析dao对应的配置文件为MappedStatement实例,将解析好的MappedStatement实例放入configuration的mappedStatements Map里,key是类的完全限定名+方法名,value就是MappedStatement实例

2)如果不存在dao接口对应的xml文件,则去加载注解sql。

public interface AtuhorMapper {
        @Select("select * from author")
        List getAuthors();
}

3)既存在注解又在xml里存在对应的sql标签,会抛出非法参数异常java.lang.IllegalArgumentException 提示: Mapped Statements collection already contains value for DAO类名.方法名()

4)如果二者皆不存在,启动没有任何问题,但是在项目里使用时,会通过dao接口完全限定名+方法名查找sql信息,没有找到会抛出Invalid bound statement (not found): cn.asae.e.contract.dao.ContractDao.方法

5)从这里我们知晓,dao接口里的方法不能重载,否则会抛出非法参数异常 提示: Mapped Statements collection already contains value for DAO类名.方法名()

ps: 再强调一遍,MappedStatement里存储着dao方法对应的sql所有信息。

以上是MyBatis启动时,需要做的工作,当然,我们仅仅只是讲了最为核心的dao接口与xml的绑定映射原理。

做完绑定映射之后,我们在对数据库进行crud时,又是怎么运行的呢?

2、sqlSession.getMapper(AuthorMapper.class)

sqlSession.getMapper返回的是AuthorMapper代理类的实例,其内部执行过程为:

1)调用configuration.getMapper

2)在configuration.getMapper里又调用了(映射注册机)mapperRegistry.getMapper,还有没有印象,在MyBatis初始化时,我们将dao的映射器代理工厂实例放入到了映射注册机里。

3)在mapperRegistry.getMapper方法里取出对应的映射器代理工厂,然后用JDK动态代理生成代理类实例返回给用户。代码如下:

MapperRegistry:
public  T getMapper(Class type, SqlSession sqlSession) {
    final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) 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);
    }
  }

MapperProxyFactory:
public class MapperProxyFactory {

  private final Class mapperInterface;
  private Map methodCache = new ConcurrentHashMap();

  public MapperProxyFactory(Class mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class getMapperInterface() {
    return mapperInterface;
  }

  public Map getMethodCache() {
    return methodCache;
  }

  // mapperProxy是一个实现java.lang.reflect.InvocationHandler接口的代理类
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy mapperProxy) {
    //用JDK自带的动态代理生成映射器
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

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

}

将代理类实例返回给用户之后,用户进行了如下操作:
List list = author.getAuthors();
实际上调用的是代理类的invoke方法。

MapperProxy如下:

public class MapperProxy implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class mapperInterface;
  private final Map methodCache;

  public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  // mybatis dao 执行处理核心入口
  // Method类,主要用于在程序运行状态中,动态地获取方法信息, args 参数
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // 代理以后,所有Mapper的方法调用时,都会调用这个invoke方法
      // 并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行
      // 方法返回表示声明由此Method对象表示的方法的类的Class对象
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    // 第一次肯定为空,之后由于缓存了,所以速度应该会有所增加
    return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
  }

  private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
      throws Throwable {
    final Constructor constructor = MethodHandles.Lookup.class
        .getDeclaredConstructor(Class.class, int.class);
    if (!constructor.isAccessible()) {
      constructor.setAccessible(true);
    }
    final Class declaringClass = method.getDeclaringClass();
    return constructor
        .newInstance(declaringClass,
            MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
                | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
        .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
  }

  /**
   * Backport of java.lang.reflect.Method#isDefault()
   */
  private boolean isDefaultMethod(Method method) {
    return (method.getModifiers()
        & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC
        && method.getDeclaringClass().isInterface();
  }
}
invoke方法内部执行过程:

1)过滤掉接口默认方法及Object中通用的方法,真正执行的是MapperMethod.execute方法。

2)MapperMethod属性有方法签名、sql命令相关信息。

3)在实例化MaperMethod时,通过dao的完全限定名+方法名找到对应的MappedStatement实例,将MappedStatement的id、SqlCommandType赋值给SqlCommond的 name、type。

4)当执行查询时,通过SqlCommond的name 找到MappedStatement,从而找到对应的sql语句、入参类型、返回类型等等,然后填充参数,由SqlSession将填充好的sql语句提交到数据库,数据返回后,通过反射装配数据,返回给用户。

看一下MapperMethod的execute方法

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    //可以看到执行时就是4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法
    // SqlCommandType是通过MappedStatement里存储的sql信息确定的
    if (SqlCommandType.INSERT == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
      if (method.returnsVoid() && method.hasResultHandler()) {
        //如果有结果处理器
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        //如果结果有多条记录
        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;
  }
重新梳理一下过程:

一、在MyBatis初始化时,加载并解析配置文件,将dao信息与sql配置文件进行绑定映射及其它配置信息的解析。
Configuration实例属性mapperRegistry,使用dao的Class对象作为key,存储映射器代理工厂实例。
Configuration实例属性mappedStatements使用dao接口的完全限定名+方法名作为key,存储MappedStatement实例。

二、在执行查询操作时,通过dao的Class对象作为key,取出代理工厂实例,由代理工厂生产一个dao接口的代理类实例,在代理类实例invoke方法里面,通过dao接口的完全限定名+方法名,取出MappedStatement实例,进行参数映射后发送到数据库,数据库返回结果后,根据MappedStatement存储的结果集映射策略,进行结果集的装配并返回给用户。

六、拓展

如果项目存在多数据源,SqlSessionFactory实例存在几个?dao方法是怎么做到区分的?事务又是如何配置的?

七、最后

1、知识点:
在刚刚梳理代码中,涉及到了单例模式、工厂模式、建造者模式,代理模式等设计模式,带来的好处就是代码层级和架构清晰,类职责明确。从实现来看,基本符合高内聚、低耦合。

MyBatis的核心就在于配置文件解析和sql语句映射这一块,也是精髓所在,尤其是利用jdk的代理,达到crud的操作,堪称画龙点睛之笔。

2、个人存疑:

  • 有些地方的代码写的有点摸不着头脑,如下图。
  • 其中的一些类,如:MapperAnnotationBuilder 存在穿插解析注解sql和xml文件sql的逻辑。
  • 为什么不支持dao接口方法重载?
疑惑的代码.png

总得来说,MyBatis源码的功底还是很不错的,对我的启迪与收货也很大。
感谢MyBatis团队。

你可能感兴趣的:(MyBatis核心源码解读)