资讯专栏INFORMATION COLUMN

MyBatis 源码解析(一):初始化和动态代理

娣辩孩 / 3089人阅读

摘要:最终解析出的和依然是设置到中。到这里,初始化部分就结束了。总结的初始化流程主要是解析配置文件,将相关信息保存在中,同时对每个代表的生成代理对象工厂。

简介

MyBatis 是 Java 开发中非常流行的 ORM 框架,其封装了 JDBC 并且解决了 Java 对象与输入参数和结果集的映射,同时又能够让用户方便地手写 SQL 语句。MyBatis 的行为类似于以下几行代码:

Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, usr, password);
PraparedStatement st = conn.prepareStatement(sql);
st.setInt(0, 1);
st.execute();
ResultSet rs = st.getResultSet();
while (rs.next()) {
    String result = rs.getString(colname);
}

上面是 JDBC 的使用流程,MyBatis 其实就是对上面的代码进行分解包装。本文将对 MyBatis 的代码进行分析,探究其中的逻辑。

基本用法

首先从 MyBatis 的基本用法开始,下面是 MyBatis 官网的入门示例:

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

其中 mabatis-config.xml 是 MyBatis 的核心配置文件,其中包括数据源、事务管理器、别名以及 SQL 对应的 Mapper 文件等,如下所示:




  
    
      
      
        
        
        
        
      
    
  
  
    
  

有了 SqlSessionFactory 后就可以创建 SqlSession 来调用 select 以及 update 等方法请求数据了:

try {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  Blog blog = mapper.selectBlog(101);
} finally {
  session.close();
}
配置文件解析

我们按照上面的代码流程开始分析源码,首先是配置文件的解析:SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream) SqlSessionFactoryBuilder 显然是为了构建 SqlSessionFactory,而且是从配置文件的输入流构建,代码如下:

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // 创建 XMLConfigBuilder
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // parse.parse() 进行解析
      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.
      }
    }
  }

首先是创建了一个 XMLConfigBuilder 对象,它是用来解析 Config 文件的。XMLConfigBuilder 继承自 BaseBuilderBaseBuilder 中有个 Configuration 类型的变量,这个类需要重点关注,Config 文件中解析出来的所有信息都保存在这个变量中。

创建了 XMLConfigBuilder 后调用了其 parse 方法:

  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // 在这个函数中解析
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

这里主要逻辑在 parseConfiguration 中:

private void parseConfiguration(XNode root) {
    try {
      // 解析 properties
      propertiesElement(root.evalNode("properties")); //issue #117 read properties first
      // 解析 type alias
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));      
      // 解析 setting
      settingsElement(root.evalNode("settings"));
      // 解析 environment 
      environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析 mapper
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

这里解析了 config 文件中所有的标签,包括 propertiessettings 以及 mappers 等,下面挑几个看一下。

settings

settings 是对 MyBatis 的一些配置项,包括缓存的开启以及是否使用驼峰转换(mapUnderscoreToCamelCase)等,代码如下:

private void settingsElement(XNode context) throws Exception {
   if (context != null) {
   // 将配置项保存到 Properties 中
     Properties props = context.getChildrenAsProperties();
     // Check that all settings are known to the configuration class
     MetaClass metaConfig = MetaClass.forClass(Configuration.class);
     for (Object key : props.keySet()) {
       if (!metaConfig.hasSetter(String.valueOf(key))) {
         throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
       }
     }
     configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
     // 默认开启缓存
     configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
     configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));      
     configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
     configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), true));
     configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
     configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
     configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
     configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
     configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
     configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
     configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
     configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
     configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
     configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
     configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
     configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
     configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
     configuration.setLogPrefix(props.getProperty("logPrefix"));
     configuration.setLogImpl(resolveClass(props.getProperty("logImpl")));
   }
 }

可以看出,settings 的子节点保存在 Properties 中,然后校验是否有不合法的子节点,最后提取出其中的属性保存到 Configuration 中,上面提到这个类专门用于保存 Config 文件解析出的信息。

