资讯专栏INFORMATION COLUMN

重拾-MyBatis-配置文件解析

王晗 / 2059人阅读

摘要:前言我们知道在使用时,我们需要通过去创建实例,譬如为的配置文件那么我们看下方法的具体实现创建实例并执行解析主要通过执行对配置文件的解析,具体实现如下文配置文件解析解析标签解析标签解析别名标签解析插件标签解析标签解析标签解析标签从的方法实现我

前言

我们知道在使用 Mybatis 时,我们需要通过 SqlSessionFactoryBuild 去创建 SqlSessionFactory 实例,譬如:

// resource 为 mybatis 的配置文件 
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

那么我们看下 build 方法的具体实现

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
        // 创建 XMLConfigBuilder 实例并执行解析
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {

      }
    }
}

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

Mybatis 主要通过 XMLConfigBuilder 执行对配置文件的解析,具体实现如下文:

配置文件解析
private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      // 解析 properties 标签
      propertiesElement(root.evalNode("properties"));
      // 解析 settings 标签
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      // 解析 typeAliases 别名标签
      typeAliasesElement(root.evalNode("typeAliases"));
      // 解析 plugins 插件标签
      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
      // 解析 environments 标签
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      // 解析 typeHandlers 标签
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析 mappers 标签
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

XMLConfigBuilder 的方法 parseConfiguration 实现我们知道,MyBatis 会依次解析配置文件中的相应标签,本文将针对开发中常用的配置进行分析;主要包括 properties, typeAliases, enviroments, typeHandlers, mappers

properties 解析 配置示例

    
    
          
          
    

从配置示例可以看出 properties 属性变量的来源可以是外部的配置文件,也可以是配置文件中自定义的,也可以是 SqlSessionFactoryBuilderbuild 方法传参譬如:

public SqlSessionFactory build(InputStream inputStream, Properties properties) {
    return build(inputStream, null, properties);
  }
那么当存在同名的属性时,将采用哪种方式的属性值呢?
解析
private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      // 获取 properties 标签下的所有 property 子标签
      Properties defaults = context.getChildrenAsProperties();
      // 获取 resource,url 属性
      String resource = context.getStringAttribute("resource");
      String url = context.getStringAttribute("url");

      // resource url 两个属性不能同时存在
      if (resource != null && url != null) {
        throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
      }
      if (resource != null) {
        // 加载 resource 指定的配置文件
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } else if (url != null) {
        // 加载 url 指定的配置文件
        defaults.putAll(Resources.getUrlAsProperties(url));
      }

      /**
       * 获取传参的 properties
       * 构建 sqlSessionFactory 时可以传参 properties
       *
       * @see SqlSessionFactoryBuilder.build(InputStream inputStream, Properties properties)
       */
      Properties vars = configuration.getVariables();
      if (vars != null) {
        defaults.putAll(vars);
      }
      parser.setVariables(defaults);
      // 将 properties 赋值 configuration 中的 variables 变量
      configuration.setVariables(defaults);
    }
  }
public Properties getChildrenAsProperties() {
    Properties properties = new Properties();
    // 遍历 properties 标签下的 propertry 子标签
    for (XNode child : getChildren()) {
      // 获取 propertry 的 name value 属性
      String name = child.getStringAttribute("name");
      String value = child.getStringAttribute("value");
      if (name != null && value != null) {
        properties.setProperty(name, value);
      }
    }
    return properties;
  }

properties 标签解析的实现来看,MyBatis 加载 properties 属性的过程如下:

首先加载 properties 标签内所有子标签的 property

其次加载 properties 标签属性 resourceurl 指定的外部属性配置

最后加载 SqlSessionFactoryBuilder 的方法 build 传参的属性配置

因此,通过方法参数传递的 properties 具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的是 properties 标签内的子标签 property 指定的属性。
typeAliases 解析
类型别名是为 Java 类型设置一个短的名字。它只和 XML 配置有关,存在的意义仅在于用来减少类完全限定名的冗余
配置示例

  
  
  

