MyBatis中Mapper接口是怎么和XML文件关联起来的

2年前 (2022) 程序员胖胖胖虎阿
184 0 0

目录

  • 1.前言
  • 2. `XML` 文件和 `Mapper` 接口的解析
    • 2.1.`XML` 文件的解析
    • 2.2. `Mapper` 接口的解析
  • 3. `XML` 文件信息和 `Mapper` 接口的使用
    • 3.1. `Mapper` 接口的使用
    • 3.2. `XML` 文件信息的使用
  • 4. 总结

1.前言

使用过 MyBatis 的,都知道它有 Mapper 接口和 Mapper.xml 文件,那么它们是如何关联起来的呢?,又是如何一起运作的呢?接下来,我们从源码级别来分析回答这个问题

2. XML 文件和 Mapper 接口的解析

2.1.XML 文件的解析

XMl 文件的解析过程太过复杂,在这里不再过多赘述,详细过程 在这里,我们先从 这里的 2.5 节点谈起,代码如下

public void parseStatementNode() {
  	// 省略所有的属性解析......
  	
  	// 将解析出来的所有参数添加到 mappedStatements 缓存
  	builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      	fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      	resultSetTypeEnum, flushCache, useCache, resultOrdered,
      	keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
 
// MapperBuilderAssistant.java
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");
  	}
 
  	// 1.将 id 填充上 namespace,id = namespace + id(方法名)
  	id = applyCurrentNamespace(id, false);
  	boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  	// 2.使用参数构建 MappedStatement.Builder
  	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);
  	}
  	// 3.使用 MappedStatement.Builder 构建 MappedStatement
  	MappedStatement statement = statementBuilder.build();
  	// 4.将 MappedStatement 添加到缓存
  	configuration.addMappedStatement(statement);
  	return statement;
}
  • statement:表示解析出来的 Mapper.xml 文件的所有参数信息
  • 上述方法做了两件事,构建 statement,然后将其添加进 mappedStatements 缓存中
  • mappedStatements 数据结构是一个 StrictMap,此处就是 Mapper 接口方法为什么不能重载的答案,参见文章
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");

2.2. Mapper 接口的解析

我们先从 这里的 2.6 节点谈起,代码如下

private void bindMapperForNamespace() {	
	// 拿到当前的 namespace,<mapper namespace="org.example.dao.UserMapper">
  	String namespace = builderAssistant.getCurrentNamespace();
  	if (namespace != null) {
    	Class<?> boundType = null;    	
    	try {
      		// 1.解析 namespace 对应的绑定类型
      		boundType = Resources.classForName(namespace);
    	} catch (ClassNotFoundException e) {}    
    	if (boundType != null && !configuration.hasMapper(boundType)) {
      		// 2.boundType 不为空,并且 configuration 还没有添加 boundType,
      		// 则将 namespace 添加到已加载列表,将 boundType 添加到 knownMappers 缓存
      		configuration.addLoadedResource("namespace:" + namespace);
      		configuration.addMapper(boundType);// addMapper() 方法调用如下
    	}
  	}
}
  
public <T> void addMapper(Class<T> type) {	
  	mapperRegistry.addMapper(type);// addMapper() 方法调用如下
}
 
public <T> void addMapper(Class<T> type) {
  	if (type.isInterface()) {
    	if (hasMapper(type)) {
      		throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    	}    	
    	boolean loadCompleted = false;
    	try {
      		// 将 namespace 对应的绑定类型放到 knownMappers 缓存中
      		knownMappers.put(type, new MapperProxyFactory<>(type));
      		MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      		parser.parse();
      		loadCompleted = true;      		
    	} finally {
      		if (!loadCompleted) {
        		knownMappers.remove(type);
      		}
    	}
  	}
}
  • 先看看 knownMappers 是个什么玩意,原来是一个 HashMap
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
  • 将解析过的 mapper.xml 文件的 namespace 的绑定类型 type 放到 knownMappers 缓存中
  • 这里的 type 可以理解为 Mapper 接口的类型,即 Mapper 接口

