Mybatis随笔

Mybatis随笔(二) 配置文件加载

2020-03-17  本文已影响0人  sunyelw

一、概述

Mybatis配置文件加载的方式非常简单, 就是读取并解析配置文件然后放到一个指定的数据结构中

而纯Mybatis项目的配置文件就两种

项目配置文件, 配置数据库信息/基本属性等

SQL配置文件, 一般每个配置文件都对应一张表, 配置了具体执行SQL

Mybatis 在解析 mybatis-config.xml 时就会顺带着把 *Mapper.xml 给解析了, 这归功于 mybatis-config.xml<mappers /> 标签了

Mybatis初始化过程:
SqlSessionFactoryBuilder通过对配置文件的解析,把配置文件放入Configuration对象中,再以这个Configuration来构造SqlSessionFactory,这里就是DefaultSqlSessionFactory

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

所以解析的重点是这个Configuration类了。

二、解析

本流程不深入讲解 XNode / XpathParser 等基础解析

BaseBuilder属性

其中一个属性就是Configuration,所以各种解析类解析来解析去,其实都是设置这个Configuration,抓住这条主线,理解配置解析整个过程就没那么难了

就一个一个来看下吧

  1. 首先调用SqlSessionFactoryBuilder#build
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
  1. XMLConfigBuilder
public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // 开始解析配置文件
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      // 解析标签 properties
      propertiesElement(root.evalNode("properties"));
      // 解析标签 settings
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      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"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      // 解析标签 environments
      environmentsElement(root.evalNode("environments"));
      // 解析标签 databaseIdProvider 数据库厂商标识, 用于识别同项目中不同的数据库厂商(e.g. Mysql/Oracle)
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      // 解析标签 typeHandlers
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析标签 mappers
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

上面这些标签解析的源码没什么难懂的。也可以看到有哪些标签,而这些标签又有哪些属性,都能一一看到,就挑几个随便看看

private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      // 解析标签 <properties/> 为 Properties 对象
      Properties defaults = context.getChildrenAsProperties();
      String resource = context.getStringAttribute("resource");
      String url = context.getStringAttribute("url");
      if (resource != null && url != null) {
        throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
      }
      if (resource != null) {
        defaults.putAll(Resources.getResourceAsProperties(resource));
      } else if (url != null) {
        defaults.putAll(Resources.getUrlAsProperties(url));
      }
      // 将解析出来的 Properties 对象放入 configuration
      Properties vars = configuration.getVariables();
        if (vars != null) {
            defaults.putAll(vars);
        }
        parser.setVariables(defaults);
        configuration.setVariables(defaults);
    }
}

就两步
<1> 解析标签 <properties/>Properties 对象
<2> 将解析出来的 Properties 对象放入 configuration

主要分三步
<1>vfsImpl

loadCustomVfs(settings);

vfs是虚拟文件系统,主要是通过该配置可以加载自定义的虚拟文件系统应用程序,实现是一种单例
<2>logImpl

loadCustomLogImpl(settings);

指定日志实现
<3>其他配置

settingsElement(settings);
->
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
...

这是三个典型的工厂模式,实现都非常简单,就是确定这个类存在后构建对应的factory,然后将之放入configuration
<1>objectFactory 对象工厂,重写DefaultObjectFactory,还支持外部配置传参,对应方法实现为setProperties,此类可以自定义构造实例
<2>objectWrapperFactory 默认实现 DefaultObjectWrapperFactory
<3>reflectorFactory 默认实现 DefaultReflectorFactory,用于构造反射器

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        // 数据源唯一标识
        String id = child.getStringAttribute("id");
        // 初始化只添加默认数据源
        if (isSpecifiedEnvironment(id)) {
          // 构造 environment (TransactionFactory/DataSource)
          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);
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
}

<1>初始化只添加默认数据源,如果没有默认数据源会报错
<2>这里对 environment 赋值了后面马上用到

private void databaseIdProviderElement(XNode context) throws Exception {
    DatabaseIdProvider databaseIdProvider = null;
    if (context != null) {
        String type = context.getStringAttribute("type");
        // awful patch to keep backward compatibility
        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);
    }
}

<1>这个标签的作用是为了兼容不同数据库厂商,注意,这里不是说多数据源,而是MYSQL ORACLE 之类数据库厂商间的共存
<2>可以在SQL执行上添加一个databaseId来指定数据库厂商

<insert id="insert" parameterType="Account" databaseId="mysql">
...
<insert id="insert" parameterType="Account" databaseId="oracle">
...

不过这种并非动态切换,而是根据你配置的数据源是什么厂商,也就是说MYSQLORACLE 你只能配一个
<3>这个标签只接受一个type和N个<property>,属性里配置的就是数据库厂商的别名
<4>可见configuration里面存的是当前数据源的databaseId, 因为数据源是从environment里面取出来的,这个databaseId就是配置中对应的value,不过需要name写对数据库的productName,参考源码可以如下方式获取productName