也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean,比如:


  
解析
private void typeAliasesElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 如果是 package 标签,对整个包下的 java bean 进行别名处理
        // 若 java bean 没有配置注解的话,使用 bean 的首字母小写类名作为别名
        // 若 java bean 配置了注解,使用注解值作为别名
        if ("package".equals(child.getName())) {
          // 获取指定的包名
          String typeAliasPackage = child.getStringAttribute("name");
          configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
        } else {
          // 别名
          String alias = child.getStringAttribute("alias");
          // 别名对应的类
          String type = child.getStringAttribute("type");
          try {
            Class clazz = Resources.classForName(type);
            if (alias == null) {
              // 默认别名为类名,若配置了别名注解则取注解值映射类
              typeAliasRegistry.registerAlias(clazz);
            } else {
              // 通过指定的别名映射类
              typeAliasRegistry.registerAlias(alias, clazz);
            }
          } catch (ClassNotFoundException e) {
            throw new BuilderException("Error registering typeAlias for "" + alias + "". Cause: " + e, e);
          }
        }
      }
    }
  }

typeAliasesElement 在对 typeAliases 标签解析时,针对采用 packagetypeAlias 两种配置方式进行了不同的解析。 下面我们先看下通过包名的配置方式

通过包名解析
public void registerAliases(String packageName) {
    registerAliases(packageName, Object.class);
  }

  public void registerAliases(String packageName, Class superType) {
      // 获取包下所有的类
    ResolverUtil> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set>> typeSet = resolverUtil.getClasses();
    for (Class type : typeSet) {
      // Ignore inner classes and interfaces (including package-info.java)
      // Skip also inner classes. See issue #6
      // 忽略内部类 接口
      if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
        registerAlias(type);
      }
    }
  }

  public void registerAlias(Class type) {
    // 别名为类名
    String alias = type.getSimpleName();
    // 是否配置了别名注解,若配置了则别名取注解值
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
      alias = aliasAnnotation.value();
    }
    registerAlias(alias, type);
  }

当通过 package 指定包名时,MyBatis 会扫描包下所有的类(忽略内部类,接口),若类没有采用 @Alias 注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名, 比如 domain.blog.Author 的别名为 author;若有注解,则别名为其注解值。

public void registerAlias(String alias, Class value) {
    if (alias == null) {
      throw new TypeException("The parameter alias cannot be null");
    }
    // issue #748
    // 别名小写处理
    String key = alias.toLowerCase(Locale.ENGLISH);
    if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
      throw new TypeException("The alias "" + alias + "" is already mapped to the value "" + typeAliases.get(key).getName() + "".");
    }
    // 别名与类映射
    typeAliases.put(key, value);
  }

在完成别名的解析之后会将其注册到 typeAliasRegistry 的变量 typeAliases Map 集合中。

配置环境 environments 解析
environments 用于事务管理器及数据源相关配置
配置示例

  
    
      
    
    
      
      
      
      
    
  
  
    
      
    
    
      
      
      
      
    
  
environments 的配置来看 MyBatis 是支持多数据源的,但每个 SqlSessionFactory 实例只能选择其中一个; 若需要连接多个数据库,就得需要创建多个 SqlSessinFactory 实例。
解析
private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        /**
         * @see org.apache.ibatis.session.SqlSessionFactoryBuilder.build 时未指定 enviorment, 则取默认的
         */
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        // 查找与 environment 匹配的配置环境
        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());
        }
      }
    }
  }
private boolean isSpecifiedEnvironment(String id) {
    if (environment == null) {
        // 若 environment 为空说明未指定当前 SqlSessionFactory 实例所需的配置环境;同时 environments 标签未配置 default 属性
      throw new BuilderException("No environment specified.");
    } else if (id == null) {
        // environment 标签需要配置 id 属性
      throw new BuilderException("Environment requires an id attribute.");
    } else if (environment.equals(id)) {
        // environment == id 说明当前匹配配置环境
      return true;
    }
    return false;
  }

environments 支持多数据源的配置,所以在解析时会先查找匹配当前 SqlSessionFactoryenvironment; 然后在解析当前配置环境所需的事务管理器和数据源。

事务管理器解析
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.");
  }

事务管理器解析时会通过配置中指定的 type 别名去查找对应的 TransactionFactory 并实例化。

那么 MyBatis 内部内置了哪些事务管理器呢?
public Configuration() {
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

    // 省略
  }

