配置解析最后一篇,MyBatis
解析mapper
:
// <mappers>
// <mapper resource="com/test/demo/mapper/CountryMapper.xml"/>-
// <package name="com.test.demo.mapper"/>
// </mappers>
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");
//如果是以resource配置的
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
//读取resource流
InputStream inputStream = Resources.getResourceAsStream(resource);
//使用XMLMapperBuilder解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
//如果是以url配置的
else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
//读取url流
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
}
//如果是以mapperClass配置的
else if (resource == null && url == null && mapperClass != null) {
//直接读取class
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
//如果在一个节点中配置了多个,则抛出异常
//类似 <mapper resource="xx" url="xx"/>
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
没什么好说的,继续往下看
首先看处理
package
MapperRegistry###addMappers()
//Configuration.addMappers()内部调用的便是这个方法
public void addMappers(String packageName, Class<?> superType) {
//这里已经很熟悉了,通过VFS读取包中所有的类
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
//通过`IsA`过滤掉不符合要求的类
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
//处理获取到的类
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
这里和前面的代码对比可以发现,少了一段过滤所有匿名类,接口以及内部成员类.并不是不需要过滤,而且
Mapper
对应点Class
只需要接口即可,看后面的代码便能知道
MapperRegistry###addMappers()
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 {
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.
//创建注解解析器,用来解析接口上的通过注解配置的SQL
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
//解析
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
可以看到这里创建了一个
MapperAnnotationBuilder
来解析Mapper
接口上的注解,接口的注解分为两种:
第一种是类似
@Select()
这种直接将SQL
配置在接口中,这种方式的配置不灵活,所以我们暂时不分析,不过最后注册的机制可能和还是和XML
配置差不多第二种便是常用的参数注解
@Param
,这种需要简单看一看
MapperAnnotationBuilder###MapperAnnotationBuilder()
public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
//best guess ???
String resource = type.getName().replace('.', '/') + ".java (best guess)";
//获取class的加载路劲,创建Mapper组建助手
//这里传入的configuration已经初始化差不多了,因为`Mapper`解析被放在了最后
this.assistant = new MapperBuilderAssistant(configuration, resource);
this.configuration = configuration;
this.type = type;
}
这里可以看见,我们需要查看的方法主要应该就在
MapperBuilderAssistant
类中,我们首先看看MapperAnnotationBuilder
的parse()
方法
MapperAnnotationBuilder###parse()
public void parse()
//获取加载的接口的具体路径/对应Mapper的命名空间
String resource = type.toString();
//如果没有加载过,则加载
if (!configuration.isResourceLoaded(resource)) {
//加载对应的`xml`文件
loadXmlResource();
//在configuration对象中标记此命名空间已经加载完成
configuration.addLoadedResource(resource);
//设置加载助手的命名空间
assistant.setCurrentNamespace(type.getName());
//加载缓存/MyBatis中的一级缓存为一个Mapper一个缓存
parseCache();
//加载指定的共享缓存
parseCacheRef();
//解析方法接口中的方法的注解,比如@Param
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// 过滤掉所有的桥接方法
if (!method.isBridge()) {
//初始化Statement
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
//解析方法
parsePendingMethods();
}
上面的代码中有个
isBridge()
,那么isBridge()
是什么意思呢?其实就是用来判断一个方法是否是桥接方法。至于什么是桥接方法,这里简单说两句:我们都知道Java的泛型是通过擦除实现的,对于一个泛型接口,
public interface InterfaceA<T>{ void methodA(T t); }
如果某个类实现了这个接口,并且指定了泛型:
public class Imple implements InterfaceA<String>{ @override void methodA(String t){ } }
那么问题来了,经过编译以后,
InterfaceA<T>
中的方法经过编译后变成了methodA(Object a)
,但是Imple
中重载的方法为methodA(String t)
,这根本没有重载啊。。。于是
java
编译器在编译Imple
类的时候会自动生成一个桥接方法:public class Imple implements InterfaceA<String>{ @override void methodA(Object t){ methodA((String)t); } void methodA(String t){ } }
这便是桥接方法的由来.
接下来首先看加载Mapper XML
配置文件
MapperAnnotationBuilder###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#bindMapperForNamespace
// 首先通过命名空间检查是否已经加载过此资源
//Spring-MyBatis可以通过`Mapper-Scaner`的方式加载`mapper`
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
//通过类全限定名称查找相同目录下的xml文件
String xmlResource = type.getName().replace('.', '/') + ".xml";
//通过class获取此文件的流
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
//如果获取失败
if (inputStream == null) {
// Search XML mapper that is not in the module but in the classpath.
try {
//尝试通过classLoader再次获取流
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
//忽略抛出的异常,因为可以通过注解配置SQL
}
}
//如果成功获取到,则通过`XMLMapperBuilder`解析XML文件
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
第一,从
type.getName()
我们能明白MyBatis
中命名空间与类所在的包的对应关系。这也是官方文档中所要求的对应关系。第二,上面查找
XML
文件的过程中,首先使用的是class
的getResourceAsStream()
,没有找到才使用的classLoader#getResourceAsStream()
,区别在于class
查找之前会使用resolveName()
来解析路径,如果是相对路径,则解析绝对路径,再调用classLoader
加载第三,可以注意到上面查找
XML
文件的方式是通过ClassLoader
查找的,也就是说,不管你的XML
配置在哪里,只要是classPath
,都能被查找到,比如resource
,甚至是%JAVA_HOME%/jre/classes/
文件夹下都能被扫描到第四,
MyBatis
源码也标记了一个fix bug
,用于解决Java 9
之后class
和classLoader
加载权限的不同第五,这里可以看见,原生
MyBatis
只能加载class
全限定名称的同级目录下的XML mapper
,只有Spring-Mapper
才增加了Mapper
扫描的功能
记下来继续看解析XML
文件
XMLMapperBuilder###parse()
public void parse() {
//如果没有加载过再加载,防止重复加载
//和前面的功能一样,不知道为什么这行代码到处都是,有点像是为了集成到`Spring`中
//将原本的代码结构破坏了一样
if (!configuration.isResourceLoaded(resource)) {
//获取mapper节点
//<mapper>
//</mapper>
configurationElement(parser.evalNode("/mapper"));
//标记加载
configuration.addLoadedResource(resource);
//将namespace绑定在解析助手上
bindMapperForNamespace();
}
//重新加载那些因为有加载依赖而加载失败节点
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
XMLMapperBuilder###configurationElement()
private void configurationElement(XNode context) {
try {
//首先获取命名空间
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//验证命名空间是否和解析助手的命名空间一致
builderAssistant.setCurrentNamespace(namespace);
//解析cache-ref配置
cacheRefElement(context.evalNode("cache-ref"));
//解析cache配置
cacheElement(context.evalNode("cache"));
//解析参数类型配置(已经废弃,最好使用@param)
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//解析resultMap配置
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析sql代码段配置
sqlElement(context.evalNodes("/mapper/sql"));
//通过sql语句创建statement
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
builderAssistant.setCurrentNamespace(namespace);
代码很简单,public void setCurrentNamespace(String currentNamespace) { if (currentNamespace == null) { throw new BuilderException("The mapper element requires a namespace attribute to be specified."); } if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) { throw new BuilderException("Wrong namespace. Expected '" + this.currentNamespace + "' but found '" + currentNamespace + "'."); } this.currentNamespace = currentNamespace; }
大约就是如果
namespace
之前被赋值了,那么就检查传入的namespace
是否和期望的一致,如果不一致则报错。前面我们看过代码,第一次的传入在于通过
class#getName()
进行赋值,也就是类的全量名称
XMLMapperBuilder###cacheRefElement()
private void cacheRefElement(XNode context) {
if (context != null) {
//给configuration 设置联动缓存
//configuration 联动缓存是通过map配置的,也就是联动缓存只能额外配置一个
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
//创建缓存解析器
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
//尝试加载联合缓存
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
//如果加载失败,则留在后面重新加载
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
这里可以看到
MyBatis
是如何处理加载的先后顺序的。
cache-ref
有个问题就是解析namesapce
的先后问题,如果所引用的缓存在被引用的时候还没加载,那么一般的操作都是提前去加载,这样就会涉及到分析依赖问题,加载顺序问题等,比较麻烦。
MyBatis
就直接用了一个未完成集合解决了这个问题,加载的时候发现还需要引用的缓存还没有加载,就先不暂存起来,当加载完其他配置的时候,再尝试一下加载,很方便可以看下具体代码:
MapperBuilderAssistant###useCacheRef()
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
//加载成功标记
unresolvedCacheRef = true;
//从configuration对象中尝试获取联合的缓存
Cache cache = configuration.getCache(namespace);
//如果没有找到,说明可能当时这个缓存的XML还没有被解析
//抛出IncompleteElementException让上层处理,上层的处理就是稍后重试
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
//如果加载成功了,则直接使用这个缓存
currentCache = cache;
//标志加载成功
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
这里可以看到,联合缓存其实就是使用的同一个缓存
XMLMapperBuilder###cacheElement()
private void cacheElement(XNode context) {
if (context != null) {
//获取缓存的实现类,如果没有设置则为`PERPETUAL`
String type = context.getStringAttribute("type", "PERPETUAL");
//通过别名注册器获取class
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
//获取缓存清除算法,默认而最近最少使用算法
String eviction = context.getStringAttribute("eviction", "LRU");
//通过别名注册器获取算法使用的类
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
//获取缓存刷新间隔时间配置
Long flushInterval = context.getLongAttribute("flushInterval");
//获取缓存大小配置
Integer size = context.getIntAttribute("size");
//获取缓存是否为只读属性
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
//新配置?文档中并没有
boolean blocking = context.getBooleanAttribute("blocking", false);
//获取配置的属性节点
Properties props = context.getChildrenAsProperties();
//通过构建助手通过这些参数创建cache
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
MapperBuilderAssistant###useNewCache()
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
//将新建的cache传入给configuration
//configuration所维护的cache是一个map,key为namespace
configuration.addCache(cache);
currentCache = cache;
return cache;
}
看到这里发现了关于
cache-ref
的问题:
- 第一,是先加载的
cache-ref
,再加载的cache
,而且看代码,cache
中新建的cache
会将cache-ref
中获取到其他域名空间的缓存给替换掉,也就是说,同时配置cache
和cache-ref
会使cache-ref
失效?- 第二,我们可以看到
cache-ref
除了一行currentCache = cache
有点修改的意思外,其他包括返回值都没有被使用或者记录,而currentCache
也会被后续的解析cache
节点给覆盖掉,cache-ref
究竟是怎么解析的?疑问先保存起来,先继续看后面的代码。
未完待续
网友评论