上一篇只是简单的对mybatis的基本流程进行了,探查。
这次准备深入看一下上篇的 使用XMLConfigBuilder进行解析, mapper到底如何配置,注解的sql如何获取的?
(一)配置mapper的方式:
根据官方文档,可以找到一共有四种方式。
官方文档链接
那么这几种方式的优先级和配置如何解析和加载的?
(二)如何解析:
上片文章中已经提到,
2.1 解析主配置文件的主要方法:
栈调用:
org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream, java.lang.String, java.util.Properties)
org.apache.ibatis.builder.xml.XMLConfigBuilder#parse
org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
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"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
2.2 解析mappers
2.2.1、源码:
org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
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) {
//通过反射获取接口,将接口放到configuration
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.");
}
}
}
}
}
2.2.2 mappers中子标签的优先级和配置规则
根据代码可知,首先会判断mappers标签下,子元素是package还是mapper。
Package与mapper是不可同时存在的。下图可知:
Package子标签:
Package指定这个包下所有的接口都是持久层接口,或者就是使用指定一个个mapper。
Mapper子标签:
Mapper标签下:三种类型的路径指定属性(resource,class,url),且每种只能使用一个属性。
Resource:xml相对于本配置文件的相对路径
Class:接口的包名全路径
url:绝对路径(file://+xml文件的全路径)
规则:
三种属性不能配置在同一个标签内:
<mapper resource="mapper/BlogMapper.xml" class="com.study.mapper.MyMapper"/>
就会导致报错:
A mapper element may only specify a url, resource or class, but not more than one.
同一个文件,只能配置一次mapper,否则也会报错:
Mapped Statements collection already contains value for org.apache.ibatis.domain.blog.mappers.BlogMapper.selectOne. please check mapper/BlogMapper.xml and file:///F:\IDEAR\IdeaProjects\mybatis3\mybatis-3\src\test\resources\mapper\BlogMapper.xml
正确使用示例:
<mappers>
<!--<package name="com.study.mapper"/>-->
<mapper class="com.study.mapper.MyMapper"/>
<!--<mapper resource="mapper/BlogMapper.xml" />-->
<mapper url="file:///F:\IDEAR\IdeaProjects\mybatis3\mybatis-3\src\test\resources\mapper\BlogMapper.xml"/>
</mappers>
2.2.3 跟踪源码探查根源:
根据2.2.1的源码可知,
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
解析mapper时,会将mapper接口放到configuration。
DEBUG 在addMapper打断点:
对应的栈调用:
parse:126, MapperAnnotationBuilder (org.apache.ibatis.builder.annotation)
addMapper:72, MapperRegistry (org.apache.ibatis.binding)
addMapper:810, Configuration (org.apache.ibatis.session)
mapperElement:383, XMLConfigBuilder (org.apache.ibatis.builder.xml)
parseConfiguration:120, XMLConfigBuilder (org.apache.ibatis.builder.xml)
parse:99, XMLConfigBuilder (org.apache.ibatis.builder.xml)
build:78, SqlSessionFactoryBuilder (org.apache.ibatis.session)
build:64, SqlSessionFactoryBuilder (org.apache.ibatis.session)
main:42, MybatisMain (com.study.anno)
addMapper源码:
configuration的addmapper:mapperRegistry.addMapper(type);
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 {//放入mapper的动态代理工厂
knownMappers.put(type, new MapperProxyFactory<>(type));
/*解析前先将mapper的类型type(我这里是myMapper)
配置给解析器parser,这样会直接用这个类型去解析mapper接口。
否则会尝试去找对应的类型然后再解析。 */
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
跟踪parse(下面2.2.4会对parse方法的每一步进行分析:)
MapperAnnotationBuilder源码
这个构建者类,以静态代码块来初始化这些属性:
static {
SQL_ANNOTATION_TYPES.add(Select.class);
SQL_ANNOTATION_TYPES.add(Insert.class);
SQL_ANNOTATION_TYPES.add(Update.class);
SQL_ANNOTATION_TYPES.add(Delete.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(SelectProvider.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(InsertProvider.class);
SQL_PROVIDER_ANNOTAION_TYPES.add(UpdateProvider.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(DeleteProvider.class);
}
parse方法:
public void parse() {
String resource = type.toString();//interface com.study.mapper.MyMapper
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();//先尝试通过找到接口对应的映射文件xml,并解析
//将resource放到configuration 的loadedResources(HashSet <String>类型)
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
if (getSqlCommandType(method) == SqlCommandType.SELECT && method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
parseStatement(method);
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
2.2.4 parse中的核心方法:
① 首先执行的是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())) {
他会先根据接口路径com.study.mapper. MyMapper去拼接一个xml路径出来,去获取这个mapper的xml
com/study/mapper/MyMapper.xml。也就是说,你配置成接口注解的方式,他也会先去查有没有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 {
//通过项目路径没有获取对应的xml,再试着到resources下获取接口的xml
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
}
}
//无论上面的那种方式,只要找到就用xml方式:
if (inputStream != null) {
//最终都会构造XMLMapperBuilder
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
结论:
如果找不到xml才会去找注解,所以在mybatis中xml的优先级是高于注解方式的。
因此如果之前用的是xml,后来改成注解了,要将相应的mapper的映射xml文件删除或者让注解所在的mapper接口路径与xml路径不一致,否则会导致在接口上的注解时效。
这种结构或者Mymapper.xml在resources下:都会导致–>只要在主配置文件的mapper配置上了MyMapper的路径,无论主配置文件有没有配置,在Mymapper接口方法上有没有加了注解,都会导致xml有效,注解失效。
② configuration.addLoadedResource(resource);
对应的Configuration源码:
将上一步获取到的需要加载的mapper放到待加载集合loadedResources 中
protected final Set<String> loadedResources = new HashSet<>();
public void addLoadedResource(String resource) {
loadedResources.add(resource);
}
③ 查缓存
1、设置当前mapper的Namespace
assistant.setCurrentNamespace(type.getName());
type:interface com.study.mapper.MyMapper
2、获取CacheNamespace :
private void parseCache() {
//根据获取的这个Mapper,来获取对应的命名空间下的缓存
CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
if (cacheDomain != null) {
Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
Properties props = convertToProperties(cacheDomain.properties());
assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
}
}
3、获取CacheNamespaceRef 缓存:
private void parseCacheRef() {
CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
if (cacheDomainRef != null) {
Class<?> refType = cacheDomainRef.value();
String refName = cacheDomainRef.name();
if (refType == void.class && refName.isEmpty()) {
throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
}
if (refType != void.class && !refName.isEmpty()) {
throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
}
String namespace = (refType != void.class) ? refType.getName() : refName;
try {
assistant.useCacheRef(namespace);
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
}
}
}
对上面缓存类CacheNamespace和CacheNamespaceRef进行一下总结:
mybatis的缓存:
(1)为每一个Mapper分配一个Cache缓存对象(使用节点配置或者 @CacheNamespace注解 );
(2)多个Mapper共用一个Cache缓存对象(使用节点配置或者本文所提到的@CacheNamespaceRef注解);
④解析方法和sql
④-1、mapper方法解析和结果集解析源码:
//获取指定type对应的mapper的所有方法。
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
//将桥连方法和default方法直接跳过
//桥连方法https://blog.csdn.net/mhmyqn/article/details/47342577
continue;
}
//判断这个方法对应的注解是不是select,如果是就解析查询结果集
if (getSqlCommandType(method) == SqlCommandType.SELECT && method.getAnnotation(ResultMap.class) == null) {
//下面单独详解
parseResultMap(method);
}
try {
//下面单独详解
parseStatement(method);
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
//parseResultMap
private String parseResultMap(Method method) {
//这里我的返回对象是 com.study.bean.MyBean
Class<?> returnType = getReturnType(method);
Arg[] args = method.getAnnotationsByType(Arg.class);
Result[] results = method.getAnnotationsByType(Result.class);
TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class);
//resultMapId为 com.study.mapper.MyMapper.selectOne-int
String resultMapId = generateResultMapName(method);
//这里是将上面的结果集应用上,即赋值到Configuration的Result Maps collection
applyResultMap(resultMapId, returnType, args, results, typeDiscriminator);
return resultMapId;
}
拼接处每个方法的resultMapId ,作为key放到Configuration的resultMaps(是Result Maps collection)中。
④-2、sql解析与处理(${}、#{})
第一步 parseStatement(method);
void parseStatement(Method method) {
//获取方法的参数类型集合
Class<?> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method);
/*获取注解类型(本次是SELECT)和对应的sql(select * from MyBean where id = #{id}),并将sql中的${}#{}处理掉,后面我会单独写一下*/
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {
.....
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = !isSelect;
boolean useCache = isSelect;
......
if (isSelect) {
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
resultMapId = String.join(",", resultMapAnnotation.value());
} else {
resultMapId = generateResultMapName(method);
}
}
assistant.addMappedStatement(
......
languageDriver,
// ResultSets
options != null ? nullOrEmpty(options.resultSets()) : null);
}
}
第二步 从注解获取sql并解析(${})
getSqlSourceFromAnnotations:
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
try {
//获取注解的类型 这里我的是SELECT
Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
if (sqlAnnotationType != null) {
if (sqlProviderAnnotationType != null) {
throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
}
Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
/*sqlAnnotation.getClass().getMethod("value")
对应的值就是sql: select * from MyBean where id = #{id}*/
final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
} else if (sqlProviderAnnotationType != null) {
Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
}
return null;
} catch (Exception e) {
throw new BuilderException("Could not find value method on SQL annotation. Cause: " + e, e);
}
}
// buildSqlSourceFromStrings
private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver) {
//sql转成字符串作为参数,创建sqlSource
return languageDriver.createSqlSource(configuration, String.join(" ", strings).trim(), parameterTypeClass);
}
第三步 languageDriver.createSqlSource远程调用的是XMLLanguageDriver。
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// issue #3
if (script.startsWith("<script>")) {
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
// issue #127 script:select * from MyBean where id = #{id}
//configuration.getVariables()获取数据源信息(url,username,driver,password)
//解析sql,将#{}和${}分别使用标记器进行处理替换成? 先替换的${},再替换#{} 下面第四步详解
script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
//下面第五步详解
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
} else {
return new RawSqlSource(configuration, script, parameterType);
}
}
}
第四步 PropertyParser.parse(script, configuration.getVariables());
public static String parse(String string, Properties variables) {
//标记处理器 在这里会初始化多种标记处理器
VariableTokenHandler handler = new VariableTokenHandler(variables);
//如果sql中有${}标记,就进行处理解析
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
标记处理解析 GenericTokenParser
public String parse(String text) {//如果有${ ,就去${和}之间的内容返回。如果没有${ 直接将sql返回。
if (text == null || text.isEmpty()) {
return "";
}
// search open token ${
int start = text.indexOf(openToken);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// openToken被转义了. 删除反斜杠再继续.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();//sql的openToken的最后一位索引
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
get到一个点:
里面使用的append(char[] str, int offset, int len)是一种高效的方法,以后工作中替代substring方法。记录一下,写一个简单的例子:
StringBuffer buff = new StringBuffer("qwer");
// char array
char[] str = new char[]
{'a','s','d','f','g','h','j','k','l'};
buff.append(str, 3, 5);
//就是在buff的后面追加一个一些字符,字符从str数组中取,从索引为3开始取5个字符,追加到buff后面。
//结果是qwerfghjk。
org.apache.ibatis.builder.SqlSourceBuilder#parse
第五步 动态拼接sql:
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
} else {
return new RawSqlSource(configuration, script, parameterType);
}
textSqlNode.isDynamic():还是调用前面的标记处理解析器 ,替换调${}为?
new RawSqlSource(configuration, script, parameterType);
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
//对#{}标记进行处理
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
SqlSourceBuilder.parse:
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
//定义一个#{}标记处理器
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
//调用前面的处理器
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
这里的parse也是调用的标记处理解析器
到整理@select注解和对应的sql全部获取到了。
下次再研究如何获取执行器并进行执行的。