mybatis源码分析之配置文件解析

一、简介

我们上一个篇文章已经配置好了,mybatis配置文件和测试类。我们先分析一下mybatis的是如何加载mybatis-config.xml文件的。

String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);

这里是通过mybatis工具类Resources加载配置文件,得到一个InputStream输入流。

二、资源文件加载

接着我们先看一下Resources的资源加载工具类。

public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
    // 通过classLoaderWrapper包装类加载
    InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
    if (in == null) {
        throw new IOException("Could not find resource " + resource);
    }
    return in;
}

接着通过类加载器,从资源路径(classpath)中加载资源,

public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
    return getResourceAsStream(resource, getClassLoaders(classLoader));
}

这时候初始化类加载有五种:

ClassLoader[] getClassLoaders(ClassLoader classLoader) {
    return new ClassLoader[]{
        classLoader,  // 出入的类加载
        defaultClassLoader, // 默认类加载
        Thread.currentThread().getContextClassLoader(),  // 线程类上下文加载器
        getClass().getClassLoader(),  // 当前类加载器
        systemClassLoader   // 系统类加载器
    };
}

然后会从这些类加载器中加载我们需要资源:

InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
    // 循环遍历获取资源
    for (ClassLoader cl : classLoader) {
        if (null != cl) {

            // try to find the resource as passed
            InputStream returnValue = cl.getResourceAsStream(resource);

            // 有些加载器需要添加`/`,所以加上这个`/`前缀,重试
            if (null == returnValue) {
                returnValue = cl.getResourceAsStream("/" + resource);
            }

            if (null != returnValue) {
                return returnValue;
            }
        }
    }
    return null;
}

资源路径: 是我们编译之后target/classes/目录下的资源。

这个逻辑其实还是很简单的,重点是我们需要复习一下类加载器相关的技能。下面是我们常见的类加载器基本上是这些:

  • ClassLoader.getSystemClassLoader():系统类加载器
  • xxx.class.getClassLoader()this.getClass().getClassLoader():当前类加载器
  • Thread.currentThread().getContextClassLoader():线程上下文加载器

主要区别:

  1. getClassLoader:是获取加载当前类的类加载器。可能是***启动类加载器***、拓展类加载器系统类加载器,取决于当前类是有哪个加载器加载的。
  2. getClontextCLassLoader:是获取当前线程上下文的类加载器,用户可以自己设置,Java SE环境下一般是AppClassLoaderJava EE环境下是WebappClassLoader
  3. getSystemClassLoader:是获取系统类加载器AppClassLoader

三、构建SessionFactory

接着我们看看一下sessionFactory是如何构建的

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

我们看一下SqlSessionFactoryBuilder这个类,这个就一个核心方法:build。这个方法大体分成两类,都是参数重载的方法。

public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
}

这个方法可以将数据源信息和属性一起构建,我们是从配置文件中构建的,所以这两个参数时null

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      // 构建配置文件解析器  
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // 这里面调用的是解析器的解析各个节点  
      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()。这里面用来解析mybatis-config.xml的各个节点。

