MyBatis 配置文件解析

MyBatis 配置文件解析

传统JDBC弊端

1、jdbc底层没有用连接池、操作数据库需要频繁的创建和关联链接。消耗很大的资源

2、写原生的jdbc代码在java中,一旦我们要修改sql的话,java需要整体编译,不利于系统维护

3、使用PreparedStatement预编译的话对变量进行设置123数字,这样的序号不利于维护

4、返回result结果集也需要硬编码。

ORM框架Mybatis介绍

MyBatis is a first class persistence framework with support for custom SQL, stored procedures and advanced mappings. MyBatis eliminates almost all of the JDBC code and manual setting of parameters and retrieval of results. MyBatis can use simple XML or Annotations for configuration and map primitives, Map interfaces and Java POJOs (Plain Old Java Objects) to database records.

MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
在这里插入图片描述

整体概述

在单独使用 MyBatis 时,第一步要做的事情就是根据配置文件构建SqlSessionFactory对象。相关代码如下:

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

在build方法中会进行具体的解析,主要解析的类容如下:

  //分步骤解析
  //issue#117readpropertiesfirst
  //1.properties
  propertiesElement(root.evalNode("properties"));
  //2.类型别名
  typeAliasesElement(root.evalNode("typeAliases"));
  //3.插件
  pluginElement(root.evalNode("plugins"));
  //4.对象工厂
  objectFactoryElement(root.evalNode("objectFactory"));
  //5.对象包装工厂
  objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
  //6.设置
  settingsElement(root.evalNode("settings"));
  //readitafterobjectFactoryandobjectWrapperFactoryissue#631
//7.环境@monkey老师
  environmentsElement(root.evalNode("environments"));
  //8.databaseIdProvider
  databaseIdProviderElement(root.evalNode("databaseIdProvider"));
  //9.类型处理器
  typeHandlerElement(root.evalNode("typeHandlers"));
  //10.映射器
  mapperElement(root.evalNode("mappers"));

properties属性解析

properties节点的配置内容如下:

<properties resource="jdbc.properties">
    <property name="jdbc.username" value="coolblog"/>
    <property name="hello" value="world"/>
</properties>

解析过程:

private void propertiesElement(XNode context) throws Exception {
  if (context != null) {
    // 解析 propertis 的子节点,并将这些节点内容转换为属性对象 Properties
    Properties defaults = context.getChildrenAsProperties();
    // 获取 propertis 节点中的 resource 和 url 属性值
    String resource = context.getStringAttribute("resource");
    String url = context.getStringAttribute("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) {
      // 从文件系统中加载并解析属性文件
      defaults.putAll(Resources.getResourceAsProperties(resource));
    } else if (url != null) {
      // 通过 url 加载并解析属性文件
      defaults.putAll(Resources.getUrlAsProperties(url));
    }
    Properties vars = configuration.getVariables();
    if (vars != null) {
      defaults.putAll(vars);
    }
    parser.setVariables(defaults);
    // 将属性值设置到 configuration 中
    configuration.setVariables(defaults);
  }
}

上面就是节点解析过程,不是很复杂。主要包含三个步骤:

  1. 解析节点的子节点,并将解析结果设置到Properties对象中。

  2. 从文件系统或通过网络读取属性配置,这取决于节点的resource和url是否为空。第二步对应的代码比较简单,这里就不分析了。

  3. 将包含属性信息的Properties对象设置到XPathParser和Configuration中。

    需要注意的是,propertiesElement方法是先解析节点的子节点内容,然后再从文件系统或者网络读取属性配置,并将所有的属性及属性值都放入到defaults属性对象中(配置文件中的会替换xml中的)。这会导致同名属性覆盖的问题,也就是从文件系统,或者网络上读取到的属性和属性值会覆盖掉子节点中同名的属性和及值。假如上面配置中的jdbc.properties内容如下:

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/myblog?...jdbc.username=root
jdbc.password=1234

settings属性解析

settings的配置如下

<settings>
 <settingname="cacheEnabled"value="false"/>
</settings>

解析过程

private Properties settingsAsProperties(XNode context) {
  if (context == null) {
    return new Properties();
  }
  // 获取 settings 子节点中的内容,getChildrenAsProperties
  Properties props = context.getChildrenAsProperties();
  // Check that all settings are known to the configuration class
  // 创建 Configuration 类的“元信息”对象
  MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
  for (Object key : props.keySet()) {
    // 检测 Configuration 中是否存在相关属性,不存在则抛出异常
    if (!metaConfig.hasSetter(String.valueOf(key))) {
      throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
    }
  }
  return props;
}
private void settingsElement(Properties props) {
  configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
  ......
}

主要解析过程:

  1. 解析settings子节点的内容,并将解析结果转成Properties对象
  2. 为Configuration创建元信息对象
  3. 通过MetaClass检测Configuration中是否存在某个属性的setter方法,不存在则抛异常
  4. 若通过MetaClass的检测,则返回Properties对象,方法逻辑结束

typeAliases属性解析

在MyBatis中,我们可以为自己写的一些类定义一个别名。这样在使用的时候,只需要输入别名即可,无需再把全限定的类名写出来。在MyBatis中,我们有两种方式进行别名配置。第一种是仅配置包名,让MyBatis去扫描包中的类型,并根据类型得到相应的别名。这种方式可配合Alias注解使用,即通过注解为某个类配置别名,而不是让MyBatis按照默认规则生成别名。这种方式的配置如下:

<typeAliases>
  <packagename="domain.blog"/>
</typeAliases>

@Alias("author")
public class Author {
}

第二种方式是通过手动的方式,明确为某个类型配置别名。这种方式的配置如下:

