Mybatis

Mybatis 博客链接

本文主要对Mybatis中启动流程、Mapper解析、Mapper代理、四大对象、SQL执行、缓存、插件、与Sprin整合等相关内容进行解析,文章较长,能力有限,有问题麻烦指出,谢谢。关于调试方面,可以直接从Github上下载Mybatis源码,里面包含很多测试代码,下载下来安装依赖就可以直接运行,当然也可以直接通过IDEA关联源码,也比较方便。原创文章,转载请标明出处!

网上找到的一张Mybatis架构图:

Mybatis

启动

public class Application {
    public static void main(String[] args) throws Exception{
        // 1、加载配置文件
        InputStream is = Resources.getResourceAsStream("application.xml");

        // 2、构建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

        // 3、获取SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();

        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.selectByUserName("admin");
        System.err.println(user);
        sqlSession.commit();
    }
}

从xml配置文件中读取配置,然后通过SqlSessionFactoryBuilder构建SqlSessionFactory实例(建造者模式)。SqlSessionFactory是Mybatis的关键对象,它是个单个数据库映射关系经过编译后的内存镜像。SqlSessionFactory是创建SqlSession的工厂,每一个Mybatis的应用程序都以一个SqlSessionFactory对象的实例为核心,同时SqlSessionFactory也是线程安全的,SqlSessionFactory一旦被创建,在应用执行期间都存在。

有关于SqlSessionFactoryBuilder的build方法

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
  try {

    XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      reader.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}
public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

public XMLConfigBuilder(Reader reader, String environment, Properties props) {
  // 在创建XMLConfigBuilder对象之前,创建了一个XPathParser对象,XPathParser用来解析xml文件。即:在构建XMLConfigBuilder之后,即完成了xml文件到dcument对象的转化
  this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
  // 创建Configuration对象
  super(new Configuration());
  ErrorContext.instance().resource("SQL Mapper Configuration");
  this.configuration.setVariables(props);
  this.parsed = false;
  this.environment = environment;
  this.parser = parser;
}

public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
  commonConstructor(validation, variables, entityResolver);
  // 创建document对象
  this.document = createDocument(new InputSource(reader));
}

在上面的代码中,我们重点关注两个东西:一个是XPathParser对象,在创建XPathParser对象的时候会将xml文件转换成document对象;两外一个是Configuration对象,在XMLConfigBuilder对象的构造方法内创建了Configuration对象。

Configuration对象是Myatis中非常非常重要的一个概念,它的作用就相当于是servlet中的ServletContext、spring中的容器,它就是Mybatis的中的Boss,Mybatis的运行就是依赖于这个对象。我们在xml文件中的一些配置,解析之后的mappedStatement等等等等,都维护在这个对像中,非常非常重要的一个对象。所以,有必要对这个对象有个简单的了解:

可以看到,Configuration有非常多的属性,每个属性都很重要,有关于每个属性的具体用途,这里没办法具体介绍,在接下来的源码解析中会慢慢接触到。可以看出,它的构造方法主要是设置了一些别名:

protected Environment environment;

protected boolean safeRowBoundsEnabled;
protected boolean safeResultHandlerEnabled = true;
protected boolean mapUnderscoreToCamelCase;
protected boolean aggressiveLazyLoading;
protected boolean multipleResultSetsEnabled = true;
protected boolean useGeneratedKeys;
protected boolean useColumnLabel = true;
protected boolean cacheEnabled = true;
protected boolean callSettersOnNulls;
protected boolean useActualParamName = true;
protected boolean returnInstanceForEmptyRow;

protected String logPrefix;
protected Class <? extends Log> logImpl;
protected Class <? extends VFS> vfsImpl;
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
protected Integer defaultStatementTimeout;
protected Integer defaultFetchSize;
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;

protected Properties variables = new Properties();
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
protected ObjectFactory objectFactory = new DefaultObjectFactory();
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();

protected boolean lazyLoadingEnabled = false;
protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL

protected String databaseId;
protected Class<?> configurationFactory;

// 用于注册mapper,这个对象中缓存了所有mapper接口的代理对象
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

// 拦截器链
protected final InterceptorChain interceptorChain = new InterceptorChain();

protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

// mapper.xml 中的每个方法对应一个 MappedStatement,重点关注
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");

protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");

