Mybatis核心配置文件加载流程详解

Mybatis核心配置文件加载流程详解

本文将介绍MyBatis在配置文件加载的过程中,如何加载核心配置文件、如何解析映射文件中的SQL语句以及每条SQL语句如何与映射接口的方法进行关联。

映射配置文件

在介绍核心配置文件加载流程前,先给出一个简单的MyBatis的配置文件mybatis-config.xml如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><settings><setting name="cacheEnabled" value="true"/><setting name="localCacheScope" value="STATEMENT"/></settings><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="com.mysql.cj.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/local?allowPublicKeyRetrieval=false"/><property name="username" value="root"/><property name="password" value="root"/></dataSource></environment></environments><mappers><mapper resource="mapper/UserMapper.xml"/><mapper resource="mapper/OrderMapper.xml"/></mappers>
</configuration>

上面的XMl配置文件中,包含了一些核心标签,比如

  • <settings> 标签:用于配置 MyBatis 的全局设置,包括设置缓存、懒加载、日志实现、数据库元信息获取方式等。您可以在这里定义各种全局设置,以定制 MyBatis 的行为。
  • <environments> 标签:用于配置 MyBatis 的数据库环境,包括数据源信息、事务管理器等。这个标签允许您配置多个环境,每个环境可以包含一个或多个数据源,以及相应的事务管理器。
  • <mappers> 标签:用于指定 MyBatis 映射器(Mapper)接口或映射文件的位置。在这个标签中,您可以列出要使用的 Mapper 接口或 XML 映射文件,MyBatis 将会加载这些映射器并将其配置为可用的映射器。

<mappers>标签中,有下面两种子节点,标签分别为<mapper><package>,这两种标签的说明如下所示:

  • <mapper>:该标签有三种属性,分别是resource、url和class,且在同一个标签中,只能设置其中一个。其中,resource和url属性均是通过告诉MyBatis映射文件所在的位置路径来注册映射文件,前者使用相对路径(相对于classpath,例如上面的mapper/UserMapper.xml),后者使用绝对路径。 class属性是通过告诉MyBatis映射文件对应的映射接口的全限定名来注册映射接口,此时要求映射文件与映射接口同名且同目录。
  • <package>:通过设置映射接口所在包名来注册映射接口,此时要求映射文件与映射接口同名且同目录。

上面列出了一些概念性的点,接下来我们通过源码来分析MyBatis是如何来加载和解析上面的配置文件的,并在源码中找到上面概念的佐证。

加载映射文件的源码分析

在读取配置文件时,我们通常会使用Resources.getResourceAsStream方法,利用字节输入流来读取配置文件。接下来,利用SqlSessionFactoryBuilderbuild方法,来读取该输入流,并将文件内容进行解析,加载到MyBatis中全局唯一的Configuration配置类中,从而完成配置文件的加载。代码如下所示:

InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);SqlSession sqlSession = sqlSessionFactory.openSession();

SqlSessionFactoryBuilder是一个建造者,其提供了共计9个重载的build()方法用于构建SqlSessionFactory,这9个build()方法可以分为三类,概括如下:

  • 基于配置文件字符流构建SqlSessionFactory
  • 基于配置文件字节流构建SqlSessionFactory
  • 基于Configuration类构建SqlSessionFactory

