Mybatis 源码执行流程

Mybatis相关文章:

Mybatis 源码执行流程

Mybatis 源码执行流程大致分为下面几步:

  • 1、获取配置文件流;
  • 2、解析XML配置文件,构建 SqlSessionFactory
  • 3、利用SqlSessionFactory创建 SqlSession 会话;
  • 4、利用SqlSession 创建 Mapper接口 的代理对象 MapperProxy
  • 5、然后在执行 Mapper接口的 SQL查询时,转而利用代理对象执行 JDBC的SQL底层操作。

下面的5个小结会对着5个步骤分别分析。

我们这里只是直接利用Mybatis框架进行操作,没有说利用Spring整合Mybatis之后操作数据库,其实Spring整合Mybatis也是对下面几步的封装。

java 复制代码
@Test
public void testMyBatisBuild() throws IOException {
    // 1、获取配置文件流;
    InputStream input = Resources.getResourceAsStream("SqlSessionConfig.xml");
    // 2、解析XML配置文件,构建 `SqlSessionFactory`;
    SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(input);
    // 3、利用`SqlSessionFactory`创建 `SqlSession` 会话;
    SqlSession sqlSession = sessionFactory.openSession();
    // 4、利用`SqlSession` 创建 `Mapper接口` 的代理对象 `MapperProxy`;
    TestMapperDao mapper = sqlSession.getMapper(TestMapperDao.class);
    // 5、然后在执行 `Mapper接口`的 SQL查询时,转而利用代理对象执行 JDBC的SQL底层操作。
    System.out.println(mapper.selectByPrimaryKey(1));
}

一、获取配置文件流

java 复制代码
// 1、获取配置文件流;
InputStream input = Resources.getResourceAsStream("SqlSessionConfig.xml");

这个没什么好说的,就是将配置文件以流的形式读入内存。

二、构建 SqlSessionFactory

java 复制代码
// 2、解析XML配置文件,构建 `SqlSessionFactory`;
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(input);

其实这里.build(input)利用了设计模式的建造者模式构建对象;我们进入 .build(input)方法:

java 复制代码
public class SqlSessionFactoryBuilder {
  public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
  }

  /**
   * 该方法的作用就是:建造一个SqlSessionFactory对象
   * 建造一个SqlSessionFactory对象 需要经历下面三步:
   *    (1)传入配置文件,创建一个 XMLConfigBuilder类准备对配置文件展开解析。
   *    (2)解析配置文件,得到配置文件对应的 Configuration对象。
   *    (3)根据 Configuration对象,获得一个 DefaultSqlSessionFactory。
   * @param inputStream Mybatis配置文件
   * @param environment 环境变量
   * @param properties 属性信息
   * @return
   */
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      //不用说,下面两句是核心代码  XMLConfigBuilder(XML配置文件的 构造者)
      // 1.传入配置文件,创建一个XMLConfigBuilder类
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // 2、解析配置文件,得到配置文件对应的Configuration对象
      // 3、根据Configuration对象,获得一个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.
      }
    }
  }
}

流程分为三步:

  • 1、传入配置文件,创建一个XMLConfigBuilder类;
  • 2、解析配置文件,得到配置文件对应的Configuration对象;
  • 3、利用Configuration对象,获得一个DefaultSqlSessionFactory。

下面将分三小节分别讲解。

2.1 创建XMLConfigBuilder类