protected final Set<String> loadedResources = new HashSet<String>();
protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>("XML fragments parsed from previous mappers");

protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<XMLStatementBuilder>();
protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<CacheRefResolver>();
protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<ResultMapResolver>();
protected final Collection<MethodResolver> incompleteMethods = new LinkedList<MethodResolver>();


public Configuration() {
  typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
  typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

  typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
  typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
  typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);

  typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
  typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
  typeAliasRegistry.registerAlias("LRU", LruCache.class);
  typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
  typeAliasRegistry.registerAlias("WEAK", WeakCache.class);

  typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);

  typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
  typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);

  typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
  typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
  typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
  typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
  typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
  typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
  typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);

  typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
  typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);

  languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
  languageRegistry.register(RawLanguageDriver.class);
}

Mapper解析

XMLConfigBuilder # mapperElement

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      // 如果配置文件中的mapper是以package的形式指定的,这里主要分析这种方式
      if ("package".equals(child.getName())) {
        // 获取到包名
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        // 非package方式,说明指明了具体的mapper接口或者xml文件
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url == null && mapperClass != null) {
          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.");
        }
      }
    }
  }
}

这里其实涉及到Mapper的配置方式,可以通过package的形式配置一个包下的所有接口,或者不以package的形式指明具体的mapper接口。我们在工作中主要是通过package的方式指明接口的位置,所以重点关注configuration.addMappers(mapperPackage)方法:

Configuration # addMappers

protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

public void addMappers(String packageName) {
  mapperRegistry.addMappers(packageName);
}

可以发现,这里引出了一个新对象:MapperRegistry,这是一个挺重要的对象,以下是其源码,非核心代码我已删除

public class MapperRegistry {
  private final Configuration config;

  // 缓存,每个Mapper接口对应一个 MapperProxyFactory
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

  public MapperRegistry(Configuration config) {
    this.config = config;
  }

  @SuppressWarnings("unchecked")
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
  
  public <T> boolean hasMapper(Class<T> type) {
    return knownMappers.containsKey(type);
  }

  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<T>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

  public Collection<Class<?>> getMappers() {
    return Collections.unmodifiableCollection(knownMappers.keySet());
  }

  public void addMappers(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
      addMapper(mapperClass);
    }
  }

  public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
  }
}

在使用Mybatis这个框架的时候,我们往往只是定义了一些Mapper接口和一些XML文件,并没有写实现类,却可以直接调用方法,很明显是Mybatis为我们生成了代理。在上面的代码中,我们注意到上面的代码中MapperProxyFactory这么一个类,根据名字也可以猜测的到这是一个工厂类,用来创建Mapper接口的代理类。事实就是这样,可以看看MapperProxyFactory的源码

很明显newInstance方法中使用了JDK动态代理创建代理类,除此之外,这里还引如了一个MapperProxy对象,我们大概可以猜测到MapperProxy实现了InvocationHandler接口并重写了invoke方法,有关于这一块内容我们等下再来验证,目前可以确定的MapperProxyFactory是创建代理类的工厂,MapperProxy是创建代理类的关键,它是实现了InvocationHandler接口

public class MapperProxyFactory<T> {
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethod> getMethodCache() {
    return methodCache;
  }

  // JDK动态代理,MapperProxy类实现了InvocationHandler接口
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}

所以,综合我们对这两个类的观察,MapperRegistry类用于Mapper接口注册,当我们调用它的addMapperf方法的时候,会为该Mapper接口创建一个MapperProxyFactory对象,并缓存在一个Map中。而MapperProxyFactory主要用于创建代理对象,在创建代理对象的时候,用到了MapperProxy这个类,它是是实现了InvocationHandler接口,所以可以推断出,在调用Mapper方法时候,实际上是调用MapperProxy的invoke方法。

