Mybatis原理剖析之Configuration(二)


Mybatis包含两类至关重要的XML配置文件,分别为mybatis-config.xml和mapper.xml。其中mybatis-config.xml全局唯一,mapper.xml可以配置多个。

mybatis-config.xml

一般整个项目只包含一个mybatis-config.xml,它主要负责配置一些全局的配置信息以及mapper.xml路径,具体标签如下

标签名作用
properties通常配置数据库连接信息,如driver、url、用户名、密码等。该元素属性值可以动态替换
settings配置 MyBatis 框架运行时的一些行为,如缓存、延迟加载、结果集控制、执行器、分页设置、命名规则等一系列控制性参数,子标签为setting
typeAlias配置类的别名信息
typeHandlers类型处理器
objectFactory对象工厂
plugins拦截器,拦截目标分别包含StatementHandler、ParameterHandler、ResultSetHandler和Executor
environments环境信息,包含本地、测试、线上环境
databaseIdProvider数据库厂商
mappersmapper.xml路径

Mybatis通过加载mybatis-config.xml将标签包含信息,映射为Configuration对象,mybatis-config.xml由XMLConfigBuilder这个类负责加载。具体加载过程如下

XMLConfigBuilder

加载过程比较简单,主要包括加载XML、解析、创建对象、将对象设置到Configuration。下面主要介绍加载XML和解析两个阶段,因为创建对象、将对象设置到Configuration两个阶段包含在解析流程里面。

加载XML

Mybatis在new XMLConfigBuilder对象的时候,通过构造方法加载XML,并生成对应的DOM对象。

代码如下

public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    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;
}

参数解析

  • inputStream : mybatis-config.xml对应IO流。
  • environment : 数据库环境信息,可以动态替换mybatis-config.xml中environment值。
  • props : 数据库连接信息,可以动态替换mybatis-config.xml中properties值。

流程分析

通过this()调用重载构造函数完成XMLConfigBuilder对象构造。注意第一个参数new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()),XPathParser是一个XML解析器,接下来所有的DOM操作都是通过这个XPathParser来进行的,我们来看一下它的构造过程。

public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
	commonConstructor(validation, variables, entityResolver);
	this.document = createDocument(new InputSource(inputStream));
}

private Document createDocument(InputSource inputSource) {
    try {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setValidating(validation);
      factory.setNamespaceAware(false);
      factory.setIgnoringComments(true);
      factory.setIgnoringElementContentWhitespace(false);
      factory.setCoalescing(false);
      factory.setExpandEntityReferences(true);
      DocumentBuilder builder = factory.newDocumentBuilder();
      builder.setEntityResolver(entityResolver);
       
        ..此处省略..
      
      return builder.parse(inputSource);
    } catch (Exception e) {
      throw new BuilderException("Error creating document instance.  Cause: " + e, e);
    }
}

流程分析

XPathParser构造函数调用createDocument()方法,生成document对象,并赋值给XPathParser的成员变量。具体document构造过程此处省略。

解析

解析过程,全部封装到了XMLConfigBuilder的parse()方法。

代码如下

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

流程分析

首先判断是否parsed,即该XML是否已经被解析过,如果被解析过,则抛出异常。如果没有被解析过,接着往下看,

首先设置parsed为true,表示已经解析过。然后调用parseConfiguration()方法解析configuration元素,并将结果返回。接下来看parseConfiguration实现。

代码如下

private void parseConfiguration(XNode root) {
      propertiesElement(root.evalNode("properties"));
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      settingsElement(root.evalNode("settings"));
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
}

参数解析

  • root : configuration元素

流程分析
没什么可说的,分别解析对应元素! 接下来我们一一解析!

解析properties

代码如下

private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
      Properties defaults = context.getChildrenAsProperties();
      String resource = context.getStringAttribute("resource");
      String url = context.getStringAttribute("url");
      if (resource != null && url != null) {
        throw new BuilderException("....");
      }
      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);
    }
}