下面以基于配置文件字节流构建SqlSessionFactory的过程为例,对整个读配置文件的流程进行说明。上面的示例中调用的build() 方法最终会进入到下面的方法中:

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {try {// XMLConfigBuiler会解析配置文件并生成Configuration类XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);// 调用入参为Configuration的build()方法构建SqlSessionFactory并返回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.}}
}

可以发现,对配置文件的解析是在XMLConfigBuilderparse()方法中的。先来看一下上面创建的XMLConfigBuilder实例:

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

可以看到XMLConfigBuilder对XML的解析是依靠XPathParser,而XPathParser是MyBatis提供的基于JAVA XPath的解析器。同时,XMLConfigBuilder通过继承BaseBuilder,在内部维护了一个Configuration,通过XPathParser解析得到的配置属性均会保存在Configuration中。
另外,在上面的new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()方法中,MyBatis会借助于DOMParser 来将配置文件的输入流解析成Document来表示。

public Document parse(InputSource is) throws SAXException, IOException {if (is == null) {throw new IllegalArgumentException(DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN,"jaxp-null-input-source", null));}if (fSchemaValidator != null) {if (fSchemaValidationManager != null) {fSchemaValidationManager.reset();fUnparsedEntityHandler.reset();}resetSchemaValidator();}// 利用DOMParser将配置文件字节输入流解析成Document对象 domParser.parse(is);Document doc = domParser.getDocument();domParser.dropDocumentReferences();return doc;
}

回到上面的parse()方法中:

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

其中,parser.evalNode("/configuration")就是从上面解析好的 Document对象中,利用xPath获取到configuration标签(根节点),然后将<configuration标签下的内容传入parseConfiguration方法中。该方法中其实也就是相同的利用xPath来获取指定标签下的内容:

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"));databaseIdProviderElement(root.evalNode("databaseIdProvider"));typeHandlerElement(root.evalNode("typeHandlers"));// 获取mappers标签下的内容mapperElement(root.evalNode("mappers"));} catch (Exception e) {throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);}
}

可以看到,上面的方法会分别获取配置文件中的各种类型的标签内容,然后执行相应的处理逻辑,实际上也是我们通用的一种处理XML文件的解析处理过程。接下来,我们选择mappers标签,来重点看看其中的处理逻辑,也就是mapperElement方法:

private void mapperElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) {// 处理package子节点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) {// 处理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 {// 如果设置了多个属性,则提示报错 throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");}}}}
}

上面的处理逻辑很清晰了,XNode parent即是<mappers>标签,首先利用getChildren方法来获取下面的子节点。判断如果是<package>子标签,则获取其name属性值,并调用configuration.addMappers进行处理。如果不是,则进入else分支,来处理<mapper>标签里的属性。 本文初介绍过<mapper>标签支持的属性值,且只能设置其中的一个,这里就找到了代码的佐证。

在上面的代码中可以发现,对于不同的标签和属性,分别有两种处理方式,一种是调用configuration.addMappers方法,另一种是调用mapperParser.parse(),接下来我们分别来看一下这两种方法的处理逻辑。

首先来看一下configuration.addMappers

public void addMappers(String packageName) {mapperRegistry.addMappers(packageName);
}/*** @since 3.2.2*/
public void addMappers(String packageName) {addMappers(packageName, Object.class);
}/*** @since 3.2.2*/
public void addMappers(String packageName, Class<?> superType) {ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();resolverUtil.find(new ResolverUtil.IsA(superType), packageName);Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();for (Class<?> mapperClass : mapperSet) {addMapper(mapperClass);}
}

层层调用addMappers方法,在上面第三个重载方法中,创建了一个ResolverUtil实例,ResolverUtil用来定位给定类路径中的可用且满足任意条件的类。 因此,resolverUtil.find(new ResolverUtil.IsA(superType), packageName) 实际上就是找到给定类路径packageName下的所有可用类:

public ResolverUtil<T> find(Test test, String packageName) {// 将packageName中的.转换成/String path = getPackagePath(packageName);try {// 扫描从所提供的包开始到子包的类List<String> children = VFS.getInstance().list(path);for (String child : children) {if (child.endsWith(".class")) {// 如果URL资源描述符以.class结尾,则判断是否加入到matches集合中addIfMatching(test, child);}}} catch (IOException ioe) {log.error("Could not read package: " + packageName, ioe);}return this;
}// 当且仅当提供的测试批准时,将所提供的完全限定类名指定的类添加到已解析类集中。
protected void addIfMatching(Test test, String fqn) {try {String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');// 获取类加载器ClassLoader loader = getClassLoader();if (log.isDebugEnabled()) {log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");}// 利用类加载器加载全限定类名指定的类Class<?> type = loader.loadClass(externalName);if (test.matches(type)) {// 如果test验证条件匹配后,则将该类加入到matches集合中// 这里的test.matches实际上就是判断type对应的类的父类是不是Object类matches.add((Class<T>) type);}} catch (Throwable t) {log.warn("Could not examine class '" + fqn + "'" + " due to a " +t.getClass().getName() + " with message: " + t.getMessage());}
}

上面我们花费了一定篇幅来细致地分析了addMappers方法中实现的核心逻辑,目前我们可以知道,resolverUtil.getClasses()方法就是获取到<package>标签中name属性指定的包名及其子包下的所有类,然后进行遍历,对每个类执行addMapper方法:

public <T> void addMapper(Class<T> type) {// 如果该类是一个接口类if (type.isInterface()) {// 判断knownMappers中是否已经有当前映射接口// Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(); // knownMappers是一个map存储结构,key为映射接口Class对象,value为MapperProxyFactory// MapperProxyFactory为映射接口对应的动态代理工厂if (hasMapper(type)) {throw new BindingException("Type " + type + " is already known to the MapperRegistry.");}boolean loadCompleted = false;try {// knownMappers中存储每个映射接口对应的动态代理工厂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来完成映射文件和映射接口中的Sql解析// 先解析映射文件,再解析映射接口MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);parser.parse();loadCompleted = true;} finally {if (!loadCompleted) {knownMappers.remove(type);}}}
}

上面代码中最重要的就是我们发现,MyBatis在解析配置文件的过程中,针对给定包名下的每个接口类,都生成了一个相应的MapperProxyFactory实例,并保存在knownMapeprs的HashMap中。 顾名思义,MapperProxyFactory就是映射接口的动态代理工厂,负责为对应的映射接口生成动态代理类。这里动态代理的生成逻辑我们暂且按下不表,先继续看一下上面对映射文件和映射接口中的SQL进行解析的MapeprAnnotationBuilder类,该类中包含了下面一些属性:

private final Configuration configuration;
private final MapperBuilderAssistant assistant;
private final Class<?> type;

可知,每个MapperAnnotationBuilder实例都会对应一个映射接口类,且持有一个全局唯一的Configuration类,目的是为了将parse方法中对映射配置文件的SQL解析丰富进该Configuration中。接下来,我们就继续来探究一下parse()方法中具体执行了什么:

public void parse() {String resource = type.toString();// 判断映射接口是否已经被解析过,没解析过才会继续往下执行if (!configuration.isResourceLoaded(resource)) {// 解析映射配置文件的内容,并丰富进Configuration中loadXmlResource();// 将当前映射接口添加到缓存中,以表示当前映射接口已经被解析过configuration.addLoadedResource(resource);assistant.setCurrentNamespace(type.getName());parseCache();parseCacheRef();// 遍历接口类中的所有方法,目的是为了处理接口方法中利用注解方式添加的SQL语句Method[] methods = type.getMethods();for (Method method : methods) {try {// issue #237if (!method.isBridge()) {parseStatement(method);}} catch (IncompleteElementException e) {configuration.addIncompleteMethod(new MethodResolver(this, method));}}}parsePendingMethods();
}

按照parse()方法的执行流程,会先解析映射配置文件XML中的配置SQL语句,然后再解析接口方法中利用注解方法配置的SQL语句。这里以解析映射文件为例,先来看一下loadXmlResource()方法的实现:

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#bindMapperForNamespaceif (!configuration.isResourceLoaded("namespace:" + type.getName())) {// 根据接口类的全限定类名,拼接上.xml来得到相应的映射配置文件// 这也就是为什么如果是<package>标签,需要映射配置文件和映射接口在同一目录String xmlResource = type.getName().replace('.', '/') + ".xml";// #1347InputStream 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}}if (inputStream != null) {XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());// 解析映射配置文件xmlParser.parse();}}
}