在了解了MapperRegistry类的大体流程之后,我们继续回到MapperRegistry的addMapper方法,该方法大概做了两件事情,一个是创建MapperProxyFactory对象并缓存起来;另一个就是执行Mapper解析。这里调用了MapperAnnotationBuilder的parse方法,根据名字应该可以猜测的到这是和注解的解析相关

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 {
      // 添加到缓存,每个Mapper接口对应一个 MapperProxyFactory
      knownMappers.put(type, new MapperProxyFactory<T>(type));

      // 下面两行代码是对Mapper的解析
      // 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.
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();

      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

事实上,MapperAnnotationBuilder的作用就是解析Mapper接口中定义的注解,并生成Cache、ResultMap、MappedStatement三种类型对象。在创建一个的时候,会关联一个MapperBuilderAssistant对象,这是一个构建助理,实际上,真正将接口中的方法转换成MappedStatement对象就是通过MapperBuilderAssistant对象完成的

public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
  String resource = type.getName().replace('.', '/') + ".java (best guess)";

  // MapperBuilderAssistant类执行真正的解析操作
  this.assistant = new MapperBuilderAssistant(configuration, resource);
  this.configuration = configuration;
  this.type = type;

  // 下面就是Mybatis中的常见注解,下面那4个可用于实现通用Mapper
  sqlAnnotationTypes.add(Select.class);
  sqlAnnotationTypes.add(Insert.class);
  sqlAnnotationTypes.add(Update.class);
  sqlAnnotationTypes.add(Delete.class);

  sqlProviderAnnotationTypes.add(SelectProvider.class);
  sqlProviderAnnotationTypes.add(InsertProvider.class);
  sqlProviderAnnotationTypes.add(UpdateProvider.class);
  sqlProviderAnnotationTypes.add(DeleteProvider.class);
}

public void parse() {
  // 例:resource = "interface com.hand.sxy.mapper.UserMapper"
  String resource = type.toString();

  // loadedResources是configuration对象中的一个属性,是一个Set集合,用于缓存已解析过的Mapper
  if (!configuration.isResourceLoaded(resource)) {

    // 先对 Mapper.xml 文件进行解析
    loadXmlResource();

    // 添加到loadedResources缓存,Key为class对应的全类名
    configuration.addLoadedResource(resource);

    // 设置MapperBuilderAssistant的命名空间 
    assistant.setCurrentNamespace(type.getName());

    // 解析缓存对象
    parseCache();

    // 解析缓存引用
    parseCacheRef();
    Method[] methods = type.getMethods();
    for (Method method : methods) {
      try {
        // 这里涉及到桥接方法,关系到Java的泛型擦除,具体参考 https://www.zhihu.com/question/54895701/answer/141623158
        if (!method.isBridge()) {

          // 解析MappedStatement和ResultMap
          parseStatement(method);
        }
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }

  // 遍历configuration的IncompleteMethods集合,重新解析MethodResolver
  parsePendingMethods();
}

这里面涉及到的内容还挺多,接下里一个个解释一下吧

loadXmlResource

这个方法主要就是解析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
  // 对于已解析过的Mapper.xml文件,会缓存起来。这里为什么要通过 namespace: + 全类名的 方式来缓存呢?因为namespace指定的是一个具体的Mapper接口,而在每个Mapper.xml文件解析之后,会将该对应的
  // 内容缓存起来,如:configuration.addLoadedResource("namespace:" + namespace)、configuration.addMapper(boundType)。可以认为"namespace:" + namespace标识着一个Mapper.xml文件,即每解析完一个Mapper.xml文件,都会缓存起该Mapper.xml文件和namespace对应的接口
  if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
    // 通过对全类名中的 . 替换成 / 得到下 Maper.xml 文件路径,所以这也就要求 使用package的方式时, Mapper.xml 文件 和 Mapper接口名必须相同且在相同目录
    // xmlResource = "com/hand/sxy/mapper/UserMapper.xml"
    String xmlResource = type.getName().replace('.', '/') + ".xml";

    InputStream inputStream = null;
    try {
      // 获取到xml文件输入流
      inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
    } catch (IOException e) {
      // ignore, resource is not required
    }
    if (inputStream != null) {
      XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());

      // 通过XMLMapperBuilder进行解析,如果是不是通过package而是直接配置Mapper.xml文件的方式,就会直接执行下面XMLMapperBuilder的parse方法
      xmlParser.parse();
    }
  }
}

