接下来我们就编码测试下。
1.首先定义一个Mapper:
/**
* @author: 君战
* @since: 2021-05-10
**/
public interface OrgRealNameMapper {
@Select("SELECT * FROM xxx WHERE id = #{id}")
OrgRealNameDO selectByPrimaryKey(Integer id);
@Select("SELECT * FROM xxx WHERE id = #{id} AND name = #{name}")
OrgRealNameDO selectByPrimaryKey(Integer id,String name);
}
然后启动(如何启动MyBatis,请查看官方文档,描述地很详细),查看控制台:
Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException:
### Error building SqlSession.
### The error may exist in com/xxx/spring/dao/OrgRealNameMapper.java (best guess)
### Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.lang.IllegalArgumentException: Mapped Statements collection already contains value for com.xxx.spring.dao.OrgRealNameMapper.selectByPrimaryKey. please check com/xxx/spring/dao/OrgRealNameMapper.java (best guess) and com/xxx/spring/dao/OrgRealNameMapper.java (best guess)
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
at org.apache.ibatis.session.SqlSessionFactoryBuilder.build(SqlSessionFactoryBuilder.java:80)
at org.apache.ibatis.session.SqlSessionFactoryBuilder.build(SqlSessionFactoryBuilder.java:64)
at com.xxx.spring.Application.startApp(Application.java:88)
at com.xxx.spring.Application.main(Application.java:49)
Caused by: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.lang.IllegalArgumentException: Mapped Statements collection already contains value for com.xxx.spring.dao.OrgRealNameMapper.selectByPrimaryKey. please check com/xxx/spring/dao/OrgRealNameMapper.java (best guess) and com/xxx/spring/dao/OrgRealNameMapper.java (best guess)
at org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration(XMLConfigBuilder.java:122)
at org.apache.ibatis.builder.xml.XMLConfigBuilder.parse(XMLConfigBuilder.java:99)
at org.apache.ibatis.session.SqlSessionFactoryBuilder.build(SqlSessionFactoryBuilder.java:78)
... 3 more
Caused by: java.lang.IllegalArgumentException: Mapped Statements collection already contains value for com.xxx.spring.dao.OrgRealNameMapper.selectByPrimaryKey. please check com/xxx/spring/dao/OrgRealNameMapper.java (best guess) and com/xxx/spring/dao/OrgRealNameMapper.java (best guess)
at org.apache.ibatis.session.Configuration$StrictMap.put(Configuration.java:992)
at org.apache.ibatis.session.Configuration$StrictMap.put(Configuration.java:948)
at org.apache.ibatis.session.Configuration.addMappedStatement(Configuration.java:746)
at org.apache.ibatis.builder.MapperBuilderAssistant.addMappedStatement(MapperBuilderAssistant.java:297)
at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.lambda$parseStatement$2(MapperAnnotationBuilder.java:358)
at java.util.Optional.ifPresent(Optional.java:159)
at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parseStatement(MapperAnnotationBuilder.java:300)
at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parse(MapperAnnotationBuilder.java:132)
at org.apache.ibatis.binding.MapperRegistry.addMapper(MapperRegistry.java:72)
at org.apache.ibatis.binding.MapperRegistry.addMappers(MapperRegistry.java:106)
at org.apache.ibatis.binding.MapperRegistry.addMappers(MapperRegistry.java:118)
at org.apache.ibatis.session.Configuration.addMappers(Configuration.java:815)
at org.apache.ibatis.builder.xml.XMLConfigBuilder.mapperElement(XMLConfigBuilder.java:367)
at org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration(XMLConfigBuilder.java:120)
... 5 more
这个异常是由谁抛出的呢?我们可以通过异常堆栈找到答案:
Configuration类的第992行。这个是MyBatis自己实现的Map-StrictMap,其继承于HashMap,并重写了相关的get和put方法。与JDK提供的Map行为不同,MyBatis自己实现的StrictMap,在进行put操作的时候,如果发现已经哈希表中已经存在相同的key就抛出异常。
那么这个StrictMap是用来存什么呢?为什么使用到了它呢?
要回答这个问题,就要追溯到SqlSessionFactoryBuilder的build方法。
// org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream)
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
在该build方法中,调用了重载的build方法。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 根据输入流以及environment和属性文件构建XMLConfigBuilder
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());// 重点是这里调用的XMLConfigBuilder的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.
}
}
}
在XMLConfigurationBuilder的parse方法中完成了MyBatis配置文件的解析。
// org.apache.ibatis.builder.xml.XMLConfigBuilder#parse
public Configuration parse() {
// 如果已经解析过,抛出异常。
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 重点!!!解析MyBatis配置文件的根节点<configuration>
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
在XMLConfigBuilder的parseConfiguration方法中完成了标签的解析。
// org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
// 从这里我们也能看出在MyBatis配置文件中各个标签的顺序。因为在代码中是按顺序解析的。
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
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);
}
}
从源码中我们也可以得出、、和这几个子标签的解析顺序。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 优先解析的是<package>标签,如果同时配置了<package>、<resource>、<url>、<class>,
// 那么只有<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");
// 优先解析<resource>,配置了<resource>就不能配置<url>以及<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();
// 其次解析<url>,如果配置了<url>,就不能配置<resource>和<class>
} 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();
// 最后才是解析<class>,配置了<class>,就不能配置<resource>和<url>
} else if (resource == null && url == null && mapperClass != null) {
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.");
}
}
}
}
}
这里我们以常用的标签解析来分析,在上面源码中获取到标签的属性值之后,调用了Configuration的addMappers方法。
// org.apache.ibatis.session.Configuration#addMappers(java.lang.String)
public void addMappers(String packageName) {
// 未做任何处理,直接调用MapperRegistry的addMappers方法
mapperRegistry.addMappers(packageName);
}
在MapperRegistry的addMappers方法中,调用重载的addMappers方法。
// org.apache.ibatis.binding.MapperRegistry#addMappers(java.lang.String)
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
在重载的addMappers方法中,首先通过ResolverUtil的find方法来找到packageName路径下的所有superType的子类,前面传递的superType为Object。我们都知道Java中的所有类(接口)均继承于Object,因此find方法的作用就是找到指定包路径下的所有类(接口)。
// org.apache.ibatis.binding.MapperRegistry#addMappers(java.lang.String, java.lang.Class<?>)
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方法处理
addMapper(mapperClass);
}
}
在addMapper方法中,通过创建MapperAnnotationBuilder实例来解析指定接口中方法上的、、等标签。
// org.apache.ibatis.binding.MapperRegistry#addMapper
public <T> void addMapper(Class<T> type) {
//如果扫描到的类不是接口跳过
if (type.isInterface()) {
// 判断传入的Class是否已处理过
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 保存到knownMappers,表示该Class已经处理过
knownMappers.put(type, new MapperProxyFactory<>(type));
// 解析接口方法上的@Select、@Insert、@Update、@Delete等标签
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 重点是这里调用的parse方法!!!
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
在MapperAnnotationBuilder的parse方法中,对接口中的方法进行了初步解析。
// org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse
public void parse() {
String resource = type.toString();
// 如果未加载过
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
// 解析接口中添加的@CacheNamespace注解,该注解的作用是启用MyBatis的二级缓存。
// 关于MyBatis的一级缓存、二级缓存后面会单开一篇文章来分析。
parseCache();
parseCacheRef();
//获取接口中的所有方法
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
//如果接口中的方法存在@Select或@SelectProvider并且存在@ResultMap注解
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
// 重点是这里调用的parseStatement方法
parseStatement(method);
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
MapperAnnotationBuilder的parseStatement方法就比较复杂了。之所以说它复杂,是因为它要解析MyBatis支持的可以在方法上添加的注解。
// org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parseStatement
void parseStatement(Method method) {
final LanguageDriver languageDriver = getLanguageDriver(method);
getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> {
// 重点,这个mappedStatementId是根据接口名 + “.” + 方法名拼接得到的。
final String mappedStatementId = type.getName() + "." + method.getName();
// 删除与本次分析无关代码...
}
String resultMapId = null;
if (isSelect) {
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
resultMapId = String.join(",", resultMapAnnotation.value());
} else {
resultMapId = generateResultMapName(method);
}
}
// 删除与本次分析无关的代码...
// 本次分析的重点是这里调用的addMappedStatement方法,
// 你敢相信这就是源码吗?多少代码规范告诉我们如果一个方法的参数多于三个就要封装为对象。
assistant.addMappedStatement(
mappedStatementId,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
// ParameterMapID
null,
parameterTypeClass,
resultMapId,
getReturnType(method),
resultSetType,
flushCache,
useCache,
// TODO gcode issue #577
false,
keyGenerator,
keyProperty,
keyColumn,
statementAnnotation.getDatabaseId(),
languageDriver,
// ResultSets
options != null ? nullOrEmpty(options.resultSets()) : null);
});
}
在MapperBuilderAssistant的addMappedStatement方法中,借助于MappedStatement的一个静态内部类Builder来构建MappedStatement,使用了构建器模式。
// org.apache.ibatis.builder.MapperBuilderAssistant#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来构建MappedStatement 。注意这里构建MappedStatement时传递的id是调用方法(MapperAnnotationBuilder#parseStatement)传递的。
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);
}
MappedStatement statement = statementBuilder.build();
// 兜兜转转又调用了Configuration的addMappedStatement方法。
configuration.addMappedStatement(statement);
return statement;
}
把目光回到Configuration的addMappedStatement方法。这已经接近我们本次分析的尾声了。
// org.apache.ibatis.session.Configuration#addMappedStatement
public void addMappedStatement(MappedStatement ms) {
// 在该方法中未作其他处理,直接调用mappedStatements的put方法。
// 注意这里的传递的Key为MappedStatement的id属性值。
mappedStatements.put(ms.getId(), ms);
}
put方法,这个方法名熟悉吗?从事Java开发的小伙伴都知道,put(K key,V value)、get(K key)这些方法出自JDK中鼎鼎大名的容器类Map接口,我们日常常用的是其实现类HashMap。那么这里调用的是HashMap的put方法吗?
这里实际调用的是MyBatis自己定义的StrictMap,这就和我们前面的分析对上了。至此,本次分析完毕!
// org.apache.ibatis.session.Configuration#mappedStatements
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
.conflictMessageProducer((savedValue, targetValue) ->
". please check " + savedValue.getResource() + " and " + targetValue.getResource());
总结
相信看完这篇博客的小伙伴,就知道为什呢使用MyBatis后,DAO层方法不能重载了。因为MyBatis将将接口中方法解析后构造成了MappedStatement对象后,在设置其id属性时,传递的值是接口名 + “.” + 方法名(Java中的方法重载规则是方法名相同,但参数类型或参数位置不同)。而在StrictMap的put方法中,会先判断容器中是否已包含相同key,如果已包含就抛出异常,而不是覆盖已有值。
其实解决这个问题很简单,只需要在设置MappedStatement的id属性值时,要把方法的参数类型作为参与项拼接上去。