java 复制代码
// 1.传入配置文件,创建一个XMLConfigBuilder类
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
java 复制代码
public class XMLConfigBuilder extends BaseBuilder {
  public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    //
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
  }
}
  • 进入:new XPathParser(inputStream, true, props, new XMLMapperEntityResolver())

    • 我们主要是进入到 下面类型的第一个构造方法,然后再看一下 this.document
    • this.document = createDocument(new InputSource(inputStream));这里就是将 配置文件的输入流构建成 原生XML Document对象。
    • 下面同时也列出了 XPathParser类中的一些属性信息;目的是为了对 XML整个解析过程有更清晰的认识。
    java 复制代码
    public class XPathParser {
      // 代表要解析的整个XML文档
      private final Document document;
      // 是否开启验证
      private boolean validation;
      // EntityResolver,通过它可以声明寻找DTD文件的方法,例如通过本地寻找,而不是只能通过网络下载dtd文件
      private EntityResolver entityResolver;
      // MyBatis配置文件中的properties信息
      private Properties variables;
      //  javax.xml.xpath.XPath工具
      private XPath xpath;  //这玩意是解析XML的
    
      public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
        commonConstructor(validation, variables, entityResolver);
        // document : 就是 将xml文件节点,内容解析,构建成Document对象
        this.document = createDocument(new InputSource(inputStream));
      }
    
      private Document createDocument(InputSource inputSource) {
        // important: this must only be called AFTER common constructor 重要提示:这只能在公共构造函数之后调用
        // 也就是说在构造方法中,先调用下面的commonConstructor()方法对XPathParser该类中的属性进行初始化操作之后;
        // 才能调用该方法,原因也很简单,下面会用到该类中的属性信息。
        try {
          // DOM文档创建器的工厂
          DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
          factory.setValidating(validation);
    ​
          factory.setNamespaceAware(false);
          factory.setIgnoringComments(true);
          factory.setIgnoringElementContentWhitespace(false);
          factory.setCoalescing(false);
          factory.setExpandEntityReferences(true);
    ​
          // DOM文档创建器
          DocumentBuilder builder = factory.newDocumentBuilder();
          builder.setEntityResolver(entityResolver);
          builder.setErrorHandler(new ErrorHandler() {
            @Override
            public void error(SAXParseException exception) throws SAXException {
              throw exception;
            }
    ​
            @Override
            public void fatalError(SAXParseException exception) throws SAXException {
              throw exception;
            }
    ​
            @Override
            public void warning(SAXParseException exception) throws SAXException {
            }
          });
          return builder.parse(inputSource);  // 这里就是将xml内容解析成 Document对象
        } catch (Exception e) {
          throw new BuilderException("Error creating document instance.  Cause: " + e, e);
        }
      } 
    }
  • 好,我们继续回到 XMLConfigBuilder 类中 this

    java 复制代码
      private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
        // 初始化父类的构造方法 BaseBuilder,
        // 初始化:configuration(配置类) , typeAliasRegistry(类型别名注册表),typeHandlerRegistry(类型处理器注册表)
        super(new Configuration());
        ErrorContext.instance().resource("SQL Mapper Configuration");
        // 一些属性信息可能是以 配置文件的方式写的,这里的props就是属性信息
        this.configuration.setVariables(props);
        this.parsed = false;
        this.environment = environment;
        this.parser = parser;
      }
    • 这里面比较重要的一行:

      java 复制代码
      // 初始化XMLConfigBuilder父类的构造方法 BaseBuilder,
      // 初始化:configuration(配置类) , typeAliasRegistry(类型别名注册表),typeHandlerRegistry(类型处理器注册表)
      
      // 创建 Configuration对象,这里先初始化Configuration里面的 typeAliasRegistry注册表
      // 然后放入 BaseBuilder构造方法 
      super(new Configuration());

OK ,XMLConfigBuilder对象构建完成。

2.2 构建Configuration对象

parser.parse()这里是 解析XML配置文件,继续初始化 Configuration对象。

parse()还是在 XMLConfigBuilder对象中。

java 复制代码
    public class XMLConfigBuilder extends BaseBuilder {
      /**
       * ★ 解析配置文件的入口方法
       * @return Configuration对象
       */
      public Configuration parse() {
        // 不允许重复解析
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        // 开始解析,根节点是configuration
        //    parser.evalNode("/configuration") 会将 XML节点解析,构建成Node对象,然后再封装成Mybatis定义的XNode对象
        //    然后parseConfiguration(parser.evalNode("/configuration")) 继续解析,并放入到 configuration对象对应的属性中!
        parseConfiguration(parser.evalNode("/configuration"));
        // 这个 configuration属性 可是在BaseBuilder中的!!!
        return configuration;
      }
    }

我们查看:parseConfiguration(parser.evalNode("/configuration"));

下面我们就可以看到,XML文件中各种节点属性信息、值都会被解析,然后放入到 configuration对象中。

java 复制代码
/**
 * 这里是解析配置文件的起始方法,解析的所有信息都放入到了configuration中
 * @param root
 */
