ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

MyBatis - 全局配置文件的加载机制

2021-11-19 21:04:57  阅读:125  来源: 互联网

标签:解析 resource String 配置文件 configuration MyBatis new null 加载


代码的调试,我们可以使用上一章的任意一个测试代码作为 Debug 载体,本章我们研究的其实是这两句代码:

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

也就是如何加载 MyBatis 全局配置文件,以及如何由全局配置文件构建出 SqlSessionFactory 。

1. 全局配置文件的加载

首先我们来看配置文件的加载,这个 Resources.getResourceAsStream 方法,只从方法名上,想必小伙伴也能猜出来,它应该是借助类加载器吧,我们快速的看一眼源码:

public static InputStream getResourceAsStream(String resource) throws IOException {
    return getResourceAsStream(null, resource);
}

public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
    InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
    if (in == null) {
        throw new IOException("Could not find resource " + resource);
    }
    return in;
}

这个地方看上去没有传入 ClassLoader ,实际上取 ClassLoader 的地方在另外一个位置:

ClassLoader[] getClassLoaders(ClassLoader classLoader) {
    return new ClassLoader[]{
        classLoader,
        defaultClassLoader,
        Thread.currentThread().getContextClassLoader(),
        getClass().getClassLoader(),
        systemClassLoader
    };
}

一下子取这么多 ClassLoader ,图个啥咧?很明显,它是想挨个 ClassLoader 都试一遍,只要能取到资源,就 OK 。下面是实际利用 ClassLoader 加载全局配置文件的底层源码:

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);
            // now, some class loaders want this leading "/", so we'll add it and try again if we didn't find the resource
            if (null == returnValue) {
                returnValue = cl.getResourceAsStream("/" + resource);
            }
            if (null != returnValue) {
                return returnValue;
            }
        }
    }
    return null;
}

逻辑很清晰吧,所以使用这种方式,就可以很简单的获取到全局配置文件的二进制流了。

2. 解析配置文件

下面就是解析的过程了,我们的测试代码是直接 new 了一个 SqlSessionFactoryBuilder ,随后调 build 方法构造出 SqlSessionFactory :

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);

而 build 方法最终来到了一个三参数的重载方法中:

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

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        return build(parser.parse());
    } // catch finally ......
}

可见,这里面用到的第一个底层核心组件,是 XMLConfigBuilder ,直译为基于 xml 的配置建造器(建造器模式的体现)。而这个 XMLConfigBuilder ,首先继承了一个叫 BaseBuilder 的东西:

public class XMLConfigBuilder extends BaseBuilder {
    // ......
}

我们先来研究这两个类的构造。

2.1 BaseBuilder

BaseBuilder 顾名思义,它是一个基础的构造器,它的初始化需要传入 MyBatis 的全局配置对象 Configuration :

public abstract class BaseBuilder {
    protected final Configuration configuration;
    protected final TypeAliasRegistry typeAliasRegistry;
    protected final TypeHandlerRegistry typeHandlerRegistry;

    public BaseBuilder(Configuration configuration) {
        this.configuration = configuration;
        this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
        this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
    }

这个 Configuration 我们都知道,最终 MyBatis 初始化完成后,所有的配置项、Mapper 、statement 都会存放到这里,小伙伴们能回忆起来就 OK 。

往下大致扫一眼定义的方法,大多数都是一些解析、获取之类的方法,看上去更像是提供基础的工具类方法支撑:(下面是引用的 BaseBuilder 中定义的两个方法)

protected Boolean booleanValueOf(String value, Boolean defaultValue) {
    return value == null ? defaultValue : Boolean.valueOf(value);
}

protected JdbcType resolveJdbcType(String alias) {
    if (alias == null) {
        return null;
    }
    try {
        return JdbcType.valueOf(alias);
    } catch (IllegalArgumentException e) {
        throw new BuilderException("Error resolving JdbcType. Cause: " + e, e);
    }
}

那照这样来看,核心的处理逻辑并不在 BaseBuilder 中,我们回到实现类 XMLConfigBuilder 中。

2.2 XMLConfigBuilder

照例,我们还是先看一眼内部成员,以及构造方法的定义。

2.2.1 构造方法定义

public class XMLConfigBuilder extends BaseBuilder {

