mybatis|Mybatis—MappedStatement

??Mybatis通过MappedStatement来描述XML中和@Select、@Update等注解配置的SQL信息,Mybatis在解析XML配置构造Configuration的时候一并解析构造好了MappedStatement,以XMLConfigBuilder#parse为切入点,一起来分析下Mybatis是如何完成构造MappedStatement。
??XMLConfigBuilder对外提供了一个parse()方法用于解析构造Configuration对象,XMLConfigBuilder#parse方法首先判断当前XML是否正在被解析,如果没有被解析就将parsed属性设置为true,就确保一次调用;然后获取configuration标签下的所有信息;最后调用XMLConfigBuilder#mapperElement方法解析对应标签信息。
??XMLConfigBuilder#parseConfiguration方法是XMLConfigBuilder中用于解析各属性信息的一个私有方法,下述代码仅截取了构造MappedStatement的部分代码,不难看出XMLConfigBuilder#parseConfiguration方法在获取到mappers属性标签信息后,直接调用了XMLConfigBuilder#mapperElement私有方法。

public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } // 标记为解析中,保证一次调用 parsed = true; // 首先获取configuration标签下的所有信息 // 然后调用parseConfiguration方法解析对应标签信息 parseConfiguration(parser.evalNode("/configuration")); return configuration; }private void parseConfiguration(XNode root) { try { // 省略部分代码 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }

解析 ??XMLConfigBuilder#mapperElement方法相当于一个分流器,将不同类型标识mapper的标签信息指引到所属的处理逻辑上,如下(亦可访问org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement源代码了解更多信息):
  1. 如果当前XNode节点描述的是package标签,那么执行package标签解析逻辑;
  2. 如果当前XNode节点描述的是resource标签,在获取到resource对应的信息后,执行resource标签解析逻辑;
  3. 如果当前XNode节点描述的是url标签,在获取到url对应的信息后,执行url标签解析逻辑;
  4. 如果当前XNode节点描述的是class标签,获取class对应的接口信息,然后执行class标签解析逻辑;
??虽然XMLConfigBuilder#mapperElement针对package、resource、url和class标签都有其专属的解析逻辑,但是归根结底就两个逻辑,一个是解析XML配置中的标签,另一个则是解析@Select、@Update等注解配置的信息,分别对应XMLMapperBuilder#parse和MapperAnnotationBuilder#parse。
XMLMapperBuilder#parse
??当XML配置的是resource和url节点时会使用XMLMapperBuilder#parse来解析构造MappedStatement,但是在调用XMLMapperBuilder#parse之前,需要先使用XMLMapperBuilder的构造方法构造一个XMLMapperBuilder对象。
??XMLMapperBuilder#parse处理逻辑大致分为,
  1. 判断当前资源是否已经被加载,如果已被加载直接解析返回参数、缓存和请求参数;
  2. 如果当前资源没有被加载,就获取mapper标签下所有信息,调用XMLMapperBuilder#configurationElement私有方法;
  3. 如果当前资源没有被加载,调用xml.XMLMapperBuilder#bindMapperForNamespace私有方法从命名空间中解析构造MappedStatement。
public void parse() { if (!configuration.isResourceLoaded(resource)) { // 首先获取mapper标签信息 // 然后解析mapper标签下所有信息 configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); // 从命名空间构造MappedStatement bindMapperForNamespace(); }parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }

??XMLMapperBuilder#configurationElement分别解析了cache-ref、cache、/mapper/parameterMap、/mapper/resultMap、/mapper/sql和select|insert|update|delete标签信息,可详见org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement源代码。
MapperAnnotationBuilder#parse
??当XML配置的是package和class节点时会使用XMLMapperBuilder#parse来解析构造MappedStatement,同样需要先使用MapperAnnotationBuilder构造方法构造MapperAnnotationBuilder对象,才能使用parse方法。
??从源代码来看MapperAnnotationBuilder#parse,首先会调用MapperAnnotationBuilder#loadXmlResource私有方法来通过XMLMapperBuilder#parse方法解析构造当前Mapper接口对应XML文件中的信息,在完成XML解析之后才是正在解析Mapper接口中定义的注解,详细可见org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse。
public void parse() { String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); parseCache(); parseCacheRef(); for (Method method : type.getMethods()) { if (!canHaveStatement(method)) { continue; } if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent() && method.getAnnotation(ResultMap.class) == null) { parseResultMap(method); } try { parseStatement(method); } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); }

【mybatis|Mybatis—MappedStatement】??无论是使用XMLMapperBuilder#parse还是MapperAnnotationBuilder#parse来解析,最终都会通过MapperBuilderAssistant#addMappedStatement方法来构造MappedStatement对象放入configuration中的mappedStatements属性。
动态代理 ??了解了Mybatis是如何完成解析构造MappedStatement之后,我们在一起来看看Mybatis是如何根据Mapper接口来动态生成真正调用类。回到XMLMapperBuilder#parse和MapperAnnotationBuilder#parse方法中调用的MapperRegistry#addMapper方法,其源代码中通过MapperProxyFactory类来注册一个Mapper代理类工厂,然后在调用MapperRegistry#getMapper方法时通过MapperProxyFactory#newInstance方法来生成代理类。
public void addMapper(Class type) { if (type.isInterface()) { // 省略部分代码 knownMappers.put(type, new MapperProxyFactory<>(type)); } }

??MapperProxyFactory#newInstance使用JDK动态代理Proxy类来完成生成mapper接口的代理类,这也正是Mybatis的mapper需要以接口形式存在的原因。
protected T newInstance(MapperProxy mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }public T newInstance(SqlSession sqlSession) { final MapperProxy mapperProxy = new MapperProxy<>( sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }

    推荐阅读