2.1.构建 XmlConfigBuilder 准备解析 XML 文件的基础环境

优质
小牛编辑
125浏览
2023-12-01

前面说过,XmlConfigBuilder对象主要用于解析mybatis的全局配置文件,并以此来获取Configuration对象的实例。

XmlConfigBuilder对外暴露了六个构造方法,这六个方法根据mybatis配置文件的输入流类型可以分为两大类:

分别负责处理字节流形式的配置文件和处理字符流形式的配置文件。

// 处理字节流类型的mybatis配置
XMLConfigBuilder(InputStream inputStream);
XMLConfigBuilder(InputStream inputStream, String environment);
XMLConfigBuilder(InputStream inputStream, String environment, Properties props);

// 处理字符流类型的mybatis配置
XMLConfigBuilder(Reader reader);
XMLConfigBuilder(Reader reader, String environment);
XMLConfigBuilder(Reader reader, String environment, Properties props);

在具体的代码实现上,这两类构造方法的调用最终会根据文件流的不同落在下面这两个方法上(因为前面我们传入的是字节流,所以此处触发的是第一个方法):

/**
 * @param inputStream  配置文件输入流
 * @param environment 当前环境ID
 * @param props       用户自定义属性
 */
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    this(new XPathParser(
                    inputStream,/*XML文件输入流*/
                    true, /*开启验证*/
                    props, /*Property参*/
                    new XMLMapperEntityResolver() /*XML实体解析器*/
            ) /*新建一个XML解析器的实例*/
            , environment/*环境对象*/
            , props /*Property参数*/
    );
}
/**
 * @param reader      配置文件输入流
 * @param environment 当前环境ID
 * @param props       用户自定义属性
 */
public XMLConfigBuilder(Reader reader, String environment, Properties props) {
    this(new XPathParser(reader,
                    true/*是否进行DTD校验*/
                    , props /*参数*/
                    , new XMLMapperEntityResolver() /*XML对应的DTD文件的寻找器*/
            )/*构建一个XPath解析器*/
            , environment /*当前环境*/
            , props /*参数*/
    );
}

在上面这两个方法内,不管传入的是字节流还是字符流,mybati都会使用传入的配置文件输入流创建一个XPathParser对象,然后利用XPathParser对象调用XMLConfigBuilder的私有构造方法XMLConfigBuilder(XPathParser parser, String environment, Properties props)来完成XmlConfigBuilder对象的构造过程。

/**
 * @param parser      XPath解析器
 * @param environment 环境ID
 * @param props       用户自定义属性
 */
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    // 初始化Configuration对象的同时注册部分别名以及语言驱动
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    // 设置变量
    this.configuration.setVariables(props);
    // 初始化解析标签
    this.parsed = false;
    // 初始化环境容器
    this.environment = environment;
    // 初始化Xml地址解析器
    this.parser = parser;
}

XPathParser是mybatis内部定义的一个基于XPath语言来读取XML文档中的数据的解析工具类。

XPath即为XML路径语言(XML Path Language),它是一种用来确定XML文档中某部分位置的语言。

mybatis在创建XPathParser对象时,传入了一个new XMLMapperEntityResolver()对象,该对象的作用我们后面再说,现在我们先忽略掉他以及XPathParser对象的具体实现,继续看XmlConfigBuilder的构造过程。

new XPathParser(reader,
                true/*是否进行DTD校验*/
                , props /*参数*/
                , new XMLMapperEntityResolver() /*XML对应的DTD文件的寻找器*/
        )

XmlConfigBuilder的私有构造方法中,mybatis会调用Configuration的无参构造方法生成一个Configuration对象,并将Configuration对象传入XmlConfigBuilder的父类BaseBuilder的构造方法中完成BaseBuilder的初始化工作。

// 初始化Configuration对象的同时注册部分别名以及语言驱动
super(new Configuration());

Configuration的无参构造方法内包含了一些基础数据的准备工作,其中主要包括注册常用的类型别名,注册脚本语言驱动器和配置默认的脚本语言驱动器。

public Configuration() {
    // 注册别名

    // 注册JDBC别名
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    // 注册事务管理别名
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
    // 注册JNDI别名
    typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
    // 注册池化数据源别名
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    // 注册为池化的数据源
    typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
    // 注册永久缓存
    typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
    // 注册先入先出的缓存
    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    // 注册最近最少使用缓存
    typeAliasRegistry.registerAlias("LRU", LruCache.class);
    // 注册软缓存
    typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
    // 注册弱缓存
    typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
    // 注册处理数据库ID的提供者
    typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
    // 注册基于XML的语言驱动
    typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
    //注册静态语言驱动(通常无需使用)
    typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
    // 注册Sl4j日志
    typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    //注册Commons日志
    typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
    //注册log4j日志
    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
    //注册log4j2日志
    typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
    //注册jdk log日志
    typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
    //注册标准输出日志
    typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
    //注册无日志
    typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
    // 注册CGLIB
    typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
    // 注册JAVASSIST
    typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
    // 默认使用XML语言驱动
    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    // 支持原始语言驱动
    languageRegistry.register(RawLanguageDriver.class);
}