private void parseConfiguration(XNode root) {
  try {
    //issue #117 read properties first
    propertiesElement(root.evalNode("properties"));
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    loadCustomLogImpl(settings);
    typeAliasesElement(root.evalNode("typeAliases"));
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);  //所有的 settings 节点中的属性都放在这这里解析了
    // read it after objectFactory and objectWrapperFactory issue #631
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

以解析 mappers节点为例,分析对一个节点是如何解析的:mapperElement(root.evalNode("mappers"));

java 复制代码
/**
 * 对配置文件中的 mappers节点进行解析
 *   ege:
 *    <mappers>
 *       <mapper resource="com.yogurt.example.mapper.TestMapperDao"/>
 *       <package name="com.yogurt.example.mapper"/>
 *    </mappers>
 * @param parent mappers节点内容
 */
private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      //下面对 mappers节点下不同的子节点进行不同的处理
      //1.对package子节点的处理
      if ("package".equals(child.getName())) {
        // 取出包的路径
        String mapperPackage = child.getStringAttribute("name");
        // 全部加入Mappers中
        configuration.addMappers(mapperPackage);
      } else {  //2.对 mapper 字节的处理
        // 下面三个 resource,url,mapperClass只能有一个生效,
        // 就是获取的 mapper节点上的属性,用于获取 xxxMapper.xml的路径,
        // 然后下面的(if/else if/else)就是针对节点属性不同进行不同的处理。
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        if (resource != null && url == null && mapperClass == null) { // resource
          ErrorContext.instance().resource(resource);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          // ★使用XMLMapperBuilder解析Mapper文件解析: ★ 这应该解析的是对应路径中的接口 继续解析(就是对XML的接口映射文件继续解析)
          XMLMapperBuilder mapperParser = 
              new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url != null && mapperClass == null) { // url
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          // ★使用XMLMapperBuilder解析Mapper文件
          XMLMapperBuilder mapperParser = 
              new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url == null && mapperClass != null) {  // 配置的不是Mapper文件,而是Mapper接口
          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.");
        }
      }
    }
  }
}

2.3 获取DefaultSqlSessionFactory对象

build(parser.parse());

java 复制代码
public class SqlSessionFactoryBuilder {
  /**
   * 根据配置信息建造一个SqlSessionFactory对象;
   * 这里可以看到构建的对象是SqlSessionFactory子类实现类: DefaultSqlSessionFactory
   * @param config 配置信息
   * @return SqlSessionFactory对象
   */
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }
}

OK,SqlSessionFactory构建完成。

三、创建 SqlSession 会话

java 复制代码
// 3、利用 SqlSessionFactory 创建 SqlSession 会话;
SqlSession sqlSession = sessionFactory.openSession();
java 复制代码
public class DefaultSqlSessionFactory implements SqlSessionFactory {
    @Override
    public SqlSession openSession() {
        return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
    }

    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
            // 找出要使用的指定环境
            final Environment environment = configuration.getEnvironment();
            // 从环境中获取事务工厂
            final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
            // 从事务工厂中生产事务
            tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
            // 创建执行器
            final Executor executor = configuration.newExecutor(tx, execType);
            // 创建DefaultSqlSession对象
            return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
            closeTransaction(tx); // may have fetched a connection so lets call close()
            throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
            // ErrorContext 类是一个错误上下文,它能够提前将一些背景信息保存下来。
            // 这样在真正发生错误时,便能将这些背景信息提供出来,进而给我们的错误排查带来便利。
            //
            // 这里整个 初始化Session完成,就将该上下文信息重置(清理)
            ErrorContext.instance().reset();
        }
    }
}

四、创建代理对象 MapperProxy

java 复制代码
// 4、利用`SqlSession` 创建 `Mapper接口` 的代理对象 `MapperProxy`;
TestMapperDao mapper = sqlSession.getMapper(TestMapperDao.class);

我们可以看到: sqlSession 类型是 :DefaultSqlSession

往里跟:sqlSession.getMapper(TestMapperDao.class);

java 复制代码
/**
 * SqlSession的默认实现。请注意,此类不是线程安全的
 *
 * 可以看到该类提供了 查询、增加、更新、删除、提交、回滚等方法。
 * 其实这就就是提供给用户,对外的。
 * 执行工作不在这里执行,而是交给了executor包
 *
 * session包是整个 MyBatis应用的对外接口包,而 executor包是最为核心的执行器包。
 * DefaultSqlSession 类做的主要工作则非常简单------把接口包的工作交给执行器包处理。
 */
public class DefaultSqlSession implements SqlSession {
    // 配置信息
    private final Configuration configuration;
    // 执行器 (下面的操作语句都交由执行去执行)
    private final Executor executor;

    // 是否自动提交
    private final boolean autoCommit;
    // 缓存是否已经污染
    private boolean dirty;
    // 游标列表
    private List<Cursor<?>> cursorList;

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

继续往下跟:configuration.getMapper(type, this);

java 复制代码
public Configuration{
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        // 在 mapper的注册表中 找到指定映射接口的映射文件,并根据映射文件信息为该映射接口 生成一个代理实现
            return mapperRegistry.getMapper(type, sqlSession);
    }
}

跟:

java 复制代码
/**
 * 用于解决:抽象方法与数据库操作节点之间的关联,就是通过该MapperRegistry里面的 knownMappers属性
 *    1.因为 MapperMethod 解决了 如果通过一个方法去执行SQL(就是将一个SQL转为为一个方法)
 *    2.因为 MapperProxy、MapperProxyFactory 通过动态代理解决了
 *          映射接口(ege:UserMapper.java)的方法去执行(数据库操作的方法);
 *          (因为 UserMapper.java本身是一个接口,没有具体的实现类,如果找到SQL数据库操作,就是通过MapperProxyFactory、MapperProxy)
 *
 */