从上面也可以看到 MyBatis 的一些默认属性,例如一级缓存如果没有配置,那么默认是开启的。

environments

environments 包含了数据源(dataSource) 和事务管理器(transactionManager) 的配置,代码如下:

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)) {
          // 解析 transactionManager
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          // 解析 dataSource
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          // 设置 environment 到 configuration
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

private TransactionFactory transactionManagerElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      Properties props = context.getChildrenAsProperties();
      // 通过反射实例化
      TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a TransactionFactory.");
  }

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

其中主要是两部分,第一部分解析 transactionManager,第二部分解析 dataSource。从 transactionManagerElementdataSourceElement 中可以看出通过对应 Class 文件的 newInstance 实例化出对应的工厂对象。最终解析出的 transactionManagerdataSource 依然是设置到 Configuration 中。

mappers

mappers 对应了具体的 SQL Mapper 文件,也是我们要分析的重点。

mappers 标签可以多种子标签,上面的示例中是 mapper 配合 resource

 
    
  

我们下面看一下此种形式在源码中的解析:

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");
          // 这个分支解析 resource 形式的标签
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 创建 XMLMapperBuilder 
            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.");
          }
        }
      }
    }
  }

resource 标签解析的对应分支是 (resource != null && url == null && mapperClass == null),其中创建了一个 XMLMapperBuilder 对象然后调用 parse 方法进行解析:

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      // 解析 mapper 下面的标签,包括 namespace、cache、parameterMap、resultMap 等
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingChacheRefs();
    parsePendingStatements();
  }

 private void configurationElement(XNode context) {
    try {
      // namespace 对应 Mapper 对应接口的全名(包名 + 类名)
      String namespace = context.getStringAttribute("namespace");
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      // 解析生成 ParameterMap
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 解析生成 ResultMap
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      // 每一个 sql 语句生成一个 MappedStatement
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new RuntimeException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }

configurationElement 用于解析具体的子标签,如 namespacecacheparameterMapresultMap 以及 select|insert|update|delete 等。

namespace 对应了 Mapper 接口类的包名 + 类名,通过 namespace 可以唯一定位一个 Class 文件,解析的 namespace 保存在 builderAssistant 中,后面会用到。

parameterMapresultMap 解析会生成 ParameterMapResultMap 对象。每个 SQL 语句解析会生成 MappedStatement

在上面的 parse 方法中,解析完标签后调用了 bindMapperForNamespace,这个实现了加载 namespace 对应的 Class,并且为每个 Class 创建了代理类工厂对象(MapperProxyFactory)。

MapperProxyFactory

MapperProxyFactory 用于为 Mapper 接口类创建代理对象,代理对象指的是
BlogMapper mapper = session.getMapper(BlogMapper.class) 生成的对象。

下面从 bindMapperForNamespace 开始:

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);
          // 添加 mapper 和 MapperProxyFactory
          configuration.addMapper(boundType);
        }
      }
    }
  }

其中先从 builderAssistant 取出 namespace,然后加载对应的 Class(boundType = Resources.classForName(namespace))。最后调用 configuration.addMapper(boundType) 添加到 configuration 中。 configuration.addMapper(boundType) 很关键,看代码:

public  void addMapper(Class type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // 添加到  Map, MapperProxyFactory> 中
        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 parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

关键的一行是 knownMappers.put(type, new MapperProxyFactory(type)),其中 knownMappers 的类型是 Map, MapperProxyFactory>,即 key 是 Class,value 是 MapperProxyFactory。这里的 MapperProxyFactory 即是动态代理对象的工厂,下面是其 newInstance 方法的代码:

  protected T newInstance(MapperProxy mapperProxy) {
    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);
  }

从中可以看出,这里用的是 Java 的动态代理,Proxy.newProxyInstance 方法生成指定接口的代理对象,这个方法的第三个参数是用于方法拦截的对象,这里是 MapperProxy 的实例。

由此可以知道,具体的执行 SQL 语句的操作是由这个类拦截并且执行的,看看这个类的 invoke 方法:

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

