Mybatis 源码:Mapper的解析工作
Posted 无虑的小猪
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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() 核心代码:
// 映射器 mappers 标签解析 private void mapperElement(XNode parent) throws Exception if (parent != null) // 处理mapper子节点 for (XNode child : parent.getChildren()) // package子节点 if ("package".equals(child.getName())) // 自动扫描包下所有映射器 String mapperPackage = child.getStringAttribute("name"); // 扫描指定的包,并向mapperRegistry注册mapper接口 configuration.addMappers(mapperPackage); else // 获取mapper节点的resource、url、class属性,三个属性互斥 String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); // 如果mapper节点指定了resource或者url属性,则创建XmlMapperBuilder对象,并通过该对象解析resource或者url属性指定的mapper配置文件 if (resource != null && url == null && mapperClass == null) // 使用类路径 ErrorContext.instance().resource(resource); // 创建XMLMapperBuilder对象,解析映射配置文件 InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); // Mapper解析器解析 mapperParser.parse(); else if (resource == null && url != null && mapperClass == null) // 使用绝对url路径 ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); // 创建XMLMapperBuilder对象,解析映射配置文件 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); else if (resource == null && url == null && mapperClass != null) // 如果mapper节点指定了class属性,则向MapperRegistry注册该mapper接口 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.");
上述四种配置方式中,package方式与接口方式使用MapperAnnotationBuilder作为解析入口,;resource方式和url方式使用XMLMapperBuilder作为解析入口。
1、XMLMapperBuilder作为解析入口的解析
XMLMapperBuilder继承自BaseBuilder抽象类,在上面已经提到主要用于解析Mapper映射器。
XMLMapperBuilder#parse() 核心代码:
// 解析SQL的Mapper映射文件 public void parse() // 判断是否已经加载过该映射文件 if (!configuration.isResourceLoaded(resource)) // 1、语句注册,具体的增删改查接口标签解析<insert> <update> <delete> <select>。一个标签一个MappedStatement对象。 configurationElement(parser.evalNode("/mapper")); // 2、接口注册,把namespace(接口类型)和工厂类绑定起来,放到一个map。一个namespace 一个 MapperProxyFactory // 将resource添加到Configuration.loadedResources集合中保存,hashset类型的集合,其中记录了已经加载过的映射文件 configuration.addLoadedResource(resource); // 绑定映射器到namespace bindMapperForNamespace(); // 处理ConfigurationElement方法中解析失败的resultMap节点 parsePendingResultMaps(); // 处理ConfigurationElement方法中解析失败的cache-ref节点 parsePendingCacheRefs(); // 处理ConfigurationElement方法中解析失败的SQL语句节点 parsePendingStatements();
主要完成两件事情:
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(),核心代码:
// 解析<Mapper标签> private void configurationElement(XNode context) try // 获取mapper节点的namespace属性 String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) throw new BuilderException("Mapper\'s namespace cannot be empty"); // 设置MapperBuilderAssistant的currentNamespace字段,记录当前命名空间 builderAssistant.setCurrentNamespace(namespace); // 解析cache-ref节点 cacheRefElement(context.evalNode("cache-ref")); // 解析cache节点 cacheElement(context.evalNode("cache")); // 解析parameterMap节点 parameterMapElement(context.evalNodes("/mapper/parameterMap")); // 解析resultMap节点 resultMapElements(context.evalNodes("/mapper/resultMap")); // 解析sql节点 sqlElement(context.evalNodes("/mapper/sql")); // 解析select、update、insert、delete等SQL节点 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);
1、解析resultMap节点
resultMap的解析通过ResultMapResolver解析器中的assistant属性完成的。并将创建的ResultMap对象设置进配置类configuration的resultMaps属性中。
MapperBuilderAssistant#resultMapElement() 核心代码段:
// 创建 resultMap解析器 ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); // 创建ResultMap对象,并添加到resultMap集合中,该集合是StrictMap类型 return resultMapResolver.resolve();
MapperBuilderAssistant#addResultMap() 核心代码段:
// 创建ResultMap对象,并添加到configuration.resultMaps集合中保存 ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping) .discriminator(discriminator) .build(); // 将resultMap添加进配置类configuration的resultMap属性中 configuration.addResultMap(resultMap);
2、生成mappedStatement对象并添加至configuration中,解析select|insert|update|delete节点
XMLMapperBuilder#buildStatementFromContext() 的核心代码:
// 构建语句 private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) for (XNode context : list) // 构建所有语句,一个mapper下可以有很多select // 语句比较复杂,核心都在这里面,所以调用XMLStatementBuilder final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try // 核心XMLStatementBuilder.parseStatementNode statementParser.parseStatementNode(); catch (IncompleteElementException e) // 如果出现SQL语句不完整,把它记下来,塞到configuration去 configuration.addIncompleteStatement(statementParser);
在buildStatementFromContext()方法中,创建了用来解析增删改查标签的XMLStatementBuilder,并且把创建的MappedStatement添加到mappedStatements中。Mybatis通过MapperBuilderAssistant将MappedStatement对象设置到configuration配置类中。
MapperBuilderAssistant#addMappedStatement() 核心代码段:
// 创建MappedStatement对象 MappedStatement statement = statementBuilder.build(); // 将MappedStatement对象添加进配置类configuration的mappedStatements属性中 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() 核心代码:
private void bindMapperForNamespace() // 获取映射配置文件的命名空间 String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) Class<?> boundType = null; try // 解析命名空间对应的类型 boundType = Resources.classForName(namespace); catch (ClassNotFoundException e) //ignore, bound type is not required if (boundType != null) // 是否已经加载了boundType接口 if (!configuration.hasMapper(boundType)) // 追加namespace前缀,并添加到loadedResources集合中保存 configuration.addLoadedResource("namespace:" + namespace); // 调用MapperRegistry.addMapper方法,注册boundType接口 configuration.addMapper(boundType);
主要是调用了configuration#addMapper()。addMapper()方法中,把接口类型注册到MapperRegistry中:实际上是为接口创建一个对应的MapperProxyFactory(用于为这个type提供工厂类,创建MapperProxy)。
添加接口Class对象与MapperProxyFactory的映射,同时解析配置文件中SQL语句。
MapperRegistry#addMapper() 核心代码:
// 记录了Mapper接口与对应MapperProxyFactory之间的关系 private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(); // 添加Mapper映射器 public <T> void addMapper(Class<T> type) // 检测type是否为接口 if (type.isInterface()) // knownMappers集合中,抛异常 if (hasMapper(type)) throw new BindingException("Type " + type + " is already known to the MapperRegistry."); // 加载完成标识 boolean loadCompleted = false; try // 将Mapper接口对应的Class对象和MapperProxyFactory对象添加到knownMappers集合 // Map<Class<?>, MapperProxyFactory<?>> 存放的是接口类型,和对应的工厂类的关系 knownMappers.put(type, new MapperProxyFactory<>(type)); // 创建MapperAnnotationBuilder对象用于解析 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); // 根据接口,开始解析所有方法上的注解,例如 @Select、@Insert...等注解 parser.parse(); loadCompleted = true; finally // 如果加载过程中出现异常需要再将这个mapper从mybatis中删除 if (!loadCompleted) knownMappers.remove(type);
Mapper接口对应的Class对象和MapperProxyFactory对象添加到knownMappers集合,MapperProxyFactory主要用于Mapper接口代理对象的创建,后续会详细介绍到。
解析SQL并获取MappedStatement对象并设置进configuration配置类中,MapperAnnotationBuilder#parse() 核心代码:
public void parse() String resource = type.toString(); // 检测是否已经加载过该接口 if (!configuration.isResourceLoaded(resource)) // 检测是否加载过对应的映射配置文件,如果未加载,则创建XMLMapperBuilder对象解析对应的映射文件 loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); // 解析@CacheNamespace注解 parseCache(); // 解析@CacheNamespaceRef注解 parseCacheRef(); Method[] methods = type.getMethods(); for (Method method : methods) try if (!method.isBridge()) // 解析@SelectKey,@ResultMap等注解,并创建MappedStatement对象,添加进configuration中的mappedStatements属性中 parseStatement(method); catch (IncompleteElementException e) // 如果解析过程出现IncompleteElementException异常,可能是引用了未解析的注解,此处将出现异常的方法添加到incompleteMethod集合中保存 configuration.addIncompleteMethod(new MethodResolver(this, method)); parsePendingMethods();
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() 核心代码:
// 记录了Mapper接口与对应MapperProxyFactory之间的关系 private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(); // 添加Mapper映射器 public <T> void addMapper(Class<T> type) // 检测type是否为接口 if (type.isInterface()) // knownMappers集合中,抛异常 if (hasMapper(type)) throw new BindingException("Type " + type + " is already known to the MapperRegistry."); // 加载完成表示 boolean loadCompleted = false; try // 将Mapper接口对应的Class对象和MapperProxyFactory对象添加到knownMappers集合 knownMappers.put(type, new MapperProxyFactory<>(type)); // 创建MapperAnnotationBuilder对象用于解析 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); // 解析 parser.parse(); loadCompleted = true; finally // 如果加载过程中出现异常需要再将这个mapper从mybatis中删除 if (!loadCompleted) knownMappers.remove(type);
优先将接口的Class类型与映射代理工厂MapperProxyFactory的映射关系设置在MapperRegistry的knownMappers属性中,映射器代理工厂MapperProxyFactory在后续会详细介绍到,此处只需知道MapperProxyFactory使用来生成Mapper接口代理对象的。
通过MapperAnnotationBuilder完成解析,创建MappedStatement对象同时设置进configuration对象中。
MapperAnnotationBuilder#parse() 核心代码:
// 解析mapper public void parse() String resource = type.toString(); // 检测是否已经加载过该接口 if (!configuration.isResourceLoaded(resource)) // 检测是否加载过对应的映射配置文件,如果未加载,则创建XMLMapperBuilder对象解析对应的映射文件 loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); // 解析@CacheNamespace注解 parseCache(); // 解析@CacheNamespaceRef注解 parseCacheRef(); Method[] methods = type.getMethods(); for (Method method : methods) try // issue #237 if (!method.isBridge()) // 解析@SelectKey,@ResultMap等注解,并创建MappedStatement对象 parseStatement(method); catch (IncompleteElementException e) // 如果解析过程出现IncompleteElementException异常,可能是引用了未解析的注解,此处将出现异常的方法添加到incompleteMethod集合中保存 configuration.addIncompleteMethod(new MethodResolver(this, method)); parsePendingMethods();
上述代码与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属性。
以上是关于Mybatis 源码:Mapper的解析工作的主要内容,如果未能解决你的问题,请参考以下文章