try (InputStream is = Resources.getResourceAsStream(configPath)) {
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
    SqlSession sqlSession = factory.openSession();
    DatabaseMetaData metaData = sqlSession.getConnection().getMetaData();
    log.info("---- version: " + metaData.getDatabaseProductVersion());
    log.info("---- name: " + metaData.getDatabaseProductName());
} catch (Exception ignore) {
}

输出

---- version: 5.7.26
---- name: MySQL

mysql的正确配置应该是 (注意大小写)

<property name="MySQL" value="mysql" />

<5>最后就是,这个type其实是个别名或间写,比如源码中的VENDORDB_VENDOR了解一下就行

这个配置不再是往configuration中扔值了,而是换成了typeHandlerRegistry,代码就不贴了

这个是配置解析中最复杂的了,因为它里面包含了解析*Mapper.xml*Mapper.xml*Mapper.java 互相引用就加载注册

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
                // package - name
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                if (resource != null && url == null && mapperClass == null) {
                    // mapper - resource
                    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) {
                    // mapper - url
                    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
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    // only one
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

解析流程非常清晰,拿到mappers下的子节点,然后遍历处理
接下来我们一个一个看下具体实现,但在这之前我们需要明确两点
<1>子节点只有两种<mapper><package>
<2>如果子节点是<mapper>,那么其三个属性中只能有一个有值,两个以上有值的会报错
1.package - name / mapper - class

这两种配置实现本质一样,只不过一个是批量一个是单个,放一起讲了

--- Configuration ---

public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
}

--- MapperRegistry ---

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

传入Object.class 表示只要目录下所有类都要, 不做父类过滤

public void addMappers(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    // 找出 packageName 目录下是 superType 子类的
    // if (child.endsWith(".class")) { addIfMatching(test, child); }
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    // 遍历这些类进行 addMapper
    for (Class<?> mapperClass : mapperSet) {
        addMapper(mapperClass);
    }
}

到目前为止,拿到配置的包下面所有class文件,遍历调用addMapper(String),这个方法非常重要(后面几个方式也会用到),重点看一下

public <T> void addMapper(Class<T> type) {
    // 必须是接口 SQLMapper
    if (type.isInterface()) {
        // 避免重复注册
        // return knownMappers.containsKey(type);
        if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
        }
        boolean loadCompleted = false;
        try {
            knownMappers.put(type, new MapperProxyFactory<>(type));
            // It's important that the type is added before the parser is run
            // otherwise the binding may automatically be attempted by the
            // mapper parser. If the type is already known, it won't try.
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            // 解析
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}

这里有一个很重要的判断,hasMapper(type)源码我直接贴在注释上了,就是通过一个Map<Class<?>, MapperProxyFactory<?>>来判断是否已经处理(存在),很显然,方法内将当前处理的type放入了knownMappers
接下来又是一个解析,这里就好玩了

--- MapperAnnotationBuilder ---

public void parse() {
    String resource = type.toString();
    // 确保没有解析过
    // return loadedResources.contains(resource);
    if (!configuration.isResourceLoaded(resource)) {
        loadXmlResource();
        configuration.addLoadedResource(resource);
        assistant.setCurrentNamespace(type.getName());
        parseCache();
        parseCacheRef();
        Method[] methods = type.getMethods();
        for (Method method : methods) {
            try {
                // issue #237
                if (!method.isBridge()) {
                    parseStatement(method);
                }
            } catch (IncompleteElementException e) {
                configuration.addIncompleteMethod(new MethodResolver(this, method));
            }
        }
    }
    parsePendingMethods();
}

这里可以暂时只看两个方法,一个是loadXmlResource();,加载同级目录下同名 xml 配置,源码如下

private void loadXmlResource() {
    // Spring may not know the real resource name so we check a flag
    // to prevent loading again a resource twice
    // this flag is set at XMLMapperBuilder#bindMapperForNamespace
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
        // 加载同级目录下同名 xml 配置
        String xmlResource = type.getName().replace('.', '/') + ".xml";
        // #1347
        InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
        if (inputStream == null) {
            // Search XML mapper that is not in the module but in the classpath.
            try {
                inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
            } catch (IOException e2) {
                // ignore, resource is not required
            }
        }
        // 解析 *Mapper.xml
        if (inputStream != null) {
            XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
            xmlParser.parse();
        }
    }
}

而且最后在构造XMLMapperBuilder时设置了namespace

public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments, String namespace) {
    this(inputStream, configuration, resource, sqlFragments);
    this.builderAssistant.setCurrentNamespace(namespace);
}

XMLMapperBuilder # parse 方法我们等下跟mapper两个属性解析一起讲