如果是 Object 类中声明的方法,则直接执行,否则调用 MapperMethodexecute,其中便是 JDBC 相关的逻辑了。限于篇幅,具体内容留到下一篇文章再看。

在分析完配置文件的解析后,再回到 XMLConfigBuilder 中:

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // 构建 SqlSessionFactory
      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.
      }
    }
  }

parser.parse() 执行完后,生成一个 Configuration 对象,最后调用 build 构建 SqlSessionFactory,代码如下:

  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

可以看到,最终创建的是 DefaultSqlSessionFactory,这个类内部持有 Configuration,并且提供了多个重载的 openSession 方法用于创建 SqlSession

到这里,初始化部分就结束了。

总结

MyBatis 的初始化流程主要是解析配置文件,将相关信息保存在 Configuration 中,同时对每个 namespace 代表的 Class 生成代理对象工厂。最后,利用 Configuration 生成了一个 DefaultSqlSessionFactory,通过这个对象可以创建 SqlSession 执行 SQL 请求,相关内容将在下一篇(MyBatis 源码解析(二):SqlSession 执行流程)分析。

如果我的文章对您有帮助,不妨点个赞支持一下(^_^)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72677.html

相关文章

  • Mybatis源码分析

    摘要:我认为学习框架源码分为两步抓住主线,掌握框架的原理和流程理解了处理思路之后,再去理解面向对象思想和设计模式的用法目前第一步尚有问题,需要多走几遍源码,加深下理解,一起加油 这篇文章我们来深入阅读下Mybatis的源码,希望以后可以对底层框架不那么畏惧,学习框架设计中好的思想; 架构原理 架构图 showImg(https://segmentfault.com/img/remote/...

    lindroid 评论0 收藏0
  • MyBatis 源码解析(二):SqlSession 执行流程

    摘要:简介上一篇文章源码解析一初始化和动态代理分析了解析配置文件以及动态代理相关的源码,这一篇接着上一篇探究的执行流程,另外了解一下中的缓存。总结本文主要分析了的执行流程,结合上一篇文章基本了解了的运行原理。 简介 上一篇文章(MyBatis 源码解析(一):初始化和动态代理)分析了 MyBatis 解析配置文件以及 Mapper 动态代理相关的源码,这一篇接着上一篇探究 SqlSessio...

    Dionysus_go 评论0 收藏0
  • 【深入浅出MyBatis笔记】MyBatis解析运行原理

    摘要:的解析和运行原理构建过程提供创建的核心接口。在构造器初始化时会根据和的方法解析为命令。数据库会话器定义了一个对象的适配器,它是一个接口对象,构造器根据配置来适配对应的对象。它的作用是给实现类对象的使用提供一个统一简易的使用适配器。 MyBatis的解析和运行原理 构建SqlSessionFactory过程 SqlSessionFactory提供创建MyBatis的核心接口SqlSess...

    bitkylin 评论0 收藏0
  • 开源框架解析,手写MyBatis细节思路

    摘要:基本纲要组成动态配置配置核心源码分析源码解析源码解析源码解析源码解析手写框架是什么本质是一种半自动的框架,前身是其源于和的组合,除了和映射关系之外,还需要编写语句映射三要素映射规则快速入门加入的依赖添加的配置文件场景介绍编写实体类接口以及文 showImg(https://segmentfault.com/img/bVblrnC); Mybatis基本纲要 Mybatis组成 · 动态...

    paulli3 评论0 收藏0
  • 源码的角度解析Mybatis的会话机制

    摘要:从源码的角度分析源码分析从哪一步作为入口呢如果是看过我之前写的那几篇关于的源码分析,我相信你不会在源码前磨磨蹭蹭,迟迟找不到入口。 微信公众号「后端进阶」,专注后端技术分享:Java、Golang、WEB框架、分布式中间件、服务治理等等。 老司机倾囊相授,带你一路进阶,来不及解释了快上车! 坐在我旁边的钟同学听说我精通Mybatis源码(我就想不通,是谁透漏了风声),就顺带问了我一个...

    DevWiki 评论0 收藏0

发表评论

0条评论

娣辩孩

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<