Configuration 的构造可以看出,其构造时会通过 typeAliasRegistry 注册了别名为 JDBC,MANAGED 的两种事务管理器。

数据源解析
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.");
  }

同事务管理器一样,数据源解析时也会通过指定的别名查找对应的数据源实现类同样其在 Configuration 构造时向 typeAliasRegistry 注册了三种数据源

public Configuration() {
    
    // 省略

    typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);

    // 省略
  }
类型转换器 typeHandlers 解析 配置示例

  

  
解析
private void typeHandlerElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String typeHandlerPackage = child.getStringAttribute("name");
          typeHandlerRegistry.register(typeHandlerPackage);
        } else {
          // 映射 java 对象类型
          String javaTypeName = child.getStringAttribute("javaType");
          // 映射 jdbc 类型
          String jdbcTypeName = child.getStringAttribute("jdbcType");
          // 类型转换器类名
          String handlerTypeName = child.getStringAttribute("handler");

          Class javaTypeClass = resolveClass(javaTypeName);
          JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
          Class typeHandlerClass = resolveClass(handlerTypeName);
          if (javaTypeClass != null) {
            if (jdbcType == null) {
              // 指定了 java type,未指定 jdbc type
              typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
            } else {
              // 指定了 java type,指定了 jdbc type
              typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
            }
          } else {
              // 未指定 java type 按 typeHandlerClass 注册
            typeHandlerRegistry.register(typeHandlerClass);
          }
        }
      }
    }
  }
typeHandler 解析
指定 javaType 和 jdbcType
public void register(Class javaTypeClass, JdbcType jdbcType, Class typeHandlerClass) {
    register(javaTypeClass, jdbcType, getInstance(javaTypeClass, typeHandlerClass));
  }
private void register(Type javaType, JdbcType jdbcType, TypeHandler handler) {
    if (javaType != null) {
      // 一个 java type 可能会映射多个 jdbc type
      Map> map = typeHandlerMap.get(javaType);
      if (map == null || map == NULL_TYPE_HANDLER_MAP) {
        map = new HashMap<>();
        typeHandlerMap.put(javaType, map);
      }
      map.put(jdbcType, handler);
    }

    // 存储 typeHandler
    allTypeHandlersMap.put(handler.getClass(), handler);
  }
当指定了 javaTypejdbcType 最终会将二者及 typeHandler 映射并注册到 typeHandlerMap 中,从 typeHandlerMap 的数据结构来看,javaType 可能会与多个 jdbcType 映射。 譬如 String -> CHAR,VARCHAR
指定 javaType 未指定 jdbcType
public void register(Class javaTypeClass, Class typeHandlerClass) {
    // 将 type handler 实例化
    register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
  }
private  void register(Type javaType, TypeHandler typeHandler) {
    // 获取 MappedJdbcTypes 注解
    // 该注解用于设置类型转换器匹配的 jdbcType
    MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
    if (mappedJdbcTypes != null) {
        // 遍历匹配的 jdbcType 并注册
      for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
        register(javaType, handledJdbcType, typeHandler);
      }
      if (mappedJdbcTypes.includeNullJdbcType()) {
        register(javaType, null, typeHandler);
      }
    } else {
      // 未指定 jdbcType 时按 null 处理    
      register(javaType, null, typeHandler);
    }
  }
当类型转换器配置了 javaType 未配置 jdbcType 时,会判断类型转换器是否配置了 @MappedJdbcTypes 注解; 若配置了则使用注解值作为 jdbcType 并注册,若未配置则按 null 注册。
未指定 javaType 和 jdbcType
public void register(Class typeHandlerClass) {
    boolean mappedTypeFound = false;
    // 获取 MappedTypes 注解
    // 该注解用于设置类型转换器匹配的 javaType
    MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
      for (Class javaTypeClass : mappedTypes.value()) {
          // 执行注册
        register(javaTypeClass, typeHandlerClass);
        mappedTypeFound = true;
      }
    }
    if (!mappedTypeFound) {
      register(getInstance(null, typeHandlerClass));
    }
  }