3. XML 文件信息和 Mapper 接口的使用

通过上面的分析,此时 MyBatis 已经完成了对 Mapper 接口和 XML 文件的解析工作,并且将它们分别添加进了各自的 Map 缓存中,那么它们是在哪里又取出来使用的呢?

3.1. Mapper 接口的使用

MapperRegistry 类中的 getMapper() 方法

@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
	// 获取已经解析完成的 Mapper 接口信息
	final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
    	// 实例化对象
      	return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      	throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
}
  • knownMappers.get():获取 namespace 的绑定类型,即 Mapper 接口类型信息
  • 在上述的 2.2. 节点中的 addMapper() 方法中有 knownMappers.put()
  • 在此时拿到 Mapper 接口类型很关键,因为马上就要创建 Mapper 接口的代理对象了,代理对象创建过程 在这里的 2.1.4.节点

3.2. XML 文件信息的使用

DefaultSqlSession 类中增删改查如下,具体代码的跟进过程 看这里 2.2.节点

@Override
public int insert(String statement, Object parameter) {
	// 调用下面的 update() 方法
  	return update(statement, parameter);
}
 
@Override
public int update(String statement, Object parameter) {
  	try {
    	dirty = true;
    	// 从 mappedStatements 缓存拿到对应的 MappedStatement 对象,执行更新操作
    	MappedStatement ms = configuration.getMappedStatement(statement);
    	return executor.update(ms, wrapCollection(parameter));
  	} catch (Exception e) {
    	throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
  	} finally {
    	ErrorContext.instance().reset();
  	}
}
 
@Override
public int delete(String statement, Object parameter) {
	// 调用上面的 update() 方法
  	return update(statement, parameter);
}
 
// select,以 executeForMany 为例
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
  	
  	List<E> result;
  	// 1.参数转换成sql命令参数
  	Object param = method.convertArgsToSqlCommandParam(args);
  	if (method.hasRowBounds()) {
    	RowBounds rowBounds = method.extractRowBounds(args);
    	// selectList() 方法在最下面
    	result = sqlSession.selectList(command.getName(), param, rowBounds);
  	} else {
    	// 2.执行查询操作,selectList() 方法在最下面
    	result = sqlSession.selectList(command.getName(), param);
  	}  	
  	// 3.处理返回结果
  	if (!method.getReturnType().isAssignableFrom(result.getClass())) {
    	if (method.getReturnType().isArray()) {
      		return convertToArray(result);
    	} else {
      		return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
    	}
  	}
  	return result;
}
 
@Override
public <E> List<E> selectList(String statement, Object parameter) {
	// selectList() 方法在最下面
  	return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
 
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {

  	try {
    	// 从 mappedStatements 缓存中拿到对应的 MappedStatement 对象,执行查询操作
    	MappedStatement ms = configuration.getMappedStatement(statement);
    	return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  	} catch (Exception e) {
    	throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  	} finally {
    	ErrorContext.instance().reset();
  	}
}

关键部分代码,在即将要执行 SQL 时,在从 StrictMap 的缓存中获取

// 参数 statement 是 String 类型的
MappedStatement ms = configuration.getMappedStatement(statement);

// Configuration 类中的 getMappedStatement() 方法如下
public MappedStatement getMappedStatement(String id) {
	// 调用下面的方法
    return this.getMappedStatement(id, true);
}

public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
    if (validateIncompleteStatements) {
      	buildAllStatements();
    }
    // 
	return mappedStatements.get(id);
}

// mappedStatements 的数据类型
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");

4. 总结

Mapper 接口是怎么和 XML 文件关联起来,最关键的部分

// 拿到当前的 namespace,<mapper namespace="org.example.dao.UserMapper">
// MyBatis 会解析为 "org.example.dao.UserMapper"
String namespace = builderAssistant.getCurrentNamespace();

// 解析为 namespace 对应的绑定类型
boundType = Resources.classForName(namespace);

MyBatis中Mapper接口是怎么和XML文件关联起来的

相关文章

暂无评论

暂无评论...