public Configuration parse() {
    // step 防止别多次读取
    if (parsed) {
        throw new BuilderException("每个XMLConfigBuilder只能使用一次.");
    }
    parsed = true;
    // step 解析各个节点
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

/**
 * 解析配置文件的各个节点
 * @param root  根节点
 */
private void parseConfiguration(XNode root) {
    try {
        // 解析 properties 节点
        propertiesElement(root.evalNode("properties"));
        // 解析 解析 settings 配置,并将其转换为 Properties 对象
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        // 加载 vfs  VFS主要用来加载容器内的各种资源,比如jar或者class文件
        loadCustomVfs(settings);
        //  解析日志配置
        loadCustomLogImpl(settings);
        // 解析 typeAliases 配置 (别名)
        typeAliasesElement(root.evalNode("typeAliases"));
        // 解析 plugins 配置 (插件)
        pluginElement(root.evalNode("plugins"));
        // 解析 objectFactory 配置 (对象工厂)
        objectFactoryElement(root.evalNode("objectFactory"));
        // 解析 objectWrapperFactory 配置  (创建对象包装器工厂)
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        // 解析 reflectorFactory 配置 (反射工厂)
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        // settings 中的信息设置到 Configuration 对象中
        settingsElement(settings);
        // 解析 environments 配置
        environmentsElement(root.evalNode("environments"));
        // 解析 databaseIdProvider,获取并设置 databaseId 到 Configuration 对象
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        // 解析 typeHandlers 配置
        typeHandlerElement(root.evalNode("typeHandlers"));
        // 解析 mappers 配置
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("解析SQL Mapper配置时出错. 原因: " + e, e);
    }
}

3.1. properties节点的解析

我们在mybatis-config.xml中配置properties节点, 我们里面配置的是resource

<configuration>
  <properties resource="jdbc.properties" />
  ......
</configuration>

jdbc.properties里面配置的是我们的数据库连接相关信息

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.252.139:3306/mybatis-test
jdbc.username=root
jdbc.password=123456

接着看着propertiesElement这个是如何解析properties节点的。

/**
 * 解析properties节点
 * @param context  properties节点
 * @throws Exception
 */
private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
        // step 读取子节点数据
        Properties defaults = context.getChildrenAsProperties();
        // step 获取节点里面的resource和url
        String resource = context.getStringAttribute("resource");
        String url = context.getStringAttribute("url");
        // step resource和url只能有一个存在
        if (resource != null && url != null) {
            throw new BuilderException("properties元素不能同时指定URL和基于资源的属性文件引用。请指定其中一个。");
        }
        // step resource存在,使用Resources工具类加载properties文件
        if (resource != null) {
            defaults.putAll(Resources.getResourceAsProperties(resource));
        } else if (url != null) {
            defaults.putAll(Resources.getUrlAsProperties(url));
        }
        // step 如果节点里面有属性,在设置到defaults里面
        Properties vars = configuration.getVariables();
        if (vars != null) {
            defaults.putAll(vars);
        }
        // step 将属性设置到XPathParser对象里面
        parser.setVariables(defaults);
        // step 将属性设置到configuration对象里面
        configuration.setVariables(defaults);
    }
}

这里我们可以看到,在加载properties节点的时候,会先加载子节点的数据,然后才会加载resource中的数据,所有就会发生resource会覆盖properties节点中子节点数据。

3.2. setting节点解析

setting配置是mybatis中非常重要的配置,一般我们使用默认配置就可以了。我们先看一下mybatis有哪些全局运行参数:

  • cacheEnabled:默认是true,改配置影响的所有映射器中配置的缓存的全局开关。
  • lazyLoadingEnabled:默认是true, 延迟加载的全局开关,当开启时,所有关联对象都会延迟加载。特定关联关系中可以通过设置fetchType属性来覆盖该项的开关状态。
  • multipleResultSetsEnabled:默认值是true,是否允许单一语句返回多结果集(需要兼容驱动)
  • useColumnLabel: 默认值是true,使用列标签代替列名。不同驱动有不同的表现。
  • useGeneratedKeys:默认值false,允许自动生成主键,需要驱动兼容。
  • autoMappingBehavior:默认值PARTIAL,指定mybatis应如何自动映射到字段或者属性。NONE:表示取消自动映射,PARTIAY:只会自动映射没有定义嵌套结果集映射的结果集,FULL会自动映射任意复杂的结果集
  • autoMappingUnkownColumnBehavior:默认值WARNING
  • defaultExecutorType:默认值SIMPLE,配置默认的执行器。SIMPLE:就是普通执行器,REUSE:执行器会重用预处理器(prepared statements)BATCH:执行器将重用语句并执行批量更新。
  • defaultStatementTimeout:默认值25,设置超时时间,它决定驱动等待数据库影响的秒数。
  • defaultFetchSize:默认值100,为驱动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。
  • safeRowBoundsEnabled:默认值false,允许在嵌套语句中使用分页RowBounds
  • mapUnderscoreToCamelCase:默认值false,是否开启自动驼峰命名规则映射。
  • localCacheScopeMyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。
  • jdbcTypeForNull:当没有提供特定的JDBC类型时,为空值指定JDBC类型。某些驱动需要指定列的JDBC类型,多数情况直接用一般类型,比如NULLVARCHAR或者OTHER
  • lazyLoadTriggerMethods:默认值equalsclonehashCodetoString,指定哪个对象的方法触发一次延迟加载。

其他的配置可以参看官网:https://mybatis.org/mybatis-3/zh/configuration.html

一般来说这些配置我们保持默认就可以。先添加一些setting的配置。

<settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="lazyLoadingEnabled" value="true"/>
</settings>

接着我们看一下如何解析setting节点的。

private Properties settingsAsProperties(XNode context) {
    if (context == null) {
        return new Properties();
    }
    // 获取setting属性内容
    Properties props = context.getChildrenAsProperties();
    // 检查配置类是否知道所有设置
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
    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).");
        }
    }
    return props;
}

context.getChildrenAsProperties可以获取我们配置的那些配置项。

MetaClass.forClass将用于获取所有的全局配置信息。在里面或包含我们方才配置的那两个属性。这个MetaClass具体如何运行的,我们后面分析。(这个地方我们可以先去省略MetaClass中操作,这个里面方法还是挺多的,我们可以知道其的作用是啥,具体怎么实现后面再看,当成一个黑盒子,不影响我们整体去看源码的结构。)

最后需要将setting解析出来的数据设置到configuration中:

这里面我们和3.2上面的那些全局属性相互对应的看看就可以了。

3.3. typeAliases别名节点解析

现在我们先配置一些别名,我们先来测试debug跟一下代码我们就知道怎么解析别名了

mybatis中,我们可以一些类定义一个别名,在使用的时候用直接用别名就可以了,不需要出入类所在类路径。

/**
 * 解析typeAliases
 * @param parent
 */
private void typeAliasesElement(XNode parent) {
    if (parent != null) {
        // step 1. 遍历typeAliases节点
        for (XNode child : parent.getChildren()) {
            // step 2. 判断是否节点名字为 package
            if ("package".equals(child.getName())) {
                // step 2.1. 获取节点属性的name
                String typeAliasPackage = child.getStringAttribute("name");
                configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
            } else { // step 3. 判断节点名为typeAlias
                // step 3.1. 获取节点属性的alias和type
                String alias = child.getStringAttribute("alias");
                String type = child.getStringAttribute("type");
                try {
                    // step 3.2. 获取class
                    Class<?> clazz = Resources.classForName(type);
                    // step 3.3. 注册别名到映射
                    if (alias == null) {
                        typeAliasRegistry.registerAlias(clazz);
                    } else {
                        typeAliasRegistry.registerAlias(alias, clazz);
                    }
                } catch (ClassNotFoundException e) { // step 4. class没有找到抛出异常
                    throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
                }
            }
        }
    }
}

从这个大体的解析过程,我们可以看到mybatis解析别名的时候是有两种方法的,第一种是解析<package name="com.mly.learn.entity"/>这一中通过扫描包的方式将别名注册到映射,第二种是解析<typeAlias type="com.mly.learn.entity.Student" alias="Student" />这两个是不可以并存的,我们只能选择其中一种。

接着我们看一下这两种方法是如何注册到映射中的。

首先mybatis已经注册一堆类型映射。

在有的别名也需要注册到typeAliasesmap中。我们看一下mybatis是如何将别名注册到map中的呢!

public void registerAliases(String packageName, Class<?> superType) {
    // 包扫描或者我们在package下的类
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
    // 遍历注册
    for (Class<?> type : typeSet) {
        // 跳过内部类和接口
        if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
            // 注册
            registerAlias(type);
        }
    }
}

接着在registerAlias判断类上是否有Alias注解,有的话将value作为key,或者将类名作为key,然后判断typeAliases中是否存在或者value相同,否则抛出异常。

/**
 * 注册别名
 * @param type
 */
public void registerAlias(Class<?> type) {
    // step 获取类名
    String alias = type.getSimpleName();
    // step 2. 判断是否存在Alias 注解, 存在的话,将注解中value值作为key, 否则将类名作为key
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
        alias = aliasAnnotation.value();
    }
    registerAlias(alias, type);
}

/**
 * 注册alias:class到map中
 * @param alias
 * @param value
 */
public void registerAlias(String alias, Class<?> value) {
    // step 1. 别名== null, 抛出异常
    if (alias == null) {
        throw new TypeException("The parameter alias cannot be null");
    }
    // step 2. 转成小写字母
    String key = alias.toLowerCase(Locale.ENGLISH);
    // step 3. 判断typeAliases是否包含key或者value相同,抛出异常
    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);
}

另一种方法,实际调用的还是registerAlias这个方法一样的。就不做累述。

3.4. typeHandlers类型转换节点解析

我们数据库的类型和Java类型是不一样的,我们需要进行转换才可以,例如我们在使用JavaString类型,但是在MySql数据类型是char或者varchar类型。这种数据类型之后的对应就需要typeHanlder进行准换。

看一下默认注册的一些类型转换器:

接着看一下mybatis的是如何处理typeHandlerxml节点的。

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 {
                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);
                if (javaTypeClass != null) {
                    if (jdbcType == null) {
                        typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
                    } else {
                        typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
                    }
                } else {
                    typeHandlerRegistry.register(typeHandlerClass);
                }
            }
        }
    }
}

扫描包下面的class将其注册到类型转换映射器中,这里面的就是各种重载方法之间的调用,有点回看晕,先画个图看看啊。

先看一下扫描包这个方法执行的过程, 代码就不一一展示了。

public void register(String packageName) {
    // 扫描包路径下的class
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
    Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
    for (Class<?> type : handlerSet) {
        // 忽略内部类、接口和抽象类
        if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
            register(type);
        }
    }
}

其他的就不一一贴代码了,可以跟着上面的图一一看一下就OK,逻辑不复杂。

private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
        Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
        if (map == null || map == NULL_TYPE_HANDLER_MAP) {
            map = new HashMap<>();
        }
        map.put(jdbcType, handler);
        typeHandlerMap.put(javaType, map);
    }
    allTypeHandlersMap.put(handler.getClass(), handler);
}

3.5. environments节点解析

MyBatis 中,事务管理器和数据源是配置在<environments>节点中的。接着我们来看是如何解析的:

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            // step  获取默认节点
            environment = context.getStringAttribute("default");
        }
        // step 遍历environment节点
        for (XNode child : context.getChildren()) {
            // step 获取id的子节点
            String id = child.getStringAttribute("id");
            // step  判断是否为指定的节点
            if (isSpecifiedEnvironment(id)) {
                // step 配置事务
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                // step 构建 DataSourceFactory 对象
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                DataSource dataSource = dsFactory.getDataSource();
                // step 将事务处理器、数据源和id组装到一起,并设置到configuration中
                Environment.Builder environmentBuilder = new Environment.Builder(id)
                    .transactionFactory(txFactory)
                    .dataSource(dataSource);
                configuration.setEnvironment(environmentBuilder.build());
            }
        }
    }
}

当我们配置两个数据源的时候,mybatis只会读取default的数据源ID。然后将事务管理器和数据源组装成一个Environment对象。

这个是比较简单。

3.6. plugins插件节点解析

这个部分在分析分页插件的时候在去具体分析吧。先挖个坑。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章