Mybatis源码 - 初始化流程 加载解析配置文件

加载配置文件

调用Resources类的静态方法getResourceAsStream,将配置文件的地址传入,完成配置文件的加载。

java 复制代码
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

Resources类中 调用重载方法 传入配置文件的地址 ClassLoader传了null

使用ClassLoaderWrapper来对配置文件进行了加载

java 复制代码
public static InputStream getResourceAsStream(String resource) throws IOException {
    return getResourceAsStream(null, resource);
}

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;
}

ClassLoaderWrapper:类加载器包装器,用来简化类加载器获取、加载资源时的判断、异常处理等操作。

java 复制代码
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) {
    //遍历ClassLoader数组,通过指定或者默认的classLoader来读取文件
    for (ClassLoader cl : classLoader) {
        if (null != cl) {
            InputStream returnValue = cl.getResourceAsStream(resource);
            //某些类加载器 加载时需要在资源地址之前加一个"/",这里是一个重试机制
            if (null == returnValue) {
              returnValue = cl.getResourceAsStream("/" + resource);
            }
            if (null != returnValue) {
              return returnValue;
            }
        }
    }
    return null;
}

拓展:包装器模式

基本思想

在上面的源码中,关于类加载器相关的代码 封装了一个包装器 ClassLoaderWrapper,也称为装饰器模式,是一种结构型设计模式。

它允许在不改变原类文件和使用继承的情况下,动态的扩展一个对象的功能。它是通过创建一个包装类来实现的,包装类中包含原始对象的引用,以及提供一些额外的功能。

或者规范行为的是一个抽象类,包装器通过继承的方式 重写方法来增强逻辑。

包装器模式的使用方式 并没有一个标准的模板,多种使用方式 不管是接口、抽象类等,都是引用了包装器的思想,实现方式上大同小异。

Mybatis应用

例如 在Mybatis加载配置文件时,类加载器是一个抽象类

java 复制代码
public abstract class ClassLoader {}

这个抽象类有若干子类,这里是原本的类加载器的实现逻辑。

而类加载器的包装器 就是使用了成员变量 来保存这个抽象类的实例,区别在于这里 包装器(ClassLoaderWrapper)并未去直接继承类加载器(ClassLoader),而是只是封装了一些加载资源时,加载器的获取、加载过程、判断、异常处理等,用来简化使用。个人理解 这也是包装器的一种使用方式,延续了包装器模式的思想,但并不一定实现方式就有标准答案和模板。

java 复制代码
public class ClassLoaderWrapper {
    //默认类加载器
    ClassLoader defaultClassLoader;
    //系统类加载器
    ClassLoader systemClassLoader;

    //...
}

解析配置文件

通过调用SqlSessionFactoryBuilder的build方法,来完成配置文件的解析,创建SqlSessionFactory工厂对象。

java 复制代码
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

SqlSessionFactoryBuilder中存在多个build方法

java 复制代码
public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
}

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

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

public SqlSessionFactory build(InputStream inputStream, String environment, 
                               Properties properties) {
    //...
}

最后多个重载的方法都会调用到下面这个三个参数的build方法

java 复制代码
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        //1.创建XPathParser解析器对象 根据inputStream解析成了document对象 解析xml配置文件 2.创建了全局配置对象 Configuration
        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.
        }
    }
}

最后的build方法中,创建了DefaultSqlSessionFactory进行返回,同时将全局配置对象Configuration 继续进行了传递。

java 复制代码
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

构造XMLConfigBuilder

java 复制代码
public class XMLConfigBuilder extends BaseBuilder {
    public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
      //XPathParser 基于Java 的 XPath解析器,用于解析Mybatis中的配置文件
      this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
    }
    private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
      //Mybaits全局配置对象:Configuration,并通过TypeAliasRegistry注册了一些Mybatis内部类相关的别名
      super(new Configuration());
      ErrorContext.instance().resource("SQL Mapper Configuration");
      this.configuration.setVariables(props);
      this.parsed = false;
      this.environment = environment;
      this.parser = parser;
    }
}

XMLConfigBuilder的构造器中执行了下面的逻辑

  1. 创建了XPathParser解析器,用来解析Mybatis中的配置文件
java 复制代码
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
  //XPathParser 基于Java 的 XPath解析器,用于解析Mybatis中的配置文件
  this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
  1. 在XPathParser的构造器中 将传入的inputStream构建成了一个Document文档对象
java 复制代码
  public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
    commonConstructor(validation, variables, entityResolver);
    //解析xml文档为Document对象
    this.document = createDocument(new InputSource(inputStream));
  }
  1. XMLConfigBuilder调用了父类的构造方法,创建了Configuration对象,也就是Mybatis的全局配置对象。
java 复制代码
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
  //Mybaits全局配置对象:Configuration,并通过TypeAliasRegistry注册了一些Mybatis内部类相关的别名
  super(new Configuration());
  ErrorContext.instance().resource("SQL Mapper Configuration");
  this.configuration.setVariables(props);
  this.parsed = false;
  this.environment = environment;
  this.parser = parser;
}
  1. XMLConfigBuilder的父类,这里完成了全局配置对象的赋值。
java 复制代码
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();
  }
}
  1. Configuration的构造器中,声明了一系列的别名,这里是可以直接在xml文件中配置别名的关键
