配置解析最后一篇, 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
//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
只需要接口即可,看后面的代码便能知道
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
,这种需要简单看一看
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()
方法
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){ } }
这便是桥接方法的由来.
参考链接: Java反射中method.isBridge()由来,含义和使用场景? – 木女孩的回答 – 知乎
接下来首先看加载 Mapper 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 // 首先通过命名空间检查是否已经加载过此资源 //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
文件
public void parse() { //如果没有加载过再加载,防止重复加载 //和前面的功能一样,不知道为什么这行代码到处都是,有点像是为了集成到`Spring`中 //将原本的代码结构破坏了一样 if (!configuration.isResourceLoaded(resource)) { //获取mapper节点 //<mapper> //</mapper> configurationElement(parser.evalNode("/mapper")); //标记加载 configuration.addLoadedResource(resource); //将namespace绑定在解析助手上 bindMapperForNamespace(); } //重新加载那些因为有加载依赖而加载失败节点 parsePendingResultMaps(); parsePendingChacheRefs(); parsePendingStatements(); }
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()
进行赋值,也就是类的全量名称
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
就直接用了一个未完成集合解决了这个问题,加载的时候发现还需要引用的缓存还没有加载,就先不暂存起来,当加载完其他配置的时候,再尝试一下加载,很方便
可以看下具体代码:
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); } }
这里可以看到,联合缓存其实就是使用的同一个缓存
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); } }
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
究竟是怎么解析的? 疑问先保存起来,先继续看后面的代码。
未完待续