!! 需要留意的是,在这里mybatis定义了默认的脚本语言驱动器是别名为XMLXMLLanguageDriver

我们之前说过XMLConfigBuilder对象是BaseBuilder的子类,在BaseBuilder里有三个final修饰的属性:

/**
 * Mybatis配置信息
 */
protected final Configuration configuration;
/**
 * 类型别名注册表
 */
protected final TypeAliasRegistry typeAliasRegistry;
/**
 * 类型转换处理器注册表
 */
protected final TypeHandlerRegistry typeHandlerRegistry;

他们的初始化赋值操作发生在BaseBuilder的构造方法中:

public BaseBuilder(Configuration configuration) {
      // 保持引用
      this.configuration = configuration;
      // 从Mybatis配置中同步过来类型别名注册表
      this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
      // 从Mybatis配置中同步过来类型处理器注册表
      this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
  }

在上面的这个构造方法中,BaseBuilder缓存了对Configuration对象的引用,同时把自身的typeHandlerRegistryTypeAliasRegistry属性指向了Configuration对象的typeHandlerRegistryTypeAliasRegistry属性。

public class Configuration {
  ...
/**
    * 类型处理器注册表
    */
   protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
   /**
    * 类型别名注册表,主要用在执行SQL语句的出入参以及一些类的简写
    */
   protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
   ...
 }
  • 类型处理器:点击了解mybatis类型处理器

    无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。

在调用BaseBuilder的构造方法之后,XMLConfigBuilder会将用户自定义的变量集合传递给Configuration对象,让其保存起来,供后面解析配置时使用。

// 缓存用户定义的变量
this.configuration.setVariables(props);

之后重置解析标记,将其状态修改为未进入解析操作,表示允许调用parse()方法:

// 初始化解析标签
this.parsed = false;

之后记录当前使用的环境信息,便于后面解析配置时使用:

// 初始化环境容器
this.environment = environment;

然后,保存当前使用的Xml地址解析器:

// 初始化Xml地址解析器
 this.parser = parser;

到这,就完成了XMLConfigBuilder对象的构造过程,除了在构造方法中涉及到的几个属性外,XMLConfigBuilder还有一个常量属性localReflectorFactory,该属性在代码中硬编码为DefaultReflectorFactory,他的作用后面我们会学到,先不管他:

/**
 * 用于创建{@link org.apache.ibatis.reflection.Reflector}对象的工厂
 */
private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();

现在我们先回过头来继续看XMLConfigBuilder依赖的XPathParser对象。

XPathParser的构造方法有很多,除了需要用户传入对应的XML文档之外,还可以接收一些其他可选传的参数。

在这里,传入的XML文档可以有多种形式,可以是XML的地址链接,输入流,甚至是Document对象都可以。

可选传的参数有三个:

  • 第一个是boolean类型的validation,该参数表示是否对XML文档进行DTD校验,默认值为false

  • 第二个参数是Properties类型的variables,该参数表示用户自定义的属性对象,这些属性对象可以在整个配置文件中以占位符的形式被读取和使用。

  • 第三个参数是EntityResolver类型的entityResolver对象,该参数的作用是为调用方寻找DTD验证文件。

    EntityResolver这个参数我们前面有提到过,他在XmlConfigBuilder构造方法中被硬编码为XMLMapperEntityResolver类型的对象。

    new XPathParser(reader,
                    true/*是否进行DTD校验*/
                    , props /*参数*/
                    , new XMLMapperEntityResolver() /*XML对应的DTD文件的寻找器*/
            )/*构建一个XPath解析器*/
    

    他的代码比较简单,定义了六个常量:

    /**
     * IBATIS 全局配置文件的systemId(用于兼容IBATIS)
     */
    private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd";
    /**
     * IBATIS Mapper配置文件的systemId(用于兼容IBATIS)
     */
    private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd";
    /**
     * MYBATIS 全局配置文件的systemId
     */
    private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd";
    /**
     * MYBATIS Mapper配置文件的systemId
     */
    private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd";
    /**
     * MYBATIS全局配置文件对应的DTD文件
     */
    private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
    /**
     * MYBATIS MAPPER 配置文件对应的DTD文件
     */
    private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";
    

    同时实现了EntityResolverresolveEntity方法,在该方法中,XMLMapperEntityResolver会为传入的systemId匹配一个对应的DTD验证文件:

    /**
      * 从本地获取DTD文件
      *
      * @param publicId 公共标志符号
      * @param systemId 系统标志符
      * @return DTD
      */
     @Override
     public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
         try {
             if (systemId != null) {
                 String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
                 if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
                     // mybatis全局配置文件`config.xml`对应的DTD文件
                     return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
                 } else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {
                     // mybatis数据映射配置文件`*Mapper.xml`对应的DTD文件
                     return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);
                 }
             }
             return null;
         } catch (Exception e) {
             throw new SAXException(e.toString());
         }
     }
    

    点击了解systemId和publicId

    • systemId: 系统标志符,通常用于直接指出对应DTD资源的实际位置,往往是应用程序范围内唯一的。
    • publicId: 公共标志符,是一个全球唯一的标志,通常与特定的DTD关联,间接的指出DTD文件的位置,通常用于跨多个应用的场景。

    其实EntityResolver及其实现类XMLMapperEntityResolver是一个标准的策略模式实现。

    策略模式是一种常见的行为性设计模式,作用:我们可以定义一系列的算法,并将这些算法封装起来,使他们可以互相替换,策略模式让算法可以独立于使用他们的客户端而独立变化.

