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属性。

 

posted @ 2023-03-20 17:54  无虑的小猪  阅读(320)  评论(0编辑  收藏  举报