public class MapperRegistry {

  private final Configuration config;

  // knownMappers维护了 映射接口与MapperProxyFactory关联起来
  // key: 映射接口 (UserMapper.class)
  // value: 对应的 MapperProxyFactory对象
  //              而这个MapperProxyFactory 作为代理工厂,里面有封装了 MapperProxy,这个MapperProxy,一个对一个,对应MapperMethod;
  //               同时MapperProxyFactory工厂里面还有一个 methodCache(这是一个map)维护了当前 代理接口的所有方法(MapperMethod)
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

  /**
   * 找到指定映射接口的映射文件,并根据映射文件信息为该映射接口 生成一个代理实现
   * @param type 映射接口
   * @param sqlSession sqlSession
   * @param <T> 映射接口类型
   * @return 代理实现对象
   */
  /**
   * TestMapperDao mapper = sqlSession.getMapper(TestMapperDao.class);
   * 这里sqlSession.getMapper(***.class)最终也会来到这里
   */
  @SuppressWarnings("unchecked")
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 1、找出指定映射接口的代理工厂
    // ★ 这里我们主要看属性: knownMappers的 (key,value)值
    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返回一个对应映射接口的代理器实例
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
}
  • 1、knownMappers.get(type); 我们代码跟到这里看一下:

    可以看到,每一个 Mapper接口都有与之对应的代理工厂。

  • 2、利用对应的代理工厂创建代理对象:mapperProxyFactory.newInstance(sqlSession);

    跟着代码可以来到 MapperProxyFactory类,来到代理创建的位置。

    java 复制代码
    public class MapperProxyFactory<T> {
      // mapperInterface:
      //    对应SQL的Java映射接口(ege: UserMapper.java)
      // methodCache:
      //     该map缓存的是映射接口的方法,但是这里没有put操作,
      //     也就说,这里一个mapperInterface,一个methodCache,而且methodCache里面只对应mapperInterface里面的一个方法,没有多个。
      private final Class<T> mapperInterface;
      private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
    
      public T newInstance(SqlSession sqlSession) {
        //这里就是为 映射接口创建代理对象
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
      }
    
     /**
      * 这里返回的应该是一个 代理(映射接口)对象
      */
      @SuppressWarnings("unchecked")
      protected T newInstance(MapperProxy<T> mapperProxy) {
        // 三个参数分别是:
        // 创建代理对象的类加载器、要代理的接口(映射接口)、代理类的处理器(映射接口中的方法所绑定的SQL)。
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
      }
    }

至此,代理对象创建完成,代理对象的类型为 :MapperProxy,我们可以返回 main 方法中查看,可以看到此时的 TestMapperDao已经是代理类型了,那么,下一步,执行mapper.selectByPrimaryKey(1) 就是由代理对象执行了。

五、代理对象执行目标方法

java 复制代码
// 5、然后在执行 `Mapper接口`的 SQL查询时,转而利用代理对象执行 JDBC的SQL底层操作。
System.out.println(mapper.selectByPrimaryKey(1));
java 复制代码
/**
 *    首先是MapperMethod已经完成方法和SQL语句之间绑定,将数据库操作(SQL
 * 语句)转化(这里的转化有反转之意)为MapperMethod中的execute()方法。
 *
 *    该类基于动态代理将针对 映射接口的方法(mapper.java中的方法)调用转接成了
 * 对MapperMethod对象execute方法的调用,进而实现了数据库操作。
 *    该类实现了InvocationHandler,这意味着当使用它的实例替代被代理对象后,
 * 对被代理对象的方法调用会被转接到MapperProxy中invoke方法上 (√ 没毛病)
 */