在上面的loadXmlResource方法中我们又找到了一处本文初介绍的<package>标签配置时规范的代码佐证,即为什么要求映射文件与映射接口同名且同目录。同样的,对于XML映射配置文件,MyBatis将其转换成字节输入流,并同样利用XMLMapperBuilder类进行解析。这里对XML文件的解析也是我们可以学习的一点,日后在其他地方对XML文件的解析都可以参照这一流程。
在这里插入图片描述

另外需要特别注意的一点是,如上图所示,解析核心配置文件和映射配置文件的解析类分别为XMLConfigBuilderXMLMapperBuilder,均继承自BaseBuilder。而BaseBuilder中持有全局唯一的Configuration,所以两者对配置文件的解析均会丰富进Configuration

继续进入xmlParser.parse()方法中,来看一下是如何对映射配置文件进行解析的:

public void parse() {if (!configuration.isResourceLoaded(resource)) {// 解析映射配置文件的mapper根标签configurationElement(parser.evalNode("/mapper"));configuration.addLoadedResource(resource);bindMapperForNamespace();}parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements();
}

继续看configurationElement的实现:

private void configurationElement(XNode context) {try {// 获取<mapper>标签的namespace属性String namespace = context.getStringAttribute("namespace");if (namespace == null || namespace.equals("")) {throw new BuilderException("Mapper's namespace cannot be empty");}builderAssistant.setCurrentNamespace(namespace);// 解析MyBatis一级/二级缓存相关的标签cacheRefElement(context.evalNode("cache-ref"));cacheElement(context.evalNode("cache"));// 解析<parameterMap>标签,生成ParameterMap并缓存到ConfigurationparameterMapElement(context.evalNodes("/mapper/parameterMap"));// 解析<resultMap>标签,生成ResultMap并缓存到ConfigurationresultMapElements(context.evalNodes("/mapper/resultMap"));// 将<sql>标签对应的SQL片段保存到sqlFragments缓存中sqlElement(context.evalNodes("/mapper/sql"));// 解析<selecct> 、<insert>、<update>和<delete>标签// 生成MappedStatement并缓存到ConfigurationbuildStatementFromContext(context.evalNodes("select|insert|update|delete"));} catch (Exception e) {throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);}
}

通过上面的方法可以发现,解析XML文件来说,实际上就是xPath获取到每个需要分析的标签,然后将各个子标签解析成相应的类。在MyBatis中,上面对每个子标签的解析结果,最后都会缓存到Configuration中。通常,在映射文件的<mapper>标签下,常用的子标签为<parameterMap><resultMap><select><insert><update><delete>。下面着重分析一下buildStatementFromContext方法,来看一下MyBatis是如何对上述标签进行解析的。

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) {// 每一个<select>、<insert>、<update>和<delete>标签均会被创建一个MappedStatement// 每个MappedStatement会存放在Configuration的mappedStatements缓存中// mappedStatements是一个map,键为映射接口全限定名+"."+标签id,值为MappedStatement  for (XNode context : list) {// 上面的每个标签都会创建一个XMLStatementBuilder来进行解析final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);try {statementParser.parseStatementNode();} catch (IncompleteElementException e) {configuration.addIncompleteStatement(statementParser);}}
}

对于每一个<select><insert><update><delete>标签,均会创建一个XMlStatementBuilder来进行解析并生成MappedStatement,同样,看一下XMlStatementBuilder的类图,如下所示:
在这里插入图片描述

XMLStatementBuilder中持有<select>,<insert>,<update>和<delete>标签对应的节点XNode,以及帮助创建MappedStatement并丰富进ConfigurationMapperBuilderAssistant类。下面看一下XMLStatementBuilderparseStatementNode() 方法。

public void parseStatementNode() {// 获取标签idString id = context.getStringAttribute("id");String databaseId = context.getStringAttribute("databaseId");if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}String nodeName = context.getNode().getNodeName();// 获取标签类型,例如SELECT,INSERT等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// 如果使用了<include>标签,则将<include>标签替换为匹配的<sql>标签中的sql片段// 匹配规则是在Configuration中根据namespace+"."+refid去匹配<sql>标签XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);includeParser.applyIncludes(context.getNode());// 获取输入参数类型String parameterType = context.getStringAttribute("parameterType");Class<?> parameterTypeClass = resolveClass(parameterType);// 获取LanguageDriver以支持实现动态sql// 这里获取到的实际上为XMLLanguageDriverString 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)// 获取KeyGeneratorKeyGenerator keyGenerator;String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);if (configuration.hasKeyGenerator(keyStatementId)) {keyGenerator = configuration.getKeyGenerator(keyStatementId);} else {// 如果缓存中获取不到,则根据useGeneratedKeys的配置决定是否使用KeyGenerator// 如果要使用,则MyBatis中使用的KeyGenerator为Jdbc3KeyGeneratorkeyGenerator = context.getBooleanAttribute("useGeneratedKeys",configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;}// 通过XMLLanguateDriver创建SqlSource,可以理解为sql语句// 如果使用到了<if>、<foreach>等标签进行动态sql语句的拼接,则创建出来的SqlSource为DynamicSqlSourceSqlSource 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并添加到Configuration中builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,resultSetTypeEnum, flushCache, useCache, resultOrdered,keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

parseStatementNode() 方法整体流程稍长,总结概括起来该方法做了如下几件事情。

  • 将<include>标签替换为其指向的SQL片段;
  • 如果未使用动态SQL,则创建RawSqlSource以保存SQL语句,如果使用了动态SQL(例如使用了<if>,<foreach>等标签),则创建DynamicSqlSource以支持SQL语句的动态拼接;
  • 获取<select>,<insert>,<update>和<delete>标签上的属性;
  • 将获取到的SqlSource以及标签上的属性传入MapperBuilderAssistantaddMappedStatement() 方法,以创建MappedStatement并添加到Configuration中。

MapperBuilderAssistant是最终创建MappedStatement以及将MappedStatement添加到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");}// 拼接出MappedStatement的唯一标识,规则是namespace + ". " + idid = applyCurrentNamespace(id, false);boolean isSelect = sqlCommandType == SqlCommandType.SELECT;MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType).resource(resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType).keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId).lang(lang).resultOrdered(resultOrdered).resultSets(resultSets).resultMaps(getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType).flushCacheRequired(valueOrDefault(flushCache, !isSelect)).useCache(valueOrDefault(useCache, isSelect)).cache(currentCache);ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);if (statementParameterMap != null) {statementBuilder.parameterMap(statementParameterMap);}// 创建MappedStatmentMappedStatement statement = statementBuilder.build();// 将MappedStatement添加到Configuration中configuration.addMappedStatement(statement);return statement;
}

至此,我们终于在这里发现了MyBatis是如何解析映射配置文件,生成MappedStatement,并将其加载到Configuration类中。实际上,解析<parameterMap>标签,解析<resultMap>标签的大体流程和上面基本一致,最终都是借助MapperBuilderAssistant生成对应的类(例如ParameterMapResultMap)然后再缓存到Configuration中,且每种解析生成的类在对应缓存中的唯一标识为namespace + “.” + 标签id

最后,回到本小结开头的XMLConfigBuilder中的mapperElement方法,目前我们已经将方法内部的逻辑大致探究清晰,主要的工作就是根据配置文件中的标签下的子标签的不同( 或 ),以及根据子标签的属性不同,获取到所配置位置的映射配置文件,再使用XMLMapperBuilder类进行统一解析。最后,将解析得到的结果封装成MappedStatement对象,添加到Configuration类。

更为具体的解析细节,大家有兴趣可以阅读源码进行深入的分析,最后,我们利用一个示意图来对上述的MyBatis加载配置文件的流程进行一个总结:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/215892.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ROS gazebo 机器人仿真,环境与robot建模,添加相机 lidar,控制robot运动

b站上有一个非常好的ros教程234仿真之URDF_link标签简介-机器人系统仿真_哔哩哔哩_bilibili&#xff0c;推荐去看原视频。 视频教程的相关文档见&#xff1a;6.7.1 机器人运动控制以及里程计信息显示 Autolabor-ROS机器人入门课程《ROS理论与实践》零基础教程 本文对视频教程…

Docker入门指南:从基础到实践

在当今软件开发领域&#xff0c;Docker已经成为一种不可或缺的工具。通过将应用程序及其依赖项打包成轻量级的容器&#xff0c;Docker实现了开发、测试和部署的高度一致性。本文将深入研究Docker的基本概念&#xff0c;并通过详细的示例代码演示如何应用这些概念于实际场景中。…

基于FPGA的视频接口之高速IO(光纤)

简介 对于高速IO口配置光纤,现在目前大部分开发板都有配置,且也有说明,在此根据自己的工作经验以及对于各开发板的说明归纳 通过高速IO接口,以及硬件配置,可以实现对于光纤的收发功能,由于GTX的速率在500Mbs到10Gbps之间,但通道高速io可配置光纤10G硬件,物理通完成,则…

掌握 Python sympy 库的高级计算技巧

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com Sympy是Python中一个强大的符号计算库&#xff0c;为数学和科学计算提供了丰富的功能。本文将深入介绍Sympy库的各项功能&#xff0c;并提供丰富的示例代码&#xff0c;以帮助大家更好地理解和应用这一工具。 S…

【论文精读】REACT: SYNERGIZING REASONING AND ACTING IN LANGUAGE MODELS

REACT: SYNERGIZING REASONING AND ACTING IN LANGUAGE MODELS 前言ABSTRACT1 INTRODUCTION2 REACT: SYNERGIZING REASONING ACTING3 KNOWLEDGE-INTENSIVE REASONING TASKS3.1 SETUP3.2 METHODS3.3 RESULTS AND OBSERVATIONS 4 DECISION MAKING TASKS5 RELATED WORK6 CONCLUSI…

SpringBoot整合ActiveMQ

SpringBoot整合ActiveMQ 文章目录 SpringBoot整合ActiveMQ下载与安装SpringBoot整合ActiveMQ导坐标改配置&#xff0c;默认的保存位置生产与消费消息实现监听类——实现消息自动消费监听器转发消息&#xff1a;流程性业务消息消费完转入下一个消息队列 下载与安装 https://act…

8、操作符重载

友元 可以通过friend关键字&#xff0c;把一个全局函数、另一个类的成员函数或者另一个类整体&#xff0c;声明为授权类的友元友元拥有访问授权类任何非公有成员的特权友元声明可以出现在授权类的公有、私有或者保护等任何区域且不受访问控制限定符的约束友元不是成员&#xf…

Qt + MySQL(简单的增删改查)

Qt编译MySql插件教程 帮助&#xff1a; SQL Programming QSqlDatabase 静态函数 1.drivers()&#xff0c;得到可以使用的数据库驱动名字的集合 [static] QStringList QSqlDatabase::drivers();2.addDatabase()&#xff0c;添加一个数据库实例 [static] QSqlDatabase QSql…

慢SQL诊断

最近经常遇到技术开发跑来问我慢SQL优化相关工作&#xff0c;所以干脆出几篇SQL相关优化技术月报&#xff0c;我这里就以公司mysql一致的5.7版本来说明下。 在企业中慢SQL问题进场会遇到&#xff0c;尤其像我们这种ERP行业。 成熟的公司企业都会有晚上的慢SQL监控和预警机制。…

Logstash输入Kafka输出Es配置

Logstash介绍 Logstash是一个开源的数据收集引擎&#xff0c;具有实时管道功能。它可以从各种数据源中动态地统一和标准化数据&#xff0c;并将其发送到你选择的目的地。Logstash的早期目标主要是用于收集日志&#xff0c;但现在的功能已经远远超出这个范围。任何事件类型都可…

FastAPI之响应模型

前言 响应模型我认为最主要的作用就是在自动化文档的显示时&#xff0c;可以直接给查看文档的小伙伴显示返回的数据格式。对于后端开发的伙伴来说&#xff0c;其编码的实际意义不大&#xff0c;但是为了可以不用再额外的提供文档&#xff0c;我们只需要添加一个 response_mod…

汽车服务行业分析:预计2028年将达到38亿元

在推进加快检验机构建设同时&#xff0c;综合评估检验机构数量、分布和检测能力&#xff0c;探索试点汽车 4S 店开展检验&#xff0c;提供维修、保养、车检一体化服务。汽车服务主要是指围绕汽车展开的一系列服务活动&#xff0c;包括维修、美容、金融等&#xff0c;除具有一般…

springboot3 liquibase SQL执行失败自动回滚,及自动打tag

一&#xff1a; 自动执行回滚&#xff0c; 已执行成功的忽略&#xff0c;新sql执行失败则执行新sql文件中的回滚sql pom.xml <dependency> <groupId>org.liquibase</groupId> <artifactId>liquibase-core</artifactId> <version>4.25.0&…

Appium 并行测试多个设备

一、前置说明 在自动化测试中&#xff0c;经常需要验证多台设备的兼容性&#xff0c;Appium可以用同一套测试运例并行测试多个设备&#xff0c;以达到验证兼容性的目的。 解决思路&#xff1a; 查找已连接的所有设备&#xff1b;为每台设备启动相应的Appium Server&#xff1b…

从手工测试进阶中高级测试?如何突破职业瓶颈...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、手工测试如何进…

物联网安全芯片ACL16 采用 32 位内核,片内集成多种安全密码模块 且低成本、低功耗

ACL16 芯片是研制的一款32 位的安全芯片&#xff0c;专门面向低成本、低功耗的应用领域&#xff0c; 特别针对各类 USB KEY 和安全 SE 等市场提供完善而有竞争力的解决方案。芯片采用 32 位内核&#xff0c;片内集成多种安全密码模块&#xff0c;包括SM1、 SM2、SM3、 SM4 算法…

权威认证!景联文科技入选杭州市2023年第二批省级“专精特新”中小企业认定名单

为深入贯彻党中央国务院和省委省政府培育专精特新的决策部署&#xff0c;10月7日&#xff0c;杭州市经济和信息化委员会公示了2023年杭州“专精特新”企业名单&#xff08;第二批&#xff09;。 根据工业和信息化部《优质中小企业梯度培育管理暂行办法》&#xff08;工信部企业…

【教3妹学编程-算法题】需要添加的硬币的最小数量

3妹&#xff1a;2哥2哥&#xff0c;你有没有看到新闻&#xff0c; 有人中了2.2亿彩票大奖&#xff01; 2哥 : 看到了&#xff0c;2.2亿啊&#xff0c; 一生一世也花不完。 3妹&#xff1a;为啥我就中不了呢&#xff0c;不开心呀不开心。 2哥 : 得了吧&#xff0c;你又不买彩票&…

最强文生图跨模态大模型:Stable Diffusion

文章目录 一、概述二、Stable Diffusion v1 & v22.1 简介2.2 LAION-5B数据集2.3 CLIP条件控制模型2.4 模型训练 三、Stable Diffusion 发展3.1 图形界面3.1.1 Web UI3.1.2 Comfy UI 3.2 微调方法3.1 Lora 3.3 控制模型3.3.1 ControlNet 四、其他文生图模型4.1 DALL-E24.2 I…

Navicat 技术指引 | 适用于 GaussDB 分布式的服务器对象的创建/设计

Navicat Premium&#xff08;16.3.3 Windows版或以上&#xff09;正式支持 GaussDB 分布式数据库。GaussDB分布式模式更适合对系统可用性和数据处理能力要求较高的场景。Navicat 工具不仅提供可视化数据查看和编辑功能&#xff0c;还提供强大的高阶功能&#xff08;如模型、结构…