• mybatis执行过程分析


    1 总结

    代码有些多,怕有的大兄弟耐不下心, 就先写一个总结。

    mybatis的的大概流程是这样的:

    • 通过解析配置文件分析mapper文件和接口,生成代理对象。
    • 根据配置文件,创建会话
    • 通过会话拿到代理对象
    • 通过代理对象,执行具体方法,将接口和sql关联,并执行。

    Note: mybatis version --3.4.6

    2 代码示例

    public void howToUseMybatis() throws Exception {
    
        String confLocation = "mybatis-conf.xml";
    
        Reader reader = Resources.getResourceAsReader(confLocation);
    
        // step1. 通过配置文件构建 SqlSessionFactory
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(reader);
    
        // step2. openSession
        SqlSession sqlSession = sessionFactory.openSession();
    
        // step3. 给Mapper接口生成对应的的代理类。
        PetMapper petMapper = sqlSession.getMapper(PetMapper.class);
    
        // step4. 调用方法
        Pet pet = petMapper.selectByName("zhangsan");
    
        System.out.println(pet);
    }
    

    2.1 Step1

    解析配置文件,然后构建出SqlSessionFactory。build中有很多细节,需要一一分析出来。

    org.apache.ibatis.session.SqlSessionFactoryBuilder#build()方法一直点下, 进入终极buiild。在终极build中,有parse方法,顾名思义,那就是解析,那就要去看去解析什么。

      public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
        try {
          XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
        	// 进入parse方法。
          return build(parser.parse());
        } 
      ..
      }
    

    进入org.apache.ibatis.builder.xml.XMLConfigBuilder#parse方法, 可以看出来,是在解析的配置文件,根节点是configuration。

      public Configuration parse() {
        ...
        // 具体解析
        parseConfiguration(parser.evalNode("/configuration"));
        // 返回的是configration对象。
        return configuration;
      }
    

    接下来,进入parseConfiguration,继续追踪细节。

      private void parseConfiguration(XNode root) {
        try {
          propertiesElement(root.evalNode("properties"));
          Properties settings = settingsAsProperties(root.evalNode("settings"));
          loadCustomVfs(settings);
          typeAliasesElement(root.evalNode("typeAliases"));
          pluginElement(root.evalNode("plugins"));
          objectFactoryElement(root.evalNode("objectFactory"));
          objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
          reflectorFactoryElement(root.evalNode("reflectorFactory"));
          settingsElement(settings);
          environmentsElement(root.evalNode("environments"));
          databaseIdProviderElement(root.evalNode("databaseIdProvider"));
          typeHandlerElement(root.evalNode("typeHandlers"));
          // 解析mapper文件,我们需要具体关注。因为关乎后面如何通过接口代理来调用方法。
          mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
      }
    

    我们通过节点名称可以看出来,在解析configuration文件中的每个节点。但里面有个我们需要特别关注,那就是 mapperElement(root.evalNode("mappers"));,这个关乎后面如何和java代理关联起来。通过接口来调用方法。我们进入这个方法,查看具体细节。

    private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
          for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
        			...
            } else {
              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());
                // 因为我配置是resource,所以进入的是此方法。
                mapperParser.parse();
              } else if (resource == null && url != null && mapperClass == null) {
                mapperParser.parse();
              } else if (resource == null && url == null && mapperClass != null) {
                configuration.addMapper(mapperInterface);
              } else {
              }
            }
          }
        }
    

    这个方法里面是根据配置文件,采用什么逻辑来解析,因为我配置的mapper-resources, 所以进入的是上面方法。没啥好逼逼的,直接点进去看看。

      public void parse() {
        if (!configuration.isResourceLoaded(resource)) {
          // 解析mapper配置文件
          configurationElement(parser.evalNode("/mapper"));
          // 防止重复解析
          configuration.addLoadedResource(resource);
          // 关联mapper.xml对应的的mapper.java文件
          bindMapperForNamespace();
        }
        ...
      }
    
    2.1.1 解析mapper.xml

    来了,就是这, 就是这!!让我开始裸看代码出现迷糊的地方,没有找到解析配置文件的地方。大家注意,我们一步步来。先从 configurationElement(parser.evalNode("/mapper"));看起来。

      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");
          }
          builderAssistant.setCurrentNamespace(namespace);
          cacheRefElement(context.evalNode("cache-ref"));
          cacheElement(context.evalNode("cache"));
          parameterMapElement(context.evalNodes("/mapper/parameterMap"));
          resultMapElements(context.evalNodes("/mapper/resultMap"));
          sqlElement(context.evalNodes("/mapper/sql"));
          // 需要关注这个,这个就是解析sql的方法入口
          buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        } catch (Exception e) {
        }
      }
    

    我们通过解析的节点可以看出来,是在解析配置中的节点。例如:resultMap,sql等。 我们需要关注buildStatementFromContext(context.evalNodes("select|insert|update|delete"));,因为这个是具体解析sql的方法。并且透露一下,这个里面也是生成MappedStatement的地方。

      private void buildStatementFromContext(List<XNode> list) {
        if (configuration.getDatabaseId() != null) {
          buildStatementFromContext(list, configuration.getDatabaseId());
        }
        buildStatementFromContext(list, null);
      }
    
      private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        for (XNode context : list) {
          final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
          try {
            // look here.  具体执行的地方。
            statementParser.parseStatementNode();
          } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
          }
        }
      }
    

    我把两个代码一起贴出来,这两段代码没什么好解释的,我们直接看statementParser.parseStatementNode();

    public void parseStatementNode() {
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");
        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);
    		...
        // MappedStatement
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered, 
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
      }
    

    代码比较多,删了一部分。不过我们还是可以看出来,就是在解析每一个sql片段,生成一个MappedStatement. 不过我们还是要点进addMappedStatement方法, 因为里面还有一段生成id的逻辑。

    public MappedStatement addMappedStatement(...){
         if (unresolvedCacheRef) {
          throw new IncompleteElementException("Cache-ref not yet resolved");
        }
    		// 处理ID, 把id转换成使用namespace+方法名。 
        id = applyCurrentNamespace(id, false);
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    
        MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
            .resource(resource)
            .fetchSize(fetchSize)
            .timeout(timeout)
            .statementType(statementType)
            .keyGenerator(keyGenerator)
            .keyProperty(keyProperty)
            .keyColumn(keyColumn)
            .databaseId(databaseId)
            .lang(lang)
            .resultOrdered(resultOrdered)
            .resultSets(resultSets)
            .resultMaps(getStatementResultMaps(resultMap, resultType, id))
            .resultSetType(resultSetType)
            .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
            .useCache(valueOrDefault(useCache, isSelect))
            .cache(currentCache);
    
        ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
        if (statementParameterMap != null) {
          statementBuilder.parameterMap(statementParameterMap);
        }
    
        MappedStatement statement = statementBuilder.build();
        configuration.addMappedStatement(statement);
        return statement;
    }
    

    我们在这就可以清楚的看到,mapper.xml文件最后被解析成了MappedStatement文件,并且id是以namespace+方法名组成的。并且把这些MappedStatement放入到了全局的configuration类中。自此,我们看到MappedStatement细节全部处理完成。

    2.1.2 解析Mapper.java

    回过头,我们接着看bindMapperForNamespace();方法,这个方法也比较重要。也解释了为什么明明是接口(Interface), 但是可以返回对象。Let's go .

    private void bindMapperForNamespace() {
      	// 获取MappedStatement 的Namespace
        String namespace = builderAssistant.getCurrentNamespace();
        if (namespace != null) {
          Class<?> boundType = null;
          try {
            // 通过namespace获取接口类型。这里也说明了,为什么我们在配置mapper.xml文件的时候,
            // namespace 要和接口的包名相同
            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);
              // 关键点,把mapper.java类型保存起来
              configuration.addMapper(boundType);
            }
          }
        }
    

    核心点了,解析到Mapper.java后,调用了configuration.addMapper(boundType);,里面具体是什么样的呢?我们一路点下去,最后addMapper的具体代码如下.

      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 {
            // 核心关键点,把类型和,和类型生成的MapperProxyFactory放入了map中保存了起来
            knownMappers.put(type, new MapperProxyFactory<T>(type));
            // 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);
            // 通过接口,解析,因为有的sql直接写在接口上面。这样的也会生成MappedStatement对象。
            parser.parse();
            loadCompleted = true;
          } finally {
            if (!loadCompleted) {
              knownMappers.remove(type);
            }
          }
        }
      }
    

    我们看到,核心是把类型,和代理工厂放入到了Map中保存了起来。接着解析接口上面的sql。至此。build代码解析完了,核心关键点都点了出来。但是还有一个小细节,我们看到解析完,返回的configuration对象。然后通过buid()方法,build出来的是DefaultSqlSessionFactory。

      public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
      }
    

    2.2 Step2

    从openSession()方法点进去,直接看看是如何开启一个会话的。(上面说到了,生成的是DefaultSqlSessionFactory, 别进错了)

      private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
          // 获取数据库信息,已经解析完。
          final Environment environment = configuration.getEnvironment();
          // 生成TransactionFactory
          final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
          // 创建事务
          tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
          // 创建执行器
          final Executor executor = configuration.newExecutor(tx, execType);
          // 生成SqlSession
          return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
          closeTransaction(tx); // may have fetched a connection so lets call close()
          throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    
    2.2.1 Executor

    这个里面有生成SqlSession的具体不走,前面没啥好看的,就是从解析出来的configuration中获取信息。 我们直接从创建执行器开始。

      public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        // 获取需要创建的执行器的类型,
        executorType = executorType == null ? defaultExecutorType : executorType;
        // 有可能默认类型也为空,所以多判断了一次。
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        if (ExecutorType.BATCH == executorType) {
          executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
          executor = new ReuseExecutor(this, transaction);
        } else {
          // 如果没有输入类型,则默认创建的是 SimpleExecutor
          executor = new SimpleExecutor(this, transaction);
        }
        if (cacheEnabled) {
          // 如果开启了二级缓存,则会在对执行器进行包装
          executor = new CachingExecutor(executor);
        }
        // 拦截器连
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
      }
    

    我们可以看到,执行器有三种类型,分别是BatchExecutorReuseExecutorSimpleExecutor(就不介绍三种执行器了)。默认采用的是SimpleExecutor。

    2.2.2 SqlSession

    我们看代码,发现生成DefaultSqlSession传入了Executor,在sqlsession中调用方法时,具体的执行逻辑是通过executor执行的。那一个构造函数和查询方法看一下。

      public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
        this.configuration = configuration;
        this.executor = executor;
        this.dirty = false;
        this.autoCommit = autoCommit;
      }
    
      public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) {
        try {
          MappedStatement ms = configuration.getMappedStatement(statement);
          // 通过执行器来执行查询
          Cursor<T> cursor = executor.queryCursor(ms, wrapCollection(parameter), rowBounds);
          registerCursor(cursor);
          return cursor;
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    

    2.3 Step3

    通过SqlSession获取Mapper对象。 这个就得好好说道说道了。明明是接口,为什么可以返回对象?我们从代码中,一直点下去,直接发现代码。

      public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        // 从最开始解析保存接口和代理工厂的map中,获取对象的MapperProxyFactory
        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);
        }
      }
    

    我们发现getMapper,最后返回的是代理对象,这也就是为什么明明是接口,但是可以返回对象。但是为了明白具体逻辑,我们还是要进去看看具体是如何实现的,以及如何是把MappedStatement和接口关联起来的。

    	public T newInstance(SqlSession sqlSession) {
        // 创建 MapperProxy ,
        final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
      }
    
    public class MapperProxy<T> implements InvocationHandler, Serializable {
      // 这是一个InvocationHandler,代理对象具体执行的逻辑
      ....
        @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
          } else if (isDefaultMethod(method)) {
            return invokeDefaultMethod(proxy, method, args);
          }
        } catch (Throwable t) {
          throw ExceptionUtil.unwrapThrowable(t);
        }
        // 这个里面有核心,方法是如何和Mapper关联起来的
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        // 具体执行逻辑
        return mapperMethod.execute(sqlSession, args);
      }
    }
    

    看到这, 我们应该都明白了,接口调用的方法,通过走代理,然后在代理对象中进行逻辑处理。 这个时候,我们就要进cachedMapperMethod(method);,这里是核心。

      private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = methodCache.get(method);
        if (mapperMethod == null) {
          // 构造MapperMethod 对象, 这个里面就是具体的关联逻辑。
          mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
          methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
      }
    	
    	// MapperMethod的构造函数
      public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
        // 关联逻辑
        this.command = new SqlCommand(config, mapperInterface, method);
        this.method = new MethodSignature(config, mapperInterface, method);
      }
    

    这段代码是构建MapperMethod, 关键的逻辑在构造函数里面。我们来看SqlCommand的构造函数

       public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
          final String methodName = method.getName();
          final Class<?> declaringClass = method.getDeclaringClass();
         // 核心逻辑,顾名思义,解析mappedStatement.并且是通过Method.
          MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
              configuration);
        	...
        }
    

    到这,我们就得就去瞅瞅了,是如何解析MappedStatement的,关联接口方法和MappedStatement.

    private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
            Class<?> declaringClass, Configuration configuration) {
      		// statementId 
          String statementId = mapperInterface.getName() + "." + methodName;
      
          if (configuration.hasStatement(statementId)) {
            // 通过statementId获取存入的mappedStatement
            return configuration.getMappedStatement(statementId);
          } else if (mapperInterface.equals(declaringClass)) {
            return null;
          }
          for (Class<?> superInterface : mapperInterface.getInterfaces()) {
            if (declaringClass.isAssignableFrom(superInterface)) {
              // 父类接口。逻辑一样
              MappedStatement ms = resolveMappedStatement(superInterface, methodName,
                  declaringClass, configuration);
              if (ms != null) {
                return ms;
              }
            }
          }
          return null;
        }
    

    看到这,就知道了,是通过方法的类名+方法名,组成statementId,然后去早已经解析的MappedStatement中,找到对应的MappedStatment,就关联起来啦。

    2.4 Step4

    执行方法。通过上面分析,我们已经知道是通过代理,找到对应的MappedStatement,然后执行具体的sql.

    3 完结撒花

    ooook, 现在已经搞完全部的流程啦,要自己在捋顺啦,钢巴得!

    挂上GitHub代码地址,拉下来,在本地Debug起来。

  • 相关阅读:
    [翻译] Blocks and Variables
    UIView独占响应事件
    iOS中block类型大全
    Erlang入门(二)—并发编程
    Erlang入门(一)
    学习Erlang--1、入门
    一位Erlang程序员的自白
    安装ejabberd2并配置MySQL为其数据库
    JDBC 与ODBC的区别
    ejabberd的多域名(domain)设置
  • 原文地址:https://www.cnblogs.com/lifacheng/p/13216572.html
Copyright © 2020-2023  润新知