javaType,jdbcType 均为指定时,会判断类型转换器是否配置了 @MappedTypes 注解; 若配置了则使用注解值作为 javaType 并注册。
package 解析
public void register(String packageName) {
    // 扫描指定包下的所有类
    ResolverUtil> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
    Set>> handlerSet = resolverUtil.getClasses();
    for (Class type : handlerSet) {
      //Ignore inner classes and interfaces (including package-info.java) and abstract classes
        // 忽略内部类 接口 抽象类
      if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
          // 执行注册
        register(type);
      }
    }
  }
当按指定包名解析时,会扫描包下的所有类(忽略内部类,接口,抽象类)并执行注册
小结

本文我们主要分析了 Mybatis 配置文件中标签 properties,typeAliases,enviroments,typeHandlers 的解析过程,由于 mappers 的解析比较复杂后续在进行分析;通过本文的分析我们了解到 Configuration 实例中包括以下内容:

variables : Properties 类型,存储属性变量

typeAliasRegistry : 别名注册中心,通过一个 Map 集合变量 typeAliases 存储别名与类的映射关系

environment : 配置环境,绑定事务管理器和当前数据源

typeHandlerRegistry : 类型转换器注册中心,存储 javaTypejdbcType,typeHandler 的映射关系,内置 jdbcTypetypeHandler 的映射关系

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

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

相关文章

  • 后台 - 收藏集 - 掘金

    摘要:探究系统登录验证码的实现后端掘金验证码生成类手把手教程后端博客系统第一章掘金转眼间时间就从月份到现在的十一月份了。提供了与标准不同的工作方式我的后端书架后端掘金我的后端书架月前本书架主要针对后端开发与架构。 Spring Boot干货系列总纲 | 掘金技术征文 - 掘金原本地址:Spring Boot干货系列总纲博客地址:http://tengj.top/ 前言 博主16年认识Spin...

    CrazyCodes 评论0 收藏0
  • 重拾-Spring Transaction

    摘要:当存在挂起的事务时,执行恢复挂起的事务将挂起的事务绑定的重新绑定到当前上下文事务的就是将挂起的事务重新绑定到当前上下文中。 问题 面试中是不是有时经常会被问到 Spring 事务如何管理的了解吗? ,Spring 事务的传播性有哪些,能聊聊它们的使用场景吗?, 事务回滚的时候是所有异常下都会回滚吗?; 下面我们就带着这些问题来看看 Spring 事务是如何实现的吧。 实现分析 首先我们...

    fasss 评论0 收藏0
  • 重拾-Spring-IOC

    摘要:为何重拾使用了多年,但是对其底层的一些实现还是一知半解,一些概念比较模糊故决定重新拾起,加深对的认识。小结是在完成创建后对其进行后置处理的接口是在完成实例化对其进行的后置处理接口是框架底层的核心接口,其提供了创建,获取等核心功能。 为何重拾 使用了 Spring 多年,但是对其底层的一些实现还是一知半解,一些概念比较模糊;故决定重新拾起,加深对 Spring 的认识。 重拾计划 spr...

    GraphQuery 评论0 收藏0
  • Spring Security

    摘要:框架具有轻便,开源的优点,所以本译见构建用户管理微服务五使用令牌和来实现身份验证往期译见系列文章在账号分享中持续连载,敬请查看在往期译见系列的文章中,我们已经建立了业务逻辑数据访问层和前端控制器但是忽略了对身份进行验证。 重拾后端之Spring Boot(四):使用JWT和Spring Security保护REST API 重拾后端之Spring Boot(一):REST API的搭建...

    keelii 评论0 收藏0
  • 重拾Java Network Programming(二)InetAddress

    摘要:前言今天,我将梳理在网络编程中很重要的一个类以及其相关的类。这类主机通常不需要外部互联网服务,仅有主机间相互通讯的需求。可以通过该接口获取所有本地地址,并根据这些地址创建。在这里我们使用阻塞队列实现主线程和打印线程之间的通信。 前言 今天,我将梳理在Java网络编程中很重要的一个类InetAddress以及其相关的类NetworkInterface。在这篇文章中将会涉及: InetA...

    daryl 评论0 收藏0

发表评论

0条评论

王晗

|高级讲师

TA的文章

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