流程分析

  • 首先判空,获取配置的property属性值defaults,获取url和resource属性,若url和resource属性值均不为空,则抛出异常。意思就是不能同时配置url和resource。
  • 将url或者resource对应的文件内容添加到defaults中。注:url或resource内容优先级大于property内容。
  • 接下来获取原variables内容vars,若vars内容不为空,添加到defaults。
    vars哪里来的呢 : 还记得XMLConfigBuilder构造函数吗,是以方法实参的形式传进来的。
    注 : vars内容优先级高于properties配置内容。
  • 将defaults设置到configuration,到此处,properties解析完毕。

解析typeAliases

typeAliases标签用户配置类的别名信息。

代码如下

private void typeAliasesElement(XNode parent) {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String typeAliasPackage = child.getStringAttribute("name");
          configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
        } else {
          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) {
           ...
          }
        }
      }
   }
}

流程分析

  • 首先,获取所有子标签typeAlias,并遍历。
  • 如果标签名为package,获取name属性,显然name为package路径,则调用configuration.getTypeAliasRegistry()的registerAliases批量注册别名。
  • 如果标签名不是package,则获取alias和type属性,type为类的全限定路径,alias为别名,然后根据type生成对应的Class对象,然后调用configuration.getTypeAliasRegistry()的registerAlias()方法注册单个别名。

接下来分析一下registerAliases和registerAlias方法。

registerAlias代码如下

//1
public void registerAlias(Class<?> type) {
    String alias = type.getSimpleName();
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
      alias = aliasAnnotation.value();
    } 
    registerAlias(alias, type);
	}
//2
public void registerAlias(String alias, Class<?> value) {
    if (alias == null) {
      throw new TypeException("...");
    }
    String key = alias.toLowerCase(Locale.ENGLISH);
    if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
      throw new TypeException("...");
    }
    TYPE_ALIASES.put(key, value);
}

流程分析

  • 若标签内未配置alias属性,或者配置了alias属性但值为空,则调用方法1。若不为空,则调用方法2。
  • 若调用方法1,则通过Class对象获取simpleName或者注解作为别名。优先级为注解大于simpleName。然后调用方法2。
  • 方法2,首先对别名判空,若空抛出异常。将别名和Class对象存入TypeAliasRegistry的TYPE_ALIASES。其实TYPE_ALIASES就是一个普通的HashMap。

registerAliases代码如下

 public void registerAliases(String packageName, Class<?> superType){
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
    for(Class<?> type : typeSet){
      if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
        registerAlias(type);
      }
    }
}

流程分析

  • 扫描包下所有类并调用registerAlias的方法1。到此处,typeAlias解析完毕。

解析plugin

由于plugin主要用于配置拦截器相关信息,我们会单独用一篇博客来介绍,此处略过。

解析objectFactory

作用不大,此处略过。

解析objectWrapperFactory

作用不大,此处略过。

解析settings

代码如下

private void settingsElement(XNode context) throws Exception {
	if (context != null) {
		Properties props = context.getChildrenAsProperties();
		MetaClass metaConfig = MetaClass.forClass(Configuration.class);
		for (Object key : props.keySet()) {
		if (!metaConfig.hasSetter(String.valueOf(key))) {
			throw new BuilderException("...");
		}
	}  		
	configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
	configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
	configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));

	...
}

流程分析
settings标签主要配置系统运行的一些行为控制信息,在configuration对象中有响应的成员变量与之对应。此处不再赘述解析过程。

解析typeHandler

由于typeHandler主要用于配置类型处理相关信息,我们会单独用一篇博客来介绍,此处略过。

解析mapper

代码如下

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        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");
          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) {
            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) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("...");
          }
        }
      }
   }
}

流程分析

  • 获取所有孩子节点,并遍历,若节点名称为package,则调用configuration的addMappers方法批量添加mapper。
  • 若节点名称不是package,则调用configuration的addMapper方法添加单个mapper。

configuration的addMapper方法代码如下

public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
}

public <T> void addMapper(Class<T> type) {
	if (type.isInterface()) {
		if (hasMapper(type)) {
		throw new BindingException("...");
		}
		boolean loadCompleted = false;
		try {
			knownMappers.put(type, new MapperProxyFactory<T>(type));
			MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
			parser.parse();
			loadCompleted = true;
		} finally {
			if (!loadCompleted) {
				knownMappers.remove(type);
			}
		}
	}
}

