Mybatis 源码(四):Mapper的解析工作
1、Mapper配置方式
1、package方式
指定包路径:
<mappers> <package name="org.snails.mapper"/> </mappers>
2、resource方式
指定mapper.xml文件的相对路径:
<mappers> <mapper resource="org/snails/mapper/SnailsMapper.xml"/> </mappers>
3、url方式
指定mapper.xml文件的绝对路径:
<mappers> <mapper url="file:///opt/org/snails/mapper/SnailsMapper.xml"/> </mappers>
4、接口方式
指定mapper接口:
<mappers> <mapper class="org.snails.inter.SnailsMapper"/> </mappers>
2、Mapper解析源码
Mappers标签的解析,根据全局配置文件中不同的注册方式,有不同的扫描方式。最终都要做两件事,1、语句注册;2、接口注册。
Mappers标签的解析,XMLConfigBuilder#mapperElement() 核心代码:
1 // 映射器 mappers 标签解析 2 private void mapperElement(XNode parent) throws Exception { 3 if (parent != null) { 4 // 处理mapper子节点 5 for (XNode child : parent.getChildren()) { 6 // package子节点 7 if ("package".equals(child.getName())) { 8 // 自动扫描包下所有映射器 9 String mapperPackage = child.getStringAttribute("name"); 10 // 扫描指定的包,并向mapperRegistry注册mapper接口 11 configuration.addMappers(mapperPackage); 12 } else { 13 // 获取mapper节点的resource、url、class属性,三个属性互斥 14 String resource = child.getStringAttribute("resource"); 15 String url = child.getStringAttribute("url"); 16 String mapperClass = child.getStringAttribute("class"); 17 // 如果mapper节点指定了resource或者url属性,则创建XmlMapperBuilder对象,并通过该对象解析resource或者url属性指定的mapper配置文件 18 if (resource != null && url == null && mapperClass == null) { 19 // 使用类路径 20 ErrorContext.instance().resource(resource); 21 // 创建XMLMapperBuilder对象,解析映射配置文件 22 InputStream inputStream = Resources.getResourceAsStream(resource); 23 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); 24 // Mapper解析器解析 25 mapperParser.parse(); 26 } else if (resource == null && url != null && mapperClass == null) { 27 // 使用绝对url路径 28 ErrorContext.instance().resource(url); 29 InputStream inputStream = Resources.getUrlAsStream(url); 30 // 创建XMLMapperBuilder对象,解析映射配置文件 31 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); 32 mapperParser.parse(); 33 } else if (resource == null && url == null && mapperClass != null) { 34 // 如果mapper节点指定了class属性,则向MapperRegistry注册该mapper接口 35 Class<?> mapperInterface = Resources.classForName(mapperClass); 36 // 直接把这个映射加入配置 37 configuration.addMapper(mapperInterface); 38 } else { 39 throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); 40 } 41 } 42 } 43 } 44 }
上述四种配置方式中,package方式与接口方式使用MapperAnnotationBuilder作为解析入口,;resource方式和url方式使用XMLMapperBuilder作为解析入口。
1、XMLMapperBuilder作为解析入口的解析
XMLMapperBuilder继承自BaseBuilder抽象类,在上面已经提到主要用于解析Mapper映射器。
XMLMapperBuilder#parse() 核心代码:
1 // 解析SQL的Mapper映射文件 2 public void parse() { 3 // 判断是否已经加载过该映射文件 4 if (!configuration.isResourceLoaded(resource)) { 5 // 1、语句注册,具体的增删改查接口标签解析<insert> <update> <delete> <select>。一个标签一个MappedStatement对象。 6 configurationElement(parser.evalNode("/mapper")); 7 // 2、接口注册,把namespace(接口类型)和工厂类绑定起来,放到一个map。一个namespace 一个 MapperProxyFactory 8 // 将resource添加到Configuration.loadedResources集合中保存,hashset类型的集合,其中记录了已经加载过的映射文件 9 configuration.addLoadedResource(resource); 10 // 绑定映射器到namespace 11 bindMapperForNamespace(); 12 } 13 14 // 处理ConfigurationElement方法中解析失败的resultMap节点 15 parsePendingResultMaps(); 16 // 处理ConfigurationElement方法中解析失败的cache-ref节点 17 parsePendingCacheRefs(); 18 // 处理ConfigurationElement方法中解析失败的SQL语句节点 19 parsePendingStatements(); 20 }
主要完成两件事情:
1、configurationElement():解析Mapper.xml所有的子标签,最终获得MappedStatement对象,并注册到配置类configuration中。
2、bindMapperForNamespace():把namespace(接口类型)和工厂类MapperProxyFactory绑定起来,将Mapper接口与接口代理工厂映射关系设置在configuration中mapperRegistry属性对象的knownMappers缓存属性中。
1.1、configurationElement()
解析Mapper.xml配置文件中的标签信息,比如namespace、cache、parameterMap、resultMap、sql和select|insert|update|delete等。
XMLMapperBuilder#configurationElement(),核心代码:
1 // 解析<Mapper标签> 2 private void configurationElement(XNode context) { 3 try { 4 // 获取mapper节点的namespace属性 5 String namespace = context.getStringAttribute("namespace"); 6 if (namespace == null || namespace.equals("")) { 7 throw new BuilderException("Mapper's namespace cannot be empty"); 8 } 9 // 设置MapperBuilderAssistant的currentNamespace字段,记录当前命名空间 10 builderAssistant.setCurrentNamespace(namespace); 11 // 解析cache-ref节点 12 cacheRefElement(context.evalNode("cache-ref")); 13 // 解析cache节点 14 cacheElement(context.evalNode("cache")); 15 // 解析parameterMap节点 16 parameterMapElement(context.evalNodes("/mapper/parameterMap")); 17 // 解析resultMap节点 18 resultMapElements(context.evalNodes("/mapper/resultMap")); 19 // 解析sql节点 20 sqlElement(context.evalNodes("/mapper/sql")); 21 // 解析select、update、insert、delete等SQL节点 22 buildStatementFromContext(context.evalNodes("select|insert|update|delete")); 23 } catch (Exception e) { 24 throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); 25 } 26 }
1、解析resultMap节点
resultMap的解析通过ResultMapResolver解析器中的assistant属性完成的。并将创建的ResultMap对象设置进配置类configuration的resultMaps属性中。
MapperBuilderAssistant#resultMapElement() 核心代码段:
1 // 创建 resultMap解析器 2 ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); 3 // 创建ResultMap对象,并添加到resultMap集合中,该集合是StrictMap类型 4 return resultMapResolver.resolve();
MapperBuilderAssistant#addResultMap() 核心代码段:
1 // 创建ResultMap对象,并添加到configuration.resultMaps集合中保存 2 ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping) 3 .discriminator(discriminator) 4 .build(); 5 // 将resultMap添加进配置类configuration的resultMap属性中 6 configuration.addResultMap(resultMap);
2、生成mappedStatement对象并添加至configuration中,解析select|insert|update|delete节点
XMLMapperBuilder#buildStatementFromContext() 的核心代码:
1 // 构建语句 2 private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { 3 for (XNode context : list) { 4 // 构建所有语句,一个mapper下可以有很多select 5 // 语句比较复杂,核心都在这里面,所以调用XMLStatementBuilder 6 final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); 7 try { 8 // 核心XMLStatementBuilder.parseStatementNode 9 statementParser.parseStatementNode(); 10 } catch (IncompleteElementException e) { 11 // 如果出现SQL语句不完整,把它记下来,塞到configuration去 12 configuration.addIncompleteStatement(statementParser); 13 } 14 } 15 }
在buildStatementFromContext()方法中,创建了用来解析增删改查标签的XMLStatementBuilder,并且把创建的MappedStatement添加到mappedStatements中。Mybatis通过MapperBuilderAssistant将MappedStatement对象设置到configuration配置类中。
MapperBuilderAssistant#addMappedStatement() 核心代码段:
1 // 创建MappedStatement对象 2 MappedStatement statement = statementBuilder.build(); 3 // 将MappedStatement对象添加进配置类configuration的mappedStatements属性中 4 configuration.addMappedStatement(statement);
注意:在addMappedStatement方法中,有个参数sqlCommandType,代表sql命令的类型,Mybtais通过sqlCommandType完成对SQL语句增删改查的判断,Mybatis解析Mapper.xml中的SQL的标签来获取sqlCommandType。
XMLStatementBuilder#parseStatementNode() 核心代码段:
// 根据SQL节点的名称决定其SqlCommandType String nodeName = context.getNode().getNodeName(); SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
MappedStatement对象中的很多属性都是在XMLStatementBuilder#parseStatementNode()方法中创建的。
1.2、bindMapperForNamespace()
构建映射的SQL语句,XMLMapperBuilder#bindMapperForNamespace() 核心代码:
1 private void bindMapperForNamespace() { 2 // 获取映射配置文件的命名空间 3 String namespace = builderAssistant.getCurrentNamespace(); 4 if (namespace != null) { 5 Class<?> boundType = null; 6 try { 7 // 解析命名空间对应的类型 8 boundType = Resources.classForName(namespace); 9 } catch (ClassNotFoundException e) { 10 //ignore, bound type is not required 11 } 12 if (boundType != null) { 13 // 是否已经加载了boundType接口 14 if (!configuration.hasMapper(boundType)) { 15 // 追加namespace前缀,并添加到loadedResources集合中保存 16 configuration.addLoadedResource("namespace:" + namespace); 17 // 调用MapperRegistry.addMapper方法,注册boundType接口 18 configuration.addMapper(boundType); 19 } 20 } 21 } 22 }
主要是调用了configuration#addMapper()。addMapper()方法中,把接口类型注册到MapperRegistry中:实际上是为接口创建一个对应的MapperProxyFactory(用于为这个type提供工厂类,创建MapperProxy)。
添加接口Class对象与MapperProxyFactory的映射,同时解析配置文件中SQL语句。
MapperRegistry#addMapper() 核心代码:
1 // 记录了Mapper接口与对应MapperProxyFactory之间的关系 2 private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(); 3 4 // 添加Mapper映射器 5 public <T> void addMapper(Class<T> type) { 6 // 检测type是否为接口 7 if (type.isInterface()) { 8 // knownMappers集合中,抛异常 9 if (hasMapper(type)) { 10 throw new BindingException("Type " + type + " is already known to the MapperRegistry."); 11 } 12 // 加载完成标识 13 boolean loadCompleted = false; 14 try { 15 // 将Mapper接口对应的Class对象和MapperProxyFactory对象添加到knownMappers集合 16 // Map<Class<?>, MapperProxyFactory<?>> 存放的是接口类型,和对应的工厂类的关系 17 knownMappers.put(type, new MapperProxyFactory<>(type)); 18 // 创建MapperAnnotationBuilder对象用于解析 19 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); 20 // 根据接口,开始解析所有方法上的注解,例如 @Select、@Insert...等注解 21 parser.parse(); 22 loadCompleted = true; 23 } finally { 24 // 如果加载过程中出现异常需要再将这个mapper从mybatis中删除 25 if (!loadCompleted) { 26 knownMappers.remove(type); 27 } 28 } 29 } 30 }
Mapper接口对应的Class对象和MapperProxyFactory对象添加到knownMappers集合,MapperProxyFactory主要用于Mapper接口代理对象的创建,后续会详细介绍到。
解析SQL并获取MappedStatement对象并设置进configuration配置类中,MapperAnnotationBuilder#parse() 核心代码:
1 public void parse() { 2 String resource = type.toString(); 3 // 检测是否已经加载过该接口 4 if (!configuration.isResourceLoaded(resource)) { 5 // 检测是否加载过对应的映射配置文件,如果未加载,则创建XMLMapperBuilder对象解析对应的映射文件 6 loadXmlResource(); 7 configuration.addLoadedResource(resource); 8 assistant.setCurrentNamespace(type.getName()); 9 // 解析@CacheNamespace注解 10 parseCache(); 11 // 解析@CacheNamespaceRef注解 12 parseCacheRef(); 13 Method[] methods = type.getMethods(); 14 for (Method method : methods) { 15 try { 16 if (!method.isBridge()) { 17 // 解析@SelectKey,@ResultMap等注解,并创建MappedStatement对象,添加进configuration中的mappedStatements属性中 18 parseStatement(method); 19 } 20 } catch (IncompleteElementException e) { 21 // 如果解析过程出现IncompleteElementException异常,可能是引用了未解析的注解,此处将出现异常的方法添加到incompleteMethod集合中保存 22 configuration.addIncompleteMethod(new MethodResolver(this, method)); 23 } 24 } 25 } 26 parsePendingMethods(); 27 }
mappedStatement通过MapperBuilderAssistant对象完成MappedStatement对象添加进配置类configuration中的mappedStatements属性。
增加映射语句,MapperBuilderAssistant#addMappedStatement() 核心代码段:
// 创建MappedStatement对象 MappedStatement statement = statementBuilder.build(); // 将MappedStatement对象添加进配置类configuration的mappedStatements属性中 configuration.addMappedStatement(statement);
注意:在MapperBuilderAssistant#addMappedStatement()中创建MappedStatement的内部类Builder时,默认的将statementType设置为PREPARED,resultSetType设置为DEFAULT,该属性会在后续创建具体的StatementHandler对象是用到。
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
2、MapperAnnotationBuilder作为解析入口的解析
其实XMLMapperBuilder作为解析入口与MapperAnnotationBuilder作为解析入口核心流程是一样的,最终都是将MappedStatement对象添加进配置类configuration中的mappedStatements属性中。只不过代码执行的顺序不同。
MapperRegistry#addMapper() 核心代码:
1 // 记录了Mapper接口与对应MapperProxyFactory之间的关系 2 private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(); 3 4 // 添加Mapper映射器 5 public <T> void addMapper(Class<T> type) { 6 // 检测type是否为接口 7 if (type.isInterface()) { 8 // knownMappers集合中,抛异常 9 if (hasMapper(type)) { 10 throw new BindingException("Type " + type + " is already known to the MapperRegistry."); 11 } 12 // 加载完成表示 13 boolean loadCompleted = false; 14 try { 15 // 将Mapper接口对应的Class对象和MapperProxyFactory对象添加到knownMappers集合 16 knownMappers.put(type, new MapperProxyFactory<>(type)); 17 // 创建MapperAnnotationBuilder对象用于解析 18 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); 19 // 解析 20 parser.parse(); 21 loadCompleted = true; 22 } finally { 23 // 如果加载过程中出现异常需要再将这个mapper从mybatis中删除 24 if (!loadCompleted) { 25 knownMappers.remove(type); 26 } 27 } 28 } 29 }
优先将接口的Class类型与映射代理工厂MapperProxyFactory的映射关系设置在MapperRegistry的knownMappers属性中,映射器代理工厂MapperProxyFactory在后续会详细介绍到,此处只需知道MapperProxyFactory使用来生成Mapper接口代理对象的。
通过MapperAnnotationBuilder完成解析,创建MappedStatement对象同时设置进configuration对象中。
MapperAnnotationBuilder#parse() 核心代码:
1 // 解析mapper 2 public void parse() { 3 String resource = type.toString(); 4 // 检测是否已经加载过该接口 5 if (!configuration.isResourceLoaded(resource)) { 6 // 检测是否加载过对应的映射配置文件,如果未加载,则创建XMLMapperBuilder对象解析对应的映射文件 7 loadXmlResource(); 8 configuration.addLoadedResource(resource); 9 assistant.setCurrentNamespace(type.getName()); 10 // 解析@CacheNamespace注解 11 parseCache(); 12 // 解析@CacheNamespaceRef注解 13 parseCacheRef(); 14 Method[] methods = type.getMethods(); 15 for (Method method : methods) { 16 try { 17 // issue #237 18 if (!method.isBridge()) { 19 // 解析@SelectKey,@ResultMap等注解,并创建MappedStatement对象 20 parseStatement(method); 21 } 22 } catch (IncompleteElementException e) { 23 // 如果解析过程出现IncompleteElementException异常,可能是引用了未解析的注解,此处将出现异常的方法添加到incompleteMethod集合中保存 24 configuration.addIncompleteMethod(new MethodResolver(this, method)); 25 } 26 } 27 } 28 parsePendingMethods(); 29 }
上述代码与xml配置文件中的解析方式相同,只不过是此处在loadXmlResource()中不会被过滤掉。
// 检测是否加载过对应的映射配置文件,如果未加载,则创建XMLMapperBuilder对象解析对应的映射文件 loadXmlResource()
loadXmlResource()最终会调用XMLMapperBuilder#parse()方法,加载Mapper.xml映射文件中的SQL映射语句,解析为MappedStatement对象并注册到配置类configuration中,然后将Mapper接口与映射代理工程MapperProxyFactory做绑定,将Mapper接口中被@Select等被注解修饰的方法也解析为MappedStatement对象并完成注册。
3、总结
对Mappers标签的解析工作,主要完成如下两件事情:
1、优先解析*Mapper.xml配置文件,将配置文件信息解析成MappedStatement对象。注册到configuration对象中的mappedStatements属性。
2、若Mapper接口未注册到配置类configuration中,把namespace(接口类型)和工厂类MapperProxyFactory绑定起来,将映射关系设置在configuration中mapperRegistry属性对象的knownMappers缓存属性中。同时将带有@Select等注解的方法解析成MappedStatement对象,注册到到configuration对象中的mappedStatements属性。