public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  // mapperInterface:映射接口,methodCache:映射接口中的方法所对应的SQL方法
  private final Class<T> mapperInterface;
  // 该Map的键为映射接口中的方法,值为MapperMethod对象。
  // 通过该属性,完成了MapperProxy内(即映射接口内)方法和MapperMethod的绑定
  // 也就是一个 抽象方法(ege:UserMapper.java中的一个抽象方法)对应一个MapperMethod对象
  // 这个map作为:映射接口方法和MapperMethod,第一次调用会put进入,再次调用的话就直接在该map中寻找了
  private final Map<Method, MapperMethod> methodCache;

  /**
   * 这里是 代理方法
   * @param proxy
   * @param method 被代理的方法
   * @param args 被代理方法的参数
   * @return 方法执行结果
   * @throws Throwable
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) { // 继承自Object的方法
        // 直接执行原有方法
        return method.invoke(this, args);
      } else if (method.isDefault()) {
        // 执行默认方法
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    // 1、找到对应的MapperMethod对象
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // 调用MapperMethod中的execute方法
    // 2、就是用于执行数据库操作的方法!
    return mapperMethod.execute(sqlSession, args);
  }
}

5.1 找到对应的MapperMethod对象

5.2 用于执行数据库操作的方法

java 复制代码
mapperMethod.execute(sqlSession, args);
java 复制代码
/**
 * 每个 MapperMethod对象都对应了一个数据库操作节点(即一次SQL操作,换句话说,一个接口映射方法就对应一个MapperMethod),
 * 调用 MapperMethod实例中的 execute方法就可以触发节点中的 SQL语句。
 *
 * MapperMethod类将一个数据库操作语句和一个Java方法绑定在一起:
 *    MethodSignature属性保存了这个方法的详细信息;SqlCommand属性持有这个方法对应的SQL语句。
 *    所以,对接方法的操作:MethodSignature,对接SQL的操作在SqlCommand;
 *    所以,只要调用 MapperMethod对象的 execute方法,就可以触发具体的数据库操作,★于是数据库操作就被转化为了方法。
 */
public class MapperMethod {

  // SqlCommand、MethodSignature这两个是该类(MapperMethod)中的两个内部类
  // SqlCommand内部类指代一条SQL语句
  // ege:
  //    {
  //       "name":"org.apache.ibatis.binding.BoundBlogMapper.selectBlogUsingConstructorWithResultMap",
  //       "type":"SELECT"
  //    }
  private final SqlCommand command;
  // MethodSignature 内部类的属性详细描述了一个方法的细节。
  private final MethodSignature method;