    private boolean parsed;
    private final XPathParser parser;
    private String environment;
    private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
    
    public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
        // 注意这里new了一个XPathParser
        this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
    }

    private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
        super(new Configuration());
        ErrorContext.instance().resource("SQL Mapper Configuration");
        this.configuration.setVariables(props);
        this.parsed = false;
        this.environment = environment;
        this.parser = parser;
    }

源码中的构造方法重载的特别多,而上面在 SqlSessionFactoryBuilder 中调用的 build 方法,最终是调用的上面代码中的三参数构造方法,这个构造方法又调用了下面重载的构造方法。注意源码中,它重载的构造方法不再需要 InputStream ,而是构造了一个 XPathParser ,这个家伙虽然我们也没见过,但也能大概猜出来,它就是解析 xml 全局配置文件的解析器,这个东西我们没有必要先去研究,到下面用到的时候再顺道着看就 OK 。

另外,最下面的构造方法中,可以发现 Configuration 对象是在这里 new 出来的,而且是非常朴实无华的、用空参构造方法 new 出来,所以各位小伙伴可以先了解一个事情:如果真要我们自己去操作,去初始化 MyBatis 的 Configuration ,也不是不行,我们自己操作都无所谓。

其余的,最下面的构造方法中,都是普通的赋值操作,也没什么好说的。

2.2.2 核心parse方法

接下来的代码就是 return build(parser.parse()); 了,这一行代码实际上是两个方法,首先它先调用 XMLConfigBuilder 的 parse 方法,生成 Configuration ,之后才是 SqlSessionFactoryBuilder 的 build 方法。我们先来看 XMLConfigBuilder 是如何解析配置文件的。

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

最核心的方法还是中间的 parseConfiguration 方法,不过在此之前,我们关注一下 parser.evalNode("/configuration") 这个动作。

2.2.2.1 XPathParser#evalNode

XPathParser 的作用就是将 xml 配置文件转为 Document 对象,并提供对应的 xml 标签节点。它的 evalNode 方法,就是用来获取 xml 中指定的标签:

public XNode evalNode(String expression) {
    return evalNode(document, expression);
}

public XNode evalNode(Object root, String expression) {
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
        return null;
    }
    return new XNode(this, node, variables);
}

private Object evaluate(String expression, Object root, QName returnType) {
    try {
        // 使用javax的XPath解析xml
        return xpath.evaluate(expression, root, returnType);
    } catch (Exception e) {
        throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);
    }
}

至于 XPath 内部怎么做的,那是 xml 解析的机制了,我们不关心,我们唯一要关心的是,中间的 evalNode 方法解析出 Node 之后,return 处又封装成了一个 XNode 。它到底是图个啥呢?

2.2.2.2 Node封装为XNode的意图

注意看 XNode 的构造方法,它额外传入了一个 variables 对象,而这个 variables 实际上就是我们在全局配置文件中,定义的那些 <properties> 标签,以及引入的 .properties 文件。可为什么又跟这些配置属性值牵扯上了呢?我们回忆一下,之前我们在全局配置文件中写过这样的代码吧:

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

如果真的要取这里面的 driver 属性,肯定不能把 ${jdbc.driverClassName} 拿出来吧,得动态替换配置属性的值。但是 javax 原生的 Node 可实现不了这玩意,所以 MyBatis 就基于 javax 的 Node 封装了一个 XNode ,并组合 XPathParser ,就可以实现动态解析配置属性值的效果了。

可能这样解释的话,很多小伙伴会一脸茫然,小册举个例子。

以上面的 xml 为例,当我们取到 <dataSource> 标签后,要解析其中的 driver 、url 等属性,而这些属性的 value 是一些占位符,在解析的时候,MyBatis 会先跟平常一样,解析出 <property> 标签,然后获取 value 属性(比方说解析 driver 属性吧,这样返回的 value 是 ${jdbc.driverClassName} ),然后!它会使用一个占位符解析器,去解析一下这个占位符,并根据 properties> 标签中定义 / 加载的配置属性,替换为对应的属性值。通过这个步骤,${jdbc.driverClassName} 就被替换为了 com.mysql.jdbc.Driver ,这样也就实现了动态配置属性值的解析