还有一个是assistant.setCurrentNamespace(type.getName());,这个方法也设置了namespace

public void setCurrentNamespace(String currentNamespace) {
    if (currentNamespace == null) {
        throw new BuilderException("The mapper element requires a namespace attribute to be specified.");
    }
    if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) {
        throw new BuilderException("Wrong namespace. Expected '"
        + this.currentNamespace + "' but found '" + currentNamespace + "'.");
    }
    this.currentNamespace = currentNamespace;
}

这里两处用的都是type.getName() 当然一致,但其他方式设置时就需要注意这点。
总结一下

  1. 注册目录下所有class类,并加载解析同级目录下的同名XML文件
  2. 设置类名为namespace,因为这里已经是类名了所以肯定能加载到

package我们就解析到这,继续看下mapper的另外两种配置的实现

不知道有没有注意到,在一些解析步骤开始前,都会判断一下是否解析过。Mybatis初始化是单线程执行的,不存在并发执行那么为什么会发现重复解析的情况?

很简单,两种情况

  1. 防止重复配置,比如你配置了两个一模一样的路径
  2. 若解析包名,先注册class文件,然后又会找到xml文件进行解析,而解析xml文件时,又会将与之关联的class文件再解析一遍!

只是入口不同,一个从classxml,一个是从xmlclass,为了精简代码,加个容器字段加个判断就行了没必要再额外写一个解析方法

就是XMLMapperBuilder # parse 这个方法,XMLMapperBuilder这个类前面我们说过是专门处理SQL配置文件的,那么很明显<mapper url/resource>配置的都是 XML文件路径,一个相对,一个绝对

这两种方式的处理几乎是一模一样的,所以我们直接来看下方法XMLMapperBuilder # parse

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        // 解析 mapper 内部标签
        configurationElement(parser.evalNode("/mapper"));
        // 避免重复处理
        configuration.addLoadedResource(resource);
        // 处理 namespace
        bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

这里主要看两个方法configurationElement("/mapper")bindMapperForNamespace()

  1. configurationElement("/mapper") 全部标签解析
private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("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);
    }
}
@Override
@SuppressWarnings("unchecked")
public V put(String key, V value) {
    if (containsKey(key)) {
        throw new IllegalArgumentException(name + " already contains value for " + key
            + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
    }
    if (key.contains(".")) {
        final String shortKey = getShortName(key);
        if (super.get(shortKey) == null) {
            super.put(shortKey, value);
        } else {
            // 一次
            super.put(shortKey, (V) new Ambiguity(shortKey));
        }
    }
    // 二次
    return super.put(key, value);
}

// 取出方法名
private String getShortName(String key) {
    final String[] keyParts = key.split("\\.");
    return keyParts[keyParts.length - 1];
}

另外,方法名直接对应的StrickMap为其内部类Ambiguity

protected static class Ambiguity {
    final private String subject;
    public Ambiguity(String subject) {
        this.subject = subject;
    }
    public String getSubject() {
        return subject;
    }
}
  1. bindMapperForNamespace() 非常简单
private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
        Class<?> boundType = null;
        try {
            // 尝试对 namespace 进行类加载
            boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {
            //ignore, bound type is not required
        }
        if (boundType != null) {
            if (!configuration.hasMapper(boundType)) {
                // Spring may not know the real resource name so we set a flag
                // to prevent loading again this resource from the mapper interface
                // look at MapperAnnotationBuilder#loadXmlResource
                configuration.addLoadedResource("namespace:" + namespace);
                configuration.addMapper(boundType);
            }
        }
    }
}

由此可以看出,namespace属性如果没有配置成XML文件对应的实体接口会加载不到导致configurationloadedResources没有存下对应的namespace,可能有以下不可预测的情况

三、构造

配置文件解析完成后就可以构造SqlSessionFactory

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

返回的是SqlSessionFactory的默认实现DefaultSqlSessionFactory,至此配置文件的加载流程就结束了。

四、其他

Configuration 的校验是否存在都是通过其某个容器字段来判断,大致分两种

protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

MapperRegistry又有字段

private final Configuration config;
Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

注意,其内部也有一个Configuration
这个容器主要用来判断 Class 是否加载过,判断方式

if (configuration.hasMapper(Class<T> type)) {}
protected final Set<String> loadedResources = new HashSet<>();

这就很简单,一个不会重复的HashSet

  1. 通过命名空间判断是否加载 XML (namespace: + clazz.getName())
  2. 命名空间是否有效取决于是否能被类加载器加载

判断方式

if (configuration.isResourceLoade(String resource)) {}

本来以为能写很多的,结果写起来查漏补缺,堪堪写完这部分就已经十二点多了,我要泡杯奶粉反省一下自己,明天继续GAN ~

上一篇 下一篇

猜你喜欢

热点阅读