public void parse() {
  // 例:resource =  "com/hand/sxy/mapper/UserMapper.xml",为什么这里又要判断一次,因为当不是通过package或者不是配置Mapper接口的方式,而是配置Mapper.xml文件的方式,会直接执行这个方法
  if (!configuration.isResourceLoaded(resource)) {
    // 真正的解析逻辑
    configurationElement(parser.evalNode("/mapper"));

    // 添加到缓存
    configuration.addLoadedResource(resource);

    // 将已经解析完的Mapper.xml文件标识和对应对应的Mapper接口标识缓存起来,Mapper.xml文件用"namespace:" + namespace表示;Mapper接口用全类名表示。
    bindMapperForNamespace();
  }

  // 残缺不全的重新解析一遍?不太懂这里是为了处理什么情况
  parsePendingResultMaps();
  parsePendingCacheRefs();
  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");
    }
    // 设置当前namespace
    builderAssistant.setCurrentNamespace(namespace);

    /** 以下用于处理各个标签的解析,详情请看:http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html#*/

    // 用于启用本xml对应的namespace的二级缓存  <cache-ref namespace="com.someone.application.data.SomeMapper"/>
    cacheRefElement(context.evalNode("cache-ref"));

    // 共享指定namespace的二级缓存  <cache  eviction="FIFO"  flushInterval="60000"  size="512"  readOnly="true"/>
    cacheElement(context.evalNode("cache"));

    parameterMapElement(context.evalNodes("/mapper/parameterMap"));

    // 进行数据库列和返回Java对象的属性之间的映射 
    resultMapElements(context.evalNodes("/mapper/resultMap"));

    // 可被其他语句引用的可重用sql语句块
    sqlElement(context.evalNodes("/mapper/sql"));

    // 增删改查映射语句
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  }
}

分别对应Mapper.xml文件中的不同标签,很多都是我们常见的,比如:resultMap、sql、select 等等。每个标签的解析对应不同的方法,虽然这些都是XMLStatementBuilder类中的方法,但实际上将各个标签解析为Mybatis中对应的类时,都是通过MapperBuilderAssistant这个助理解析类来完成的。这里就简单分析一下resultMapElements 和 buildStatementFromContext 这个两个方法

resultMapElements

private void resultMapElements(List<XNode> list) throws Exception {
  // 每个XNode就代表一个 <resultMap>, <resultMap>可以有多个
  for (XNode resultMapNode : list) {
    try {
      resultMapElement(resultMapNode);
    } catch (IncompleteElementException e) {
      // ignore, it will be retried
    }
  }
}

private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
  // 每次解析一个 <resultMap> 的时候,都传一个空集合,在解析的过程中再往里面填充
  return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList());
}

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
  ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
  
  // 获取id
  String id = resultMapNode.getStringAttribute("id",
      resultMapNode.getValueBasedIdentifier());

  // 获取 dto 全类名    
  String type = resultMapNode.getStringAttribute("type",
      resultMapNode.getStringAttribute("ofType",
          resultMapNode.getStringAttribute("resultType",
              resultMapNode.getStringAttribute("javaType"))));

  // 获取extend属性值            
  String extend = resultMapNode.getStringAttribute("extends");

 // 获取autoMapping属性值  
  Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");

  Class<?> typeClass = resolveClass(type);
  Discriminator discriminator = null;
  List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
  resultMappings.addAll(additionalResultMappings);

  // 获取到该<resultMap>标签下每个 数据库字段 和 dto字段对应关系
  List<XNode> resultChildren = resultMapNode.getChildren();
  for (XNode resultChild : resultChildren) {
    if ("constructor".equals(resultChild.getName())) {
      processConstructorElement(resultChild, typeClass, resultMappings);
    } else if ("discriminator".equals(resultChild.getName())) {
      discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    } else {
      List<ResultFlag> flags = new ArrayList<ResultFlag>();

      // 代表主键
      if ("id".equals(resultChild.getName())) {
        flags.add(ResultFlag.ID);
      }

      // buildResultMappingFromContext用于将<resultMap>标签下的每行对应关系维护成一个ResultMapping对象,其实该方法内最终也是调用了MapperBuilderAssistant类的buildResultMapping方法来完成这个操作
      resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
  }
  ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
  try {
    // 内部通过MapperBuilderAssistant类的addResultMap方法,构建一个ResultMap对象,并缓存到configuration的resultMaps属性中,以 Mapper全类名 + id 为key,如: com.hand.sxy.mapper.UserMapper.BaseResultMap
    return resultMapResolver.resolve();
  } catch (IncompleteElementException  e) {
    configuration.addIncompleteResultMap(resultMapResolver);
    throw e;
  }
}