<typeAliases>
  <typeAliasalias="Author"type="domain.blog.Author"/>
  <typeAliasalias="Blog"type="domain.blog.Blog"/>
  <typeAliasalias="Comment"type="domain.blog.Comment"/>
  <typeAliasalias="Post"type="domain.blog.Post"/>
  <typeAliasalias="Section"type="domain.blog.Section"/>
  <typeAliasalias="Tag"type="domain.blog.Tag"/>
</typeAliases>

解析过程:

private void typeAliasesElement(XNode parent) {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      //从指定的包中解析别名和类型的映射
      if ("package".equals(child.getName())) {
        String typeAliasPackage = child.getStringAttribute("name");
        //(一)调用TypeAliasRegistry.registerAliases,去包下找所有类,然后注册别名(有@Alias注解则用,没有则取类的simpleName)
        configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
        //从 typeAlias 节点中解析别名和类型的映射
      } else {
        // 获取 alias 和 type 属性值,alias 不是必填项,可为空
        String alias = child.getStringAttribute("alias");
        String type = child.getStringAttribute("type");
        try {
          // 加载 type 对应的类型
          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);
        }
      }
    }
  }
}

plugins属性解析

插件是 MyBatis 提供的一个拓展机制,通过插件机制我们可在 SQL 执行过程中的某些点上做一些自定义操作。实现一个插件需要比简单,首先需要让插件类实现Interceptor接口。然后在插件类上添加@Intercepts@Signature注解,用于指定想要拦截的目标方法。MyBatis 允许拦截下面接口中的一些方法:

  • Executor: update 方法,query 方法,flushStatements 方法,commit 方法,rollback 方法, getTransaction 方法,close 方法,isClosed 方法
  • ParameterHandler: getParameterObject 方法,setParameters 方法
  • ResultSetHandler: handleResultSets 方法,handleOutputParameters 方法
  • StatementHandler: prepare 方法,parameterize 方法,batch 方法,update 方法,query 方法

比较常见的插件有分页插件、分表插件等,先来了解插件的配置。如下:

<plugins>
    <plugin interceptor="xyz.coolblog.mybatis.ExamplePlugin">
        <property name="key" value="value"/>
    </plugin>
</plugins>

解析过程

private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      String interceptor = child.getStringAttribute("interceptor");
      // 获取配置信息
      Properties properties = child.getChildrenAsProperties();
      // 解析拦截器的类型,并创建拦截器
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      // 设置属性
      interceptorInstance.setProperties(properties);
      // 添加拦截器到 Configuration 中
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

插件解析的过程还是比较简单的。首先是获取配置,然后再解析拦截器类型,并实例化拦截器。最后向拦截器中设置属性,并将拦截器添加到Configuration中。

environments属性解析

在 MyBatis 中,事务管理器和数据源是配置在 environments 中的。它们的配置大致如下:

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
</environments>

解析过程:

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            // 获取 default 属性
            environment = context.getStringAttribute("default");
        }
        for (XNode child : context.getChildren()) {
            // 获取 id 属性
            String id = child.getStringAttribute("id");
            /*
             * 检测当前 environment 节点的 id 与其父节点 environments 的属性 default 
             * 内容是否一致,一致则返回 true,否则返回 false
             */
            if (isSpecifiedEnvironment(id)) {
                // 解析 transactionManager 节点,逻辑和插件的解析逻辑很相似,不在赘述
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                // 解析 dataSource 节点,逻辑和插件的解析逻辑很相似,不在赘述
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                // 创建 DataSource 对象
                DataSource dataSource = dsFactory.getDataSource();
                Environment.Builder environmentBuilder = new Environment.Builder(id)
                    .transactionFactory(txFactory)
                    .dataSource(dataSource);
                // 构建 Environment 对象,并设置到 configuration 中
                configuration.setEnvironment(environmentBuilder.build());
            }
        }
    }
}

typeHandlers 属性解析

在向数据库存储或读取数据时,我们需要将数据库字段类型和 Java 类型进行一个转换。比如数据库中有CHARVARCHAR等类型,但 Java 中没有这些类型,不过 Java 有String类型。所以我们在从数据库中读取 CHAR 和 VARCHAR 类型的数据时,就可以把它们转成 String 。在 MyBatis 中,数据库类型和 Java 类型之间的转换任务是委托给类型处理器TypeHandler去处理的。MyBatis 提供了一些常见类型的类型处理器,除此之外,我们还可以自定义类型处理器以非常见类型转换的需求

private void typeHandlerElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // 从指定的包中注册 TypeHandler
            if ("package".equals(child.getName())) {
                String typeHandlerPackage = child.getStringAttribute("name");
                // 注册方法 ①
                typeHandlerRegistry.register(typeHandlerPackage);

            // 从 typeHandler 节点中解析别名到类型的映射
            } else {
                // 获取 javaType,jdbcType 和 handler 等属性值
                String javaTypeName = child.getStringAttribute("javaType");
                String jdbcTypeName = child.getStringAttribute("jdbcType");
                String handlerTypeName = child.getStringAttribute("handler");

                // 解析上面获取到的属性值
                Class<?> javaTypeClass = resolveClass(javaTypeName);
                JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
                Class<?> typeHandlerClass = resolveClass(handlerTypeName);

                // 根据 javaTypeClass 和 jdbcType 值的情况进行不同的注册策略
                if (javaTypeClass != null) {
                    if (jdbcType == null) {
                        // 注册方法 ②
                        typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
                    } else {
                        // 注册方法 ③
                        typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
                    }
                } else {
                    // 注册方法 ④
                    typeHandlerRegistry.register(typeHandlerClass);
                }
            }
        }
    }
}
              } else {
                        // 注册方法 ③
                        typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
                    }
                } else {
                    // 注册方法 ④
                    typeHandlerRegistry.register(typeHandlerClass);
                }
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值