因为我们在创建sqlSessionFactory对象时,传入的是配置文件的字节流,所以这里实际调用XPathParser的构造方法是XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver)(其余的构造方法和该方法代码类似):

/**
 * XPathParser
 *
 * @param inputStream    XML文件输入流
 * @param validation     是否进行DTD验证
 * @param variables      用户自定义的属性对象,这些属性对象可以在整个配置文件中以占位符的形式被读取和使用
 * @param entityResolver 自定义寻找DTD验证文件的解析器
 */
public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
    // 调用通用的构造方法
    commonConstructor(validation, variables, entityResolver);
    // 将用户传入的XML文件流转换为Document对象
    this.document = createDocument(new InputSource(inputStream));
}

这个构造方法中的代码可以分为两个部分,一个是用于初始化属性的通用方法:commonConstructor,另一个是用来解析获取XML文件对应的Document对象的方法:createDocument.

其中commonConstructor方法的内容相对比较简单,唯一值得一提的就是为xpath属性硬编码赋值了XPath类型的对象实例,代码如下:

  /**
     * 公共构造参数,主要是用来配置DocumentBuilderFactory
     *
     * @param validation     是否进行DTD校验
     * @param variables      属性配置
     * @param entityResolver 实体解析器
     */
    private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
        // 是否进行DTD校验
        this.validation = validation;
        // 配置XML实体解析器
        this.entityResolver = entityResolver;
        //  属性配置
        this.variables = variables;

        // 初始化XPath
        XPathFactory factory = XPathFactory.newInstance();
        this.xpath = factory.newXPath();
    }

createDocument方法则主要是根据当前的配置解析XML文件获取对应的Document对象:

    /**
     * 根据输入源,创建一个文本对象
     *
     * @param inputSource 输入源
     * @return 文本对象
     */
    private Document createDocument(InputSource inputSource) {
        // important: this must only be called AFTER common constructor
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            // 是否验证被解析的文档
            factory.setValidating(validation);
            // 是否提供对XML命名空间的支持
            factory.setNamespaceAware(false);
            //忽略注释
            factory.setIgnoringComments(true);
            //忽略空白符
            factory.setIgnoringElementContentWhitespace(false);
            //是否解析CDATA节点转换为TEXT节点
            factory.setCoalescing(false);
            //是否展开实体引用节点
            factory.setExpandEntityReferences(true);

            DocumentBuilder builder = factory.newDocumentBuilder();
            // 配置XML文档的解析器,现在主要是XMLMapperEntityResolver
            builder.setEntityResolver(entityResolver);
            // 配置错误处理器
            builder.setErrorHandler(new ErrorHandler() {
                @Override
                public void error(SAXParseException exception) throws SAXException {
                    throw exception;
                }

                @Override
                public void fatalError(SAXParseException exception) throws SAXException {
                    throw exception;
                }

                @Override
                public void warning(SAXParseException exception) throws SAXException {
                }
            });
            // 解析出DOM树
            return builder.parse(inputSource);
        } catch (Exception e) {
            throw new BuilderException("Error creating document instance.  Cause: " + e, e);
        }
    }

在执行完这两个方法之后,XPathParser的构造过程就已经结束了,这时候XPathParser不仅持有Document对象还持有一个Xpath访问器,接下来XPathParser的很多操作都会使用Xpath来读取Document对象中的内容。

下面是XPathParser对象的属性定义:

public class XPathParser {
    /**
     * XML对象
     */
    private final Document document;
    /**
     * 是否进行DTD验证
     */
    private boolean validation;
    /**
     * DTD实体解析器
     */
    private EntityResolver entityResolver;
    /**
     * 用户自定义的属性对象,这些属性对象可以在整个配置文件中以占位符的形式被读取和使用。
     */
    private Properties variables;
    /**
     * XML地址访问器
     */
    private XPath xpath;
  }

到这里,XMLConfigBuilder对象及其依赖的对象的构造过程基本上就完成了,接下来就是执行解析mybatis配置文件的工作了。