可以看看buildResultMappingFromContext方法的返回结果,也就是ResultMapping对象

ResultMapping

至此,有关于<resultMap>这个标签的解析到此结束,接下来看看buildStatementFromContext方法,这个方法主要时解析 select|insert|update|delete 这个四种情况,最后转换成一个MappedStatement对象,然后缓存在configuration的mappedStatements属性中,以 Mapper全类名 + id 为key,如: com.hand.sxy.mapper.UserMapper.selectByUserName

buildStatementFromContext

  private void buildStatementFromContext(List<XNode> list) {
    // 这里是为了处理 在Mybatis配置文件中配置了databaseId的情况
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  // 每个 select|insert|update|delet 就是一个XNode
  for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

public void parseStatementNode() {

  /** 各个属性 */

  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");

  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }

  Integer fetchSize = context.getIntAttribute("fetchSize");
  Integer timeout = context.getIntAttribute("timeout");
  String parameterMap = context.getStringAttribute("parameterMap");
  String parameterType = context.getStringAttribute("parameterType");
  Class<?> parameterTypeClass = resolveClass(parameterType);

  // 返回类型
  String resultMap = context.getStringAttribute("resultMap");
  String resultType = context.getStringAttribute("resultType");
  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver(lang);

  Class<?> resultTypeClass = resolveClass(resultType);
  String resultSetType = context.getStringAttribute("resultSetType");
  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

  // 获取到该标签的名称,即时 select|insert|update|delet 中的哪一种
  String nodeName = context.getNode().getNodeName();

  // SqlCommandType 是个枚举类型,包括    UNKNOWN,INSERT,UPDATE,DELETE,SELECT,FLUSH 这几个值
  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

  boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  boolean useCache = context.getBooleanAttribute("useCache", isSelect);
  boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

  // 在解析SQL之前,处理SQL中的 <include> 标签,即引用了一段SQL
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());

  // 解析selectKey,SelectKey在Mybatis中是为了解决Insert数据时不支持主键自动生成的问题
  processSelectKeyNodes(id, parameterTypeClass, langDriver);
  
  // 正式开始解析SQL,这时候的SQL中的<selectKey> 和 <include> 已经解析好了并且替换成正常的sql语句了。
  // SqlSource非常重要,是用来处理SQL语句的。它有一个getBoundSql(Object parameterObject)方法,就是用来返回最终的SQL的
  // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

  String resultSets = context.getStringAttribute("resultSets");
  String keyProperty = context.getStringAttribute("keyProperty");
  String keyColumn = context.getStringAttribute("keyColumn");
  KeyGenerator keyGenerator;

  // query!selectKey
  String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;

  // com.hand.sxy.mapper.UserMapper.query!selectKey
  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);

  if (configuration.hasKeyGenerator(keyStatementId)) {
    keyGenerator = configuration.getKeyGenerator(keyStatementId);
  } else {
    keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
        configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
        ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  }

  // MapperBuilderAssistant#addMappedStatement生成一个MappedStatement对象,并缓存在configuration的mappedStatements对象中,以Mapper全类名 + id 为key,如: com.hand.sxy.mapper.UserMapper.selectByUserName 
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered, 
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

可以看看buildStatementFromContext方法的返回结果,也就是MappedStatement对象

MappedStatement

解析完之后,还有一个方法需要执行,即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) {
      if (!configuration.hasMapper(boundType)) {
        // Spring may not know the real resource name so we set a flag
        // to prevent loading again this resource from the mapper interface
        // look at MapperAnnotationBuilder#loadXmlResource
        configuration.addLoadedResource("namespace:" + namespace);
        configuration.addMapper(boundType);
      }
    }
  }
}

有关于Mapper.xml文件的解析暂时就分析到这里,可以发现一个规律,就是将一类类标签解析成各种类,然后缓存到configuration对象的各个属性中。

前面讲解的loadXmlResource方法内容有点多,这里再将主流程的代码贴一下