流程分析

  • configuration通过调用mapperRegistry的addMapper方法注册mapper。
  • mapperRegistry.addMapper : 首先判断Class必须是接口,其次判重。最后加入knownMappers,key为mapper.java对应的Class对象,值为一个MapperProxyFactory工厂对象。
  • 注意 : 此处存储的是mapper.java对应Class对象,而不是mapper.xml文件配置的相关信息。mapper.xml配置信息在XMLMapperBuilder的parse()方法进行解析。

mapper.xml

单个项目可以包含多个mapper.xml,一般来讲,一个mapper.xml对应一个表。mapper.xml包含标签如下

标签名作用
mappermapper.xml的根元素,包含一个属性namespace
cache给当前mapper添加二级缓存
sql可以被其他语句引入的可重用语句块
insert映射插入语句
delete映射删除语句
update映射更新语句
select映射查询语句

mapper.xml由XMLMapperBuilder这个类负责加载。具体加载过程如下

XMLMapperBuilder

mapper.xml由XMLMapperBuilder这个类的parse()方法,开始。

代码如下

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    ...
}

流程分析

  • 首先,判断文件是否被重复加载。
  • 然后解析mapper标签,代码为
    configurationElement(parser.evalNode("/mapper"));
  • 最后根据namespace属性值,绑定映射器。

接下来,开始分析configurationElement()方法。

代码如下

private void configurationElement(XNode context) {
    try {
	String namespace = context.getStringAttribute("namespace");
	if (namespace.equals("")) {
		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. Cause: " + e, e);
    }
}

流程分析

  • 首先,获取mapper标签的namespace属性,并判空,将这个值设置到 builderAssistant助手类。
  • 接下来,解析cacheRef、cache、parameterMap、resultMap、sql、select、insert、update、delete等标签。

在此,我们将讲解select、insert、update、delete等标签解析流程。

解析Select|insert|update|delete

代码如下

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

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

流程分析
有代码可知,通过遍历标签列表,每一个标签对应一个XMLStatementBuilder,然后调用
parseStatementNode()解析标签。

parseStatementNode()代码如下

public void parseStatementNode() {
    ..代码较长,此处省略.

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

流程分析
由于代码较长,我将其略掉了,略去代码部分可以简单理解为针对标签各个属性值进行了抽取。然后调用助手类的addMappedStatement()方法构建Statement并添加到configuration。

addMappedStatement代码如下

  public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      StatementType statementType,
      SqlCommandType sqlCommandType,
      Integer fetchSize,
      Integer timeout,
      String parameterMap,
      Class<?> parameterType,
      String resultMap,
      Class<?> resultType,
      ResultSetType resultSetType,
      boolean flushCache,
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang,
      String resultSets) {
    
    if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }
    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
    statementBuilder.resource(resource);
    statementBuilder.fetchSize(fetchSize);
    statementBuilder.statementType(statementType);
    statementBuilder.keyGenerator(keyGenerator);
    statementBuilder.keyProperty(keyProperty);
    statementBuilder.keyColumn(keyColumn);
    statementBuilder.databaseId(databaseId);
    statementBuilder.lang(lang);
    statementBuilder.resultOrdered(resultOrdered);
    statementBuilder.resulSets(resultSets);
    setStatementTimeout(timeout, statementBuilder);

   	..此处省略..
   
    configuration.addMappedStatement(statement);
    return statement;
}

流程分析
建造者模式构建MappedStatement.Builder对象,并调用configuration的addMappedStatement添加statement。

configuration的addMappedStatement代码如下

public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);
}

流程分析
configuration将MappedStatement放入mappedStatements,其实mappedStatements就是一个普通的HashMap。
注意 : 键为映射器的全路径,即包名+Mapper类名。值为MappedStatement对象,封装了各种sql语句执行相关的信息。

至此,全部XML配置解析完毕。

温馨提示

  • 如果您对本文有疑问,请在评论部分留言,我会在最短时间回复。
  • 如果本文帮助了您,也请评论,作为对我的一份鼓励。
  • 如果您感觉我写的有问题,也请批评指正,我会尽量修改。
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页
实付 9.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值