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);
  }
相关推荐
㳺三才人子3 小时前
初探 Flask
后端·python·flask·html
星栈独行3 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Java爱好狂.4 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易4 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
装不满的克莱因瓶4 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
ltl5 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
excel6 小时前
为什么我推荐使用 Termius:现代 SSH 工具的完整体验
前端·后端
卷毛的技术笔记6 小时前
Java后端硬核实战:用Spring AI Alibaba+Redis给LLM装上“超强记忆中枢”
java·人工智能·redis·后端·spring·ai·系统架构
IT_陈寒7 小时前
Java的Optional差点让我掉坑里,这几个坑你别踩
前端·人工智能·后端
子兮曰8 小时前
Harness 驾驭工程深度教程:从 AGENTS.md 到全链路 AI 编码基础设施
前端·后端·ai编程