public void parse() {
  String resource = type.toString();
  if (!configuration.isResourceLoaded(resource)) {
    // 解析Mapper.xml
    loadXmlResource();
    configuration.addLoadedResource(resource);
    assistant.setCurrentNamespace(type.getName());

    // 解析@CacheNamespace注解,也就是对应Mapper.xml中的<cache>标签,因为在loadXmlResource方法中已经解析过<cache>标签了,所以这里只要解析@CacheNamespace注解
    parseCache();

    // 解析CacheNamespaceRef注解,也就是对应Mapper.xml中的<cache-ref>标签,与上面类似
    parseCacheRef();

    Method[] methods = type.getMethods();
    for (Method method : methods) {
      try {
        // 排除桥接方法,关系到Java的泛型擦除,具体参考 https://www.zhihu.com/question/54895701/answer/141623158
        if (!method.isBridge()) {
          // 解析方法上的注解,例:@Options、@ResultMap 等注解
          parseStatement(method);
        }
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }

  // 重新解析残缺的method?不太清楚什么情况下会产生残缺的方法
  parsePendingMethods();
}

parseStatement

这个方法主要是解析Mapper接口方法上的一些注解,例:@Options、 @Inser、@Select、@Update、@SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider 等注解,有关于我们常用这几个注解,还是很有必要解释一下。那几个Provider注解,允许我们自己在方法里面写生成SQL的逻辑,可以通过他们在开发通用Mapper组件

void parseStatement(Method method) {
  Class<?> parameterTypeClass = getParameterType(method);
  LanguageDriver languageDriver = getLanguageDriver(method);

  // 获取 @Inser、@Select、@Update、@SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider 注解上对应的 SqlSource
  SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);

  if (sqlSource != null) {
    // @Options 注解
    Options options = method.getAnnotation(Options.class);
    final String mappedStatementId = type.getName() + "." + method.getName();
    Integer fetchSize = null;
    Integer timeout = null;
    StatementType statementType = StatementType.PREPARED;
    ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
    SqlCommandType sqlCommandType = getSqlCommandType(method);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = !isSelect;
    boolean useCache = isSelect;

    KeyGenerator keyGenerator;
    String keyProperty = "id";
    String keyColumn = null;
    if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
      // first check for SelectKey annotation - that overrides everything else
      SelectKey selectKey = method.getAnnotation(SelectKey.class);
      if (selectKey != null) {
        keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
        keyProperty = selectKey.keyProperty();
      } else if (options == null) {
        keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
      } else {
        keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        keyProperty = options.keyProperty();
        keyColumn = options.keyColumn();
      }
    } else {
      keyGenerator = NoKeyGenerator.INSTANCE;
    }

    if (options != null) {
      if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
        flushCache = true;
      } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
        flushCache = false;
      }
      useCache = options.useCache();
      fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
      timeout = options.timeout() > -1 ? options.timeout() : null;
      statementType = options.statementType();
      resultSetType = options.resultSetType();
    }

    String resultMapId = null;
    // @ResultMap 注解
    ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
    if (resultMapAnnotation != null) {
      String[] resultMaps = resultMapAnnotation.value();
      StringBuilder sb = new StringBuilder();
      for (String resultMap : resultMaps) {
        if (sb.length() > 0) {
          sb.append(",");
        }
        sb.append(resultMap);
      }
      resultMapId = sb.toString();
    } else if (isSelect) {
      resultMapId = parseResultMap(method);
    }

    // 可以发现,最终也是生成一个MappedStatement对象
    assistant.addMappedStatement(
        mappedStatementId,
        sqlSource,
        statementType,
        sqlCommandType,
        fetchSize,
        timeout,
        // ParameterMapID
        null,
        parameterTypeClass,
        resultMapId,
        getReturnType(method),
        resultSetType,
        flushCache,
        useCache,
        // TODO gcode issue #577
        false,
        keyGenerator,
        keyProperty,
        keyColumn,
        // DatabaseID
        null,
        languageDriver,
        // ResultSets
        options != null ? nullOrEmpty(options.resultSets()) : null);
  }
}

重点看看这部分内容

SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);