这样解释一下之后是不是就好理解一些了呢?下面我们再来看源码:

// XNode
public String evalString(String expression) {
    // 自己不解析,委托XPathParser去解析
    return xpathParser.evalString(node, expression);
}

// XPathParser
public String evalString(Object root, String expression) {
    // 先从标签中取出明文属性值
    String result = (String) evaluate(expression, root, XPathConstants.STRING);
    // 交由占位服务解析器,处理占位符,替换为真实配置值
    result = PropertyParser.parse(result, variables);
    return result;
}

处理思路一目了然,所以这也就能明白,为什么 MyBatis 选择自己额外封一层 XNode ,而不是直接用 javax 的那个 Node 了吧。

2.2.3 parseConfiguration

回到上面的 parse 方法中,parse 方法的本质,是解析 <configuration> 标签的内容,所以下面我们进入 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);
    }
}

呀,哎呀呀呀呀呀呀呀,这不就是把整个 MyBatis 全局配置文件,从头到尾顺序解析了一遍吗?这也太朴实无华了吧!OK ,那既然我们挖掘到了这个地方,下面我们就可以逐个来看了。

2.2.3.1 propertiesElement-解析properties

首先解析的是 <properties> 标签,这里面它就会解析内部定义的 <property> ,以及配置的 resource 、url 属性:(关键注释已标注在源码中)

private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
        // 先加载内部定义的<property>
        Properties defaults = context.getChildrenAsProperties();
        String resource = context.getStringAttribute("resource");
        String url = context.getStringAttribute("url");
        if (resource != null && url != null) {
            // 二者不可兼得
            // throw ex ......
        }
        if (resource != null) {
            defaults.putAll(Resources.getResourceAsProperties(resource));
        } else if (url != null) {
            defaults.putAll(Resources.getUrlAsProperties(url));
        }
        // 编程式加载的配置属性值
        Properties vars = configuration.getVariables();
        if (vars != null) {
            defaults.putAll(vars);
        }
        // 将配置属性值放入解析器、全局配置中
        parser.setVariables(defaults);
        configuration.setVariables(defaults);
    }
}

纵读整段源码,可以发现整个加载的流程,就是我们在上一章中解释的流程。而且通过源码的走读,我们也能明白为什么配置的优先级会是编程式的最高,properties 文件次之,配置文件内定义的最低了。

这个方法很简单,源码中也标注了注释,小册就不过多展开讲解了。

2.2.3.2 settingsAsProperties-加载配置项

下面是解析 <settings> 标签了,这个标签的解析涉及到 3 行代码:

    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    loadCustomLogImpl(settings);

也不难理解,这个操作很明显是将 <settings> 标签中的一行行配置,封装为一个 Properties ,然后额外处理一下 VFS 和 Log 组件。关于 VFS 的内容小册暂时不提,我们先来看看底层如何处理 Log 组件的配置:

private void loadCustomLogImpl(Properties props) {
    Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
    configuration.setLogImpl(logImpl);
}

嚯,这么简单的逻辑吗?它直接从配置中取到 logImpl 的配置值,然后设置到 Configuration 中就完事了,而这个 resolveClass 方法,其实是用别名解析的:

// BaseBuilder
protected <T> Class<? extends T> resolveClass(String alias) {
    if (alias == null) {
        return null;
    }
    try {
        return resolveAlias(alias);
    } // catch ......
}

protected <T> Class<? extends T> resolveAlias(String alias) {
    return typeAliasRegistry.resolveAlias(alias);
}

而这些别名的注册,早在 Configuration 创建的时候,就全部初始化好了:

public Configuration() {
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

    // ......

    typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
    typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
    typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
    typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
    typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);

    // ......
}

so 我们在配置那些 setting 配置项的时候,可供填充的内容,其实是参考自这里 MyBatis 预定的别名(这也能解释为什么我们如果在 settings 中配置 log4j 不好使,必须配置 LOG4J 才可以)。

2.2.3.3 typeAliasesElement-注册类型别名

OK ,下面一个要解析的是 typeAliases 标签了,这里面我们知道可以用 <package> 直接扫描,也可以使用 <typeAlias> 直接声明指定类型的别名。底层针对这两种情况分别做了处理:

private void typeAliasesElement(XNode parent) {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // 处理package的包扫描指定别名
            if ("package".equals(child.getName())) {
                String typeAliasPackage = child.getStringAttribute("name");
                // 注意这里调用的是registerAliases注册一组
                configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
            } else {
                // 处理typeAlias标签的逐个定义
                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);
                }
            }
        }
    }
}

注意在源码中,如果是处理 <package> 标签声明的包扫描,此处调用的方法也不一样了!进入到 registerAliases 方法中,我们会发现,这里会使用一个 ResolverUtil 的工具类,来扫描所有类(父类是 Object ),扫描完成后,在下面的 for 循环中,判断这些类是否为普通的类(非接口、非匿名内部类、非内部类),是则注册别名。

public void registerAliases(String packageName) {
    registerAliases(packageName, Object.class);
}

public void registerAliases(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    // 上面传入的是Object
    // 注意这个扫描动作是全层次扫描,会扫描到子包
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> 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);
        }
    }
}

注意一点!这里面有一个全层次包扫描的动作!所以配置的 package 实际上是扫描的指定包及其子包下的所有类,并将他们全部注册 alias 别名。

2.2.3.4 pluginElement-注册插件

接下来注册的是 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).getDeclaredConstructor().newInstance();
            // 拦截器的属性赋值
            interceptorInstance.setProperties(properties);
            configuration.addInterceptor(interceptorInstance);
        }
    }
}

整体下来,它就是简单的把拦截器创建出来,注册进全局 Configuration 中。当然我们要意识到的一件事:拦截器是 MyBatis 自行创建的,如果我们要用 Spring 整合 MyBatis ,并且想让 Spring 管理 MyBatis 的拦截器,似乎不太现实

2.2.3.5 注册一堆Factory

下面的 3 行代码,注册的都是一些 Factory :

    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));

这 3 种 Factory 的注册,在底层几乎是一模一样(以 ObjectFactory 为例):

private void objectFactoryElement(XNode context) throws Exception {
    if (context != null) {
        String type = context.getStringAttribute("type");
        Properties properties = context.getChildrenAsProperties();
        ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();
        factory.setProperties(properties);
        configuration.setObjectFactory(factory);
    }
}

可以发现,跟上面初始化拦截器的逻辑几乎一模一样吧!所以这几个逻辑咱也都知道一下就 OK 。

2.2.3.6 settingsElement-应用配置项

接下来的动作是应用之前初始化的配置项,这个方法那是又长又宽(每行代码都好长啊!实在不想全部贴过来),所以我们只截取其中几行代码象征性的看一眼就行:

private void settingsElement(Properties props) {
    // ......
    configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
    configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
    configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
    // ......
}

emmmm ,得了,这不就是把全局配置文件中的那些 <settings> 都应用进全局的 Configuration 对象中嘛,那这也没什么神秘的了,小伙伴们快速扫一遍就 OK 。

2.2.3.7 environmentsElement-数据源环境配置

下面是解析数据库环境配置的部分了,这里面因为存在嵌套标签 <transactionManager> 与 <dataSource> ,所以这里面的源码会稍微复杂一点(也仅仅是一点而已):

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            // 从default中取出默认的数据库环境配置标识
            environment = context.getStringAttribute("default");
        }
        for (XNode child : context.getChildren()) {
            String id = child.getStringAttribute("id");
            // 只会构造默认的数据库环境配置
            if (isSpecifiedEnvironment(id)) {
                // 解析transactionManager标签,生成TransactionFactory
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                // 解析dataSource标签,生成DataSource
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                DataSource dataSource = dsFactory.getDataSource();
                // 简单的建造器,构造出Environment对象
                Environment.Builder environmentBuilder = new Environment.Builder(id)
                    .transactionFactory(txFactory)
                    .dataSource(dataSource);
                configuration.setEnvironment(environmentBuilder.build());
            }
        }
    }
}

纵读整段源码,思路还是非常清晰的吧,它干的事情就是把事务管理器,以及数据源的配置加载好,构造进 Environment 对象中,完事。而解析 transactionManager 标签,以及 dataSource 标签的逻辑,跟上面解析那一堆 Factory 也都几乎完全一致,所以小册也不重复贴源码了,小伙伴们跟着 IDE 翻一下源码就好。