java 复制代码
public class Configuration {
    public Configuration() {
        typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
        typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
    
        typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
        typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
        typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
        //...
    }
    //...
}

XMLConfigBuilder.parse()

java 复制代码
  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //通过XPATH解析器 解析configuration根节点
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

parser.evalNode("/configuration") 去解析了配置文件,将根标签configuration里面的内容封装为了一个XNode对象

java 复制代码
  public XNode evalNode(String expression) {
    return evalNode(document, expression);
  }

然后通过调用parseConfiguration方法,去解析XNode里面所有的配置项,也就是根标签configuration里面所有的子标签。

java 复制代码
  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);
    }
  }

environmentsElement()方法

主要用来解析environments标签 封装数据源信息

通过指定的environment,创建了事务工厂对象、数据源对象,然后通过Environment.Builder 的 build方法,创建了Environment对象,封装到了全局配置对象Configuration中

java 复制代码
private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            //判断 如果调用build方法时,没有指定environment。这里获取到配置的默认值
            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);
                //通过Environment.Builder创建了Environment对象,封装到了全局配置对象Configuration中
                configuration.setEnvironment(environmentBuilder.build());
                break;
            }
        }
    }
}

mapperElement()方法

java 复制代码
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            //如果配置的是package 代表是mapper代理方式
            if ("package".equals(child.getName())) {
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                //反之 是单个mapper配置方式
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                /*
                    下面有三个判断 里边的判断方式都是互斥的。
                    这里是获取到映射文件的配置方式,根据不同的方式 通过不同的类来进行解析
                */
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
                        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                        mapperParser.parse();
                    }
                } else if (resource == null && url != null && mapperClass == null) {
                    ErrorContext.instance().resource(url);
                    try(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.");
                }
            }
        }
    }
}

上面17-34行,是获取到映射文件的配置方式,根据不同的方式 通过不同的类来进行解析。下面就重点说一下resource配置方式,对应的解析类就是XMLMapperBuilder

构建XMLMapperBuilder

这部分跟构建XMLConfigBuilder是基本相似的

java 复制代码
XMLMapperBuilder mapperParser = new XMLMapperBuilder(
    inputStream, configuration, resource, configuration.getSqlFragments());


public class XMLMapperBuilder extends BaseBuilder {
  public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
        configuration, resource, sqlFragments);
  }

  private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    super(configuration);
    this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
    this.parser = parser;
    this.sqlFragments = sqlFragments;
    this.resource = resource;
  }
}

XMLMapperBuilder.parse()

java 复制代码
public void parse() {
    //首先进行重复加载的判断
    if (!configuration.isResourceLoaded(resource)) {
        //通过configurationElement方法来解析mapper标签
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

configurationElement方法中,对不同的标签来进行解析

java 复制代码
private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        //会对namespace有一个判断,如果namespace为空 会抛出异常
        if (namespace == null || namespace.isEmpty()) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
}
buildStatementFromContext()方法

主要看一下buildStatementFromContext方法,也就是用来解析select、insert、update、delete四个增删改查标签的方法。

java 复制代码
private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}

buildStatementFromContext方法:通过的XMLStatementBuilder的parseStatementNode方法 来进行进一步的解析。

java 复制代码
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}
parseStatementNode()方法

该方法 完成对select、insert、update、delete四个增删改查标签的解析。

java 复制代码
  public void parseStatementNode() {
    //获取到sql的ID
    String id = context.getStringAttribute("id");
    //获取到数据源的ID
    String databaseId = context.getStringAttribute("databaseId");

    //关于数据源ID的判断
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    //获取到标签名称:select
    String nodeName = context.getNode().getNodeName();
    //用来指定:当前执行的数据库操作,是增删改查哪个类型
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    //创建了SqlSource,来完成 解析SQL、封装SQL语句、处理入参信息
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    //构建MappedStatement对象,过程中完成了statementId的拼接
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }
相关推荐
P.H. Infinity19 分钟前
【SpringCloud】01-远程调用
后端·spring·spring cloud
xjjeffery1 小时前
文件和目录
linux·c语言·后端
林九生1 小时前
【Golang】(推荐项目)Go后端工程项目
开发语言·后端·golang
掘金-我是哪吒2 小时前
springboot第74集:设计模式
java·spring boot·后端·spring·设计模式
计算机学姐3 小时前
基于SpringBoot+Vue的旅游攻略平台管理系统
java·vue.js·spring boot·后端·intellij-idea·mybatis·旅游
向上的车轮5 小时前
ASP.NET Zero是什么?适合哪些业务场景?
后端·asp.net·多租户支持
knoci7 小时前
【Go】-基于Gin框架的IM通信项目
开发语言·后端·学习·golang·gin
高高要努力9 小时前
SpringBoot日志集成-LogBack
spring boot·后端·logback
Pandaconda11 小时前
【计算机网络 - 基础问题】每日 3 题(二十七)
开发语言·经验分享·笔记·后端·计算机网络·面试·职场和发展
Pandaconda11 小时前
【计算机网络 - 基础问题】每日 3 题(二十四)
开发语言·经验分享·笔记·后端·计算机网络·面试·职场和发展