  /**
   * 每个MapperMethod对象都对应了一个数据库操作节点,调用MapperMethod实例中的execute方法就可以触发节点中的SQL语句。
   * 这个方法会被执行:
   *    可以看到,这里里面由于 增、删、改、查
   * @param sqlSession
   * @param args
   * @return
   */
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {  //增
        // 将参数顺序与实参对应好
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: { //更新
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: { //删
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT: //查
        if (method.returnsVoid() && method.hasResultHandler()) { // 方法返回值为void,且有结果处理器
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {  //多条结果查询
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {  //Map结果查询
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {  //游标类型结果查询
          result = executeForCursor(sqlSession, args);
        } else { //单条结果查询
          Object param = method.convertArgsToSqlCommandParam(args);
          // ★单条数据结果查询,就跟着这个方法往下走就行了。
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH: //清缓存
        result = sqlSession.flushStatements();
        break;
      default: //未知
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      // 查询结果为null,但返回类型为基本类型。因此返回变量无法接收查询结果,抛出异常。
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }
}

我们这里是单条数据结果查询:就是其为例。

java 复制代码
result = sqlSession.selectOne(command.getName(), param);

sqlSession类型是 DefaultSqlSession

java 复制代码
public class DefaultSqlSession implements SqlSession {
  @Override
  public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    // 接着跟这一行
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(),
                                        but found: " + list.size());
    } else {
      return null;
    }
  }

  @Override
  public <E> List<E> selectList(String statement, Object parameter) {
    // 接着跟这一行,走到最终下面的方法
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }

  /**
   * 查询结果列表
   * @param <E> 返回的列表元素的类型
   * @param statement SQL语句
   * @param parameter 参数对象
   * @param rowBounds  翻页限制条件
   * @return 结果对象列表
   */
  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      // 每一个MappedStatement对象对应了我们设置的一个数据库操作节点,它主要定义了数据库操作语句、输入/输出参数等信息
      // configuration.getMappedStatement(statement) : 将 要执行的MappedStatement对象从Configuration对象存储的映射文件信息中找出来
      //
      // 获取查询语句
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 交由执行器进行查询
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }                                    
}

5.2.1 交由执行器,执行SQL操作

java 复制代码
// 交由执行器进行查询
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

executor类型为 CachingExecutor,我们往下跟:

java 复制代码
public class CachingExecutor implements Executor {
  // 被装饰的执行器
  private final Executor delegate;
  // 事务缓存管理器
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, 
                           RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //1. BoundSql是经过层层转化后去除掉 if、where等标签的 SQL语句
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    //2. CacheKey是为该次查询操作计算出来的缓存键
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    //3. 执行查询操作
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
}
  • 1、boundSql值:

  • 2、key值

    CacheKey是为该次查询操作计算出来的缓存键

  • 3、查询数据库中的数据

这里涉及到:

  • 先去 二级缓存 查找数据,如果有,返回

    • 二级缓存没有,去一级缓存查找,一级缓存有,返回

      • 一级缓存没有,执行数据查询操作。

如果我们设置了缓存的话, 那么我们就要在执行数据库操作之后,将其放入到缓存中。

5.2.1.1 二级缓存 查找数据
java 复制代码
/**
 * 查询数据库中的数据
 * @param ms 映射语句
 * @param parameterObject 参数对象
 * @param rowBounds 翻页限制条件
 * @param resultHandler 结果处理器
 * @param key 缓存的键
 * @param boundSql 查询语句
 * @param <E> 结果类型
 * @return 结果列表
 * @throws SQLException
 */
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject,
                         RowBounds rowBounds, ResultHandler resultHandler,
                         CacheKey key, BoundSql boundSql) throws SQLException {

    // 获取MappedStatement对应的缓存,可能的结果有:该命名空间的缓存、共享的其它命名空间的缓存、无缓存
    Cache cache = ms.getCache();
    // 如果映射文件未设置<cache>或<cache-ref>则,此处cache变量为null
    if (cache != null) {  // 存在缓存
        // 根据要求判断语句执行前是否要清除二级缓存,如果需要,清除二级缓存
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) { // 该语句使用缓存且没有输出结果处理器
            // 二级缓存不支持含有输出参数的CALLABLE语句,故在这里进行判断
            ensureNoOutParams(ms, boundSql);
            @SuppressWarnings("unchecked")
            // 从缓存中读取结果
            List<E> list = (List<E>) tcm.getObject(cache, key);
            // 说明缓存中没有相应数据
            if (list == null) {
                // 交给被包装的执行器执行
                // 首先: 当前类CachingExecutor本身是二级缓存,然后 来到这里说明在二级缓存中没有找到数据,
                //  这里应该就会调用做为一级缓存的BaseExecutor查询数据库。
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // 缓存 被包装执行器返回的结果
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            return list;
        }
    }
    // 交由 被包装的实际执行器 执行
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
5.2.1.2 一级缓存 查找数据
  • 二级缓存没有的话,执行 delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

    去一级缓存查找数据;

    java 复制代码
    这里是到了 执行的基类:`org.apache.ibatis.executor.BaseExecutor`
    
    /**
     * 查询数据库中的数据;
     * 该方法大致的流程是:会尝试读取一级缓存,而在缓存中无结果时,则会调用queryFromDatabase方法进行数据库中结果的查询
     * @param ms 映射语句
     * @param parameter 参数对象
     * @param rowBounds 翻页限制条件
     * @param resultHandler 结果处理器
     * @param key 缓存的键
     * @param boundSql 查询语句
     * @param <E> 结果类型
     * @return 结果列表
     * @throws SQLException
     */
    @SuppressWarnings("unchecked")
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    
        // 执行器已经关闭
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        // 新的查询栈且要求清除缓存
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            clearLocalCache(); // 清除一级缓存
        }
        List<E> list;
        try {
            queryStack++;
            // 尝试从本地缓存获取结果
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            if (list != null) {
                // 本地缓存中有结果,则对于CALLABLE语句还需要绑定到IN/INOUT参数上
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                // ★本地缓存没有结果,故需要查询数据库
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            queryStack--;
        }
        if (queryStack == 0) {
            // 懒加载操作的处理
            for (DeferredLoad deferredLoad : deferredLoads) {
                deferredLoad.load();
            }
            // issue #601
            deferredLoads.clear();
            // 如果本地缓存的作用域为STATEMENT,则立刻清除本地缓存
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }
5.2.1.3 数据库查找数据
java 复制代码
// 本地缓存没有结果,故需要查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

/**
 * 从数据库中查询结果
 * @param ms 映射语句
 * @param parameter 参数对象
 * @param rowBounds 翻页限制条件
 * @param resultHandler 结果处理器
 * @param key 缓存的键
 * @param boundSql 查询语句
 * @param <E> 结果类型
 * @return 结果列表
 * @throws SQLException
 */
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
                                      ResultHandler resultHandler,
                                      CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // MyBatis先在缓存中放置一个占位符,然后调用doQuery方法实际执行查询操作。
    // 最后,又把缓存中的占位符替换成真正的查询结果(list)。
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // ★★这里相当于一个模板方法, 模板定义在这里,具体实现交由子类实现。★★
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        // 删除占位符
        localCache.removeObject(key);
    }
    // 将查询结果写入缓存
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}

  /**
   * 该方法中,不论下面的那个实现类实现的次方法,都有这样几步:
   * 1. 生成Statement对象stmt;Statement类并不是MyBatis中的类,而是JDK java.sql包中的类,Statement类能够执行静态SQL语句并返回结果。
   * 2. 通过Configuration的newStatementHandler方法获得了一个StatementHandler对象handler,
   *      然后将查询操作交给StatementHandler对象进行,StatementHandler是一个语句处理器类,其中封装了很多语句操作方法。
   * 3. handler.query(stmt, resultHandler);
   *      会进入到 StatementHandler接口下面的某一个实现类中(这里进入PreparedStatementHandler)
   *      里面的 ps.execute(), ps类型是 PreparedStatement(这个已经是 com.mysql.cj.jdbc包中的类负责)的了
   */
  protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, 
                                         RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
                                                                                    throws SQLException;

我们这里单个查询的实现类是:org.apache.ibatis.executor.SimpleExecutor#doQuery

java 复制代码
public class SimpleExecutor extends BaseExecutor {
  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, 
                                                    RowBounds rowBounds, ResultHandler resultHandler, 
                                                    BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      // SQL语句的处理方式:
      //    handler有下面4种类型:
      //  		CallableStatementHandler,PreparedStatementHandler,
      //  		SimpleStatementHandler,RoutingStatementHandler
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
}

这里是:PreparedStatementHandler类型

java 复制代码
public class PreparedStatementHandler extends BaseStatementHandler {
  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    //这里是 com.mysql.cj.jdbc包中的类负责;
    //执行的结果,我们可以在 ps对象中找到: (这里是debug模式,查看ps对象中的值)
    //  ->h
    //    ->statement
    //      ->results
    //        ->(catalog:数据库名;
    //           connection:连接对象,里面有数据库明,地址,端口,账号,密码等信息;
    //           rowData:rows记录了该次数据库查询的结果信息)
    ps.execute();
    //这里最终数据库查询得到的结果交给 ResultHandler对象处理
    return resultSetHandler.handleResultSets(ps);
  }
}

OK,到这里就要调用 JDBC的PreparedStatement执行SQL(数据库操作)了。

然后再层层返回,执行流程走完。

完结撒花。

六、目标方法的注册

这里单独介绍一下 Mapper接口如何和代理类绑定起来的;以及如何注册到 Configuration中的,因为只有这些Mapper接口信息以及接口中的方法信息注册了,我们才能在会话中用。

其实就是在 2.2节中解析mapper节点的一个步骤:

java 复制代码
// ★使用XMLMapperBuilder解析Mapper文件解析: ★ 这应该解析的是对应路径中的接口 继续解析(就是对XML的接口映射文件继续解析)
XMLMapperBuilder mapperParser = 
    new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();

XMLMapperBuilder就是 Mapper文件方式的解析类。

我们进入该类的 parse() 方法:

java 复制代码
/**
 * 解析映射文件
 */
public void parse() {
    //该节点是否被解析过
    if (!configuration.isResourceLoaded(resource)) {
        // 处理mapper节点
        configurationElement(parser.evalNode("/mapper"));
        // 加入到已解析列表
        configuration.addLoadedResource(resource);
        // 将mapper注册给 configuration
        bindMapperForNamespace();
    }

    // 下面分别用来处理暂时性失败的<resultMap>、<cache-ref>、SQL语句
    // ege:
    //    <resultMap id="girlUserMap" type="Girl" extends="userMap">
    //        <result property="email" column="email"/>
    //    </resultMap>
    //    <resultMap id="userMap" type="User" autoMapping="false">
    //        <id property="id" column="id" javaType="Integer"/>
    //        <result property="name" column="name"/>
    //    </resultMap>
    // 由于映射文件(xxxMapper.xml)节点的解析顺序是由上往上的;
    // 当先解析girlUserMap的时候,extends="userMap"引用的是id="userMap";
    // 可是,id="userMap"的resultMap(由于在下面)还未被读入,此时就会出现暂时性的错误。
    // 这里就是处理这种场景的!!!
    // (由于已经在第一遍解析时读入了所有节点,因此第二遍解析的时候可以解决这种依赖。)
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

关于 mapper解析过程中的细枝末节,这里不做关注,想看的话,可以到博主源码工程中去看。

这里直接看 bindMapperForNamespace(); 注册 对应Mapper的代理工厂

java 复制代码
private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
        Class<?> boundType = null;
        try {
            boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {
            //ignore, bound type is not required
        }
        if (boundType != null) {
            if (!configuration.hasMapper(boundType)) {
                // Spring may not know the real resource name so we set a flag
                // to prevent loading again this resource from the mapper interface
                // look at MapperAnnotationBuilder#loadXmlResource
                configuration.addLoadedResource("namespace:" + namespace);
                // 这里注册!!!
                configuration.addMapper(boundType);
            }
        }
    }
}

跟:configuration.addMapper(boundType);

这里,我们要注意,这里是 Configuration类中的 方法,并且这里是 mapperRegistry属性中添加 mapper 的代理工厂。

好的, 找到了,与第四节创建代理对象时,先从mapperRegistry注册表中获取对应mapper的代理工厂构成对应关系了。

java 复制代码
public class Configuration {
    // 这里是注册 mapper的代理工厂的。
    public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
    }

    // 第四节时候用的这个方法获取的!!!
    // 与第四节创建代理对象时,先从`mapperRegistry`注册表中获取对应mapper的代理工厂构成对应关系了。
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }  
}

跟:mapperRegistry.addMapper(type);

注意看里面的 knownMappers属性。

java 复制代码
/**
 * 用于解决:抽象方法与数据库操作节点之间的关联,就是通过该MapperRegistry里面的 knownMappers属性
 *    1.因为 MapperMethod 解决了 如何通过一个方法去执行SQL(就是将一个SQL转为为一个方法)
 *    2.因为 MapperProxy、MapperProxyFactory 通过动态代理解决了
 *          映射接口(ege:UserMapper.java)的方法去执行(数据库操作的方法);
 *          (因为 UserMapper.java本身是一个接口,没有具体的实现类,如何找到SQL数据库操作,就是通过MapperProxyFactory、MapperProxy)
 *
 */
public class MapperRegistry {

    // knownMappers维护了 映射接口与MapperProxyFactory关联起来
    // key: 映射接口 (UserMapper.class)
    // value: 对应的 MapperProxyFactory对象
    //        而这个MapperProxyFactory 作为代理工厂,里面又封装了 MapperProxy,这个MapperProxy一个对一个,对应MapperMethod;
    //        同时MapperProxyFactory工厂里面还有一个methodCache(这是一个map)维护了当前 代理接口的所有方法(MapperMethod)
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

    /**
     * 该方法就是为映射接口 创建代理工厂并将该工厂加入 knownMappers中
     * 然后后面在 上面的getMapper()方法中使用该工厂(是这样使用的:在getMapper()方法中①+②(②就是通过代理工厂创建代理对象))
     */
    public <T> void addMapper(Class<T> type) {
        // 只有是接口的时候才可以添加
        if (type.isInterface()) {
            //判断是否已经注册过了
            if (hasMapper(type)) {
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                //★添加 Mapper对应的代理的工厂
                knownMappers.put(type, new MapperProxyFactory<>(type));
                // It's important that the type is added before the parser is run
                // otherwise the binding may automatically be attempted by the
                // mapper parser. If the type is already known, it won't try.
                // 必须在运行解析器之前添加类型,否则映射器解析器可能会自动尝试绑定。如果类型已知,则不会尝试。
                // 这里应该是注解方式的处理 : 是的没错
                //    这里是构造一个 MapperAnnotationBuilder,
                //    如果 映射接口中有注解方式的SQL语句:@Select/@SelectProvider...删改查;就会用该类中的parse()方法进行解析
                //
                // ★不,不,解析的不止是注解方式的,还有配置文件方式,parse()会有判断步骤分开处理。
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }
}

这里,可以看到注册了 Mapper接口对应的代理工厂。

java 复制代码
//★添加 Mapper对应的代理的工厂
knownMappers.put(type, new MapperProxyFactory<>(type));

其实这里就完结了!!!

相关推荐
2202_7544215416 分钟前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
蓝染-惣右介19 分钟前
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
java·数据库·tomcat·mybatis
小林想被监督学习20 分钟前
idea怎么打开两个窗口,运行两个项目
java·ide·intellij-idea
HoneyMoose22 分钟前
IDEA 2024.3 版本更新主要功能介绍
java·ide·intellij-idea
我只会发热23 分钟前
Java SE 与 Java EE:基础与进阶的探索之旅
java·开发语言·java-ee
是老余24 分钟前
本地可运行,jar包运行错误【解决实例】:通过IDEA的maven package打包多模块项目
java·maven·intellij-idea·jar
crazy_wsp25 分钟前
IDEA怎么定位java类所用maven依赖版本及引用位置
java·maven·intellij-idea
.Ayang27 分钟前
tomcat 后台部署 war 包 getshell
java·计算机网络·安全·web安全·网络安全·tomcat·网络攻击模型
一直学习永不止步33 分钟前
LeetCode题练习与总结:最长回文串--409
java·数据结构·算法·leetcode·字符串·贪心·哈希表
hummhumm1 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j