对于MyBatis底层实现的一点见解

在这里插入图片描述
花了一天的看了一些MyBatis的一些最基本的源码。其实MyBatis在使用起来并没有那么复杂,我使用一般是直接在SpringBoot项目中进行关联,然后对于数据库的四大要素进行配置之后就可以直接使用MyBatis对数据库进行操作。甚至比传统的JDBC需要一步步的创建Connection,Statement,ResultSet还要方便与高效。但是对于底层是如何从xml配置文件中的信息获取到数据库并操作数据库的过程并不是非常清楚。所以我将对MyBatis在获取数据源,获取执行操作语句,返回操作结果这三个步骤的底层实现说一说的自己的一些小看法。

为了了解,我在github上clone下来了MyBatis3的源码:MyBatis3 github网址

获取数据源

众所周知,MyBatis框架对于数据库的信息的配置都存储在MyBatisConfig.xml这个文件中,因此,也可以理解为在MyBatis中是哪个部分对MyBatisConfig.xml这个文件进行解析就是一个获取数据源的过程。
我们从入口文件的sqlSessionFactory进入XMLConfigBuilder类中找到parseConfiguration方法:

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);
      // 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);
    }
  }

这个方法传入的参数XNode root其实就是工程中的MyBatisConfig.xml配置文件,在配置文件中有许多的标签(例如properties,enviroment等等),在这个方法中就对配置文件根据标签的不同进行解析,并且进行不同的处理,而对于获取数据源最重要的environments标签,进行了下面的操作:

environmentsElement(root.evalNode("environments"));

于是我们再进入environmentsElement()方法:

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        if (isSpecifiedEnvironment(id)) {
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));

          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));

          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

我们可以发现,方法中也是根据enviroment标签下的子标签(如dataSource),对配置文件中的内容进行解析。
这里对dataSource标签进行操作的是dataSourceElement()方法:

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      Properties props = context.getChildrenAsProperties();
      DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");
  }

这个方法将dataSouce传入到DataSourceFactory中调用getDataSource方法,至此,就拿到了dataSource标签中对于获取数据源最重要的四个要素(driver,url,username和password)。
配置文件中的各个标签的数据都根据类似的方法进行获取,之后会将这些数据都打到Enviroment这个类中:

 private final String id;
  private final TransactionFactory transactionFactory;
  private final DataSource dataSource;

这是Enviroment这个类中的一些属性,我们可以发现与配置文件中的标签是一样的。其实MyBatis会在java中创建出一个与xml文件相对应的类,对配置文件中的配置信息通过类属性的方式进行存储,方便在java中对这些信息进行操作。最后再调用Enviroment中的getEnviroment()方法从类中获取配置文件中的信息添加到Configuration类中,方便之后通过类属性进行操作。

在这里插入图片描述

获取执行操作语句

在MyBatis中,sql语句写在Mapper.xml文件中,并且MyBatisConfig.xml中关联了Mapper.xml。所以,我们只需找到是什么对配置文件中的mapper标签进行解析,就能知道是如何获取操作语句的了。

MyBatis中关联Mapper有4种方法:package,resource,url,class。其中优先级最高的是package。

解析mapper的过程与上面类似,首先还是在XMLConfigBuilder类中找到parseConfiguration()方法加载整个配置文件:

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);
      // 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);
    }
  }

在其中我们找到加载mapper标签的位置:

mapperElement(root.evalNode("mappers"));

于是再进入到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");
          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) {
            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.");
          }
        }
      }
    }
  }

我们会发现mapperElement()方法会根据我们上面说到的关联mapper的几种方式的优先级顺序,对mapper文件进行解析,然后进入到XMLmapperBuilder类中,并执行parse()方法:

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

可以看出使用了configurationElement()方法对mapper标签下的内容进行了操作:

private void configurationElement(XNode context) {
      try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.isEmpty()) {
          throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
      } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
      }
  }

这其中的buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
根据sql语句的类型(select,insert,update或者delete)对sql语句进行解析,并且调用buildStatementFromContext()方法对sql语句中的内容一一打到parseStatementNode()方法中的一一对应的每一个属性中。并返回一个MappedStatement对象添加到Configuration中,方便之后通过类的属性进行操作。

在这里插入图片描述

执行语句的实现

在MyBatis执行sql语句的时候,会用到MyBatis的执行器。MyBatis的执行器有三种:
Simple Execute(默认),Reuse Execute,Batch Execute。

要能够执行sql语句首先需要的是获得环境和获得sql语句。
从Configuration中获取数据源:

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);
      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.instance().reset();
    }
  }

从Configuration中获取sql语句:

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      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();
    }
  }

接着就是用获得的数据源和sql操作语句调用query()方法,对数据库进行操作,其实query()底层使用的就是java的JDBC操作。
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值