// 先聊来SqlSource这个接口,它只有一个getBoundSql,就是根据参数计算出实际的可执行SQL,这个可执行SQL在Mybatis中用BoundSql表示,这里就时根据注解获取它对应的SqlSource
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
  try {
    //  @Inser、@Select、@Update、@Delete 其中一个
    Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);

    // @SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider 其中一个
    Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);

    // 这两种注解不能同时出现,如果你了解@xxprovider的用法就知道了
    if (sqlAnnotationType != null) {
      if (sqlProviderAnnotationType != null) {
        throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
      }

      Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
      // 获取该注解上的SQL
      final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);

      // 根据SLQ 创建一个SqlSource 
      return buildSqlSourceFromStrings(strings, parameterType, languageDriver);

    // 如果是Provider类型注解,返回ProviderSqlSource。需要注意的是,@xxxProvider注解可以指定类和方法,然后自己在指定的方法里面写SQL的生成逻辑,也就是动态SQL
    } else if (sqlProviderAnnotationType != null) {
      Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
      return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
    }
    return null;
  } catch (Exception e) {
    throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
  }
}

这一小部分内容主要需要注意的是:@xxxProvider注解可以指定类和方法,然后自己在指定的方法里面写SQL的生成逻辑,也就是动态SQL。

可以看看SqlSource的继承结构图

有关去解析这部分内容,也就是XMLConfigBuilder的parse()方法,暂时就介绍到这里,主要是讲解了Mapper这部分的解析逻辑。下面再次回到SqlSessionFactoryBuilder中的build方法上来。

构建SqlSessionFactory

在构建好configuration对象后,接下来的逻辑比较简单,在XMLConfigBuilder的parse方法执行完之后,就已经构建了一个完整的configuration对象,剩下的只是新建一个DefaultSqlSessionFactory对象,并将已构建好的configuration对象作为DefaultSqlSessionFactory对象的一个熟属性

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}

// 创建了一个SqlSessionFactory,默认实现类 DefaultSqlSessionFactory
public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

public DefaultSqlSessionFactory(Configuration configuration) {
  this.configuration = configuration;
}

对于接口中的方法,我们也简单了解一下,主要是和SqlSession相关,具体内容在接下来的章节介绍

public interface SqlSessionFactory {

  SqlSession openSession();

  SqlSession openSession(boolean autoCommit);
  SqlSession openSession(Connection connection);
  SqlSession openSession(TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType);
  SqlSession openSession(ExecutorType execType, boolean autoCommit);
  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType, Connection connection);

  Configuration getConfiguration();

}

至此,有关于Mybatis这个框架的初始化操作已经完成了,现在可以正常使用了,接下来需要了解的就是这个框架的执行流程了。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏青玉伏案

Oracle常用函数

前一段时间学习Oracle 时做的学习笔记,整理了一下,下面是分享的Oracle常用函数的部分笔记,以后还会分享其他部分的笔记,请大家批评指正。 1.Oracl...

2079
来自专栏IMWeb前端团队

Redux源码解析系列(四)-- combineReducers

本文作者:IMWeb 黄qiong 原文出处:IMWeb社区 未经同意,禁止转载 combindeReducer 字面意思就是用来合并reducer的...

1987
来自专栏me的随笔

Redis中的数据结构与常用命令

对于Redis的介绍这里只写一句:Redis是一种基于内存的高性能非关系型数据库,它以kye-value的形式来存储数据。

1893
来自专栏数据结构与算法

BZOJ4260: Codechef REBXOR (01Tire树)

973
来自专栏Web项目聚集地

手写一个Mybatis框架

在手写自己的Mybatis框架之前,我们先来了解一下Mybatis,它的源码中使用了大量的设计模式,阅读源码并观察设计模式在其中的应用,才能够更深入的理解源...

1042
来自专栏JAVA后端开发

给mybatis添加自动建表,自动加字段的功能

以前项目用惯了hibernate,jpa,它有个自动建表功能,只要在PO里加上配置就可以了,感觉很爽. 但现在用mybatis,发现没有该功能,每次都加个字段...

4813
来自专栏Java成神之路

HQL语句大全

Hibernate配备了一种非常强大的查询语言,这种语言看上去很像SQL。但是不要被语法结构 上的相似所迷惑,HQL是非常有意识的被设计为完全面向对象的查询,它...

1635
来自专栏calmound

struts2+Hibernate实现用户登陆功能

实现的功能,在登陆页面输入Username和PassWord后,将username和password通过Hibernate匹对数据库是否含有一样的usernam...

3349
来自专栏编程坑太多

Mybatis入门到精通

891
来自专栏函数式编程语言及工具

浅谈Slick(1)- 基本功能描述

   Slick (Scala language-integrated connection kit)是scala的一个FRM(Functional Relat...

1827

扫码关注云+社区

领取腾讯云代金券