另外,Environment 的结构,也仅仅是组合了上面的 TransactionFactory 与 DataSource :

public final class Environment {
    private final String id;
    private final TransactionFactory transactionFactory;
    private final DataSource dataSource;
    
    // ......
}

2.2.3.8 databaseIdProviderElement-数据库厂商标识解析

接下来是 <databaseIdProvider> 标签的解析,这个东西我们说如果要用的话,就是声明 "DB_VENDOR" ,然后根据不同的数据库厂商,定义好别名即可。这个逻辑反映到源码中也不难理解:

private void databaseIdProviderElement(XNode context) throws Exception {
    DatabaseIdProvider databaseIdProvider = null;
    if (context != null) {
        String type = context.getStringAttribute("type");
        // awful patch to keep backward compatibility
        // 写 VENDOR 跟写 DB_VENDOR 一样
        if ("VENDOR".equals(type)) {
            type = "DB_VENDOR";
        }
        Properties properties = context.getChildrenAsProperties();
        databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance();
        databaseIdProvider.setProperties(properties);
    }
    Environment environment = configuration.getEnvironment();
    if (environment != null && databaseIdProvider != null) {
        String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
        configuration.setDatabaseId(databaseId);
    }
}

前半段就是正常的初始化 DatabaseIdProvider 类型的对象,关键是下面还有个 configuration.setDatabaseId(databaseId); 的动作,它是干什么呢?

仔细想一下,我们上一章提过,databasseIdProvider 是配合 mapper.xml 中定义 statement 用的,而源码走到这个位置的时候,还没有轮到 mapper.xml 解析,如果像我们上一章那样,在一个 mapper.xml 中定义了两个一样的 statement ,那后面轮到 mapper.xml 解析的时候,MyBatis 一看,呦,两个 statement 竟然 id 一模一样? 这不相当于撞车了吗?不行,这我得给他挂了!所以就会抛出 statement 的 id 相同的异常。但是!这逻辑不对啊,虽然两个 statement 的 id 一致,但 databaseId 不一样啊,一个 SqlSessionFactory 只能连一个数据源,而这个数据源的数据库厂商是确定的,所以这两个 statement 只能有一个可用,所以在解析 mapper.xml ,读取 statement 的时候还要比对一下 databaseId 呢!那这个 databaseId 从哪来呢?很明显可以从全局 Configuration 得到,那上面的这个 setDatabaseId 的动作就可以理解了吧!这个动作就是为了提前确定好数据源对应的数据库厂商,为后面解析 mapper.xml 做准备

2.2.3.9 typeHandlerElement-注册类型处理器

下面一个解析的是 TypeHandler 了,这里面的逻辑也是比较简单,要么包扫描,要么逐个注册,但最终都是注册到了 typeHandlerRegistry 中:

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 {
                // 逐个注册TypeHandler
                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);
                }
            }
        }
    }
}

逻辑很简单,小伙伴们自己翻一下源码就 OK 了,小册不再啰嗦。

2.2.3.10 mapperElement-解析mapper.xml

最后一个节点的解析是 mapper 了,这里面乍一看似乎不是很复杂:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // 包扫描Mapper接口
            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加载的mapper.xml
                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) {
                    // 处理url加载的mapper.xml
                    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) {
                    // 注册单个Mapper接口
                    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.");
                }
            }
        }
    }
}

但请不要忘记!这个方法完事之后,整个 MyBatis 的初始化工作就完成了!但此时 mapper.xml 还没有加载呢!所以这个环节也是相当重要的!源码中,可以发现,除了包扫描 Mapper 接口,以及单个注册 Mapper 接口之外,其余两个都是解析 mapper.xml 文件。至于解析 mapper.xml 的底层是如何处理,我们放到映射文件的讲解之后再展开讲解,这里我们先知道一点即可:mapper.xml 的解析是使用 XMLMapperBuilder 完成的


到此为止,整个 MyBatis 全局配置文件的加载就完成了,处理流程相对不算复杂

标签:解析,resource,String,配置文件,configuration,MyBatis,new,null,加载
来源: https://blog.csdn.net/song_hai_lei/article/details/121425207

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有