mybatis-mapper执行流程解析

139 阅读12分钟
  • 什么是mybatis?

    • Mybatis是一款SQL映射框架,是对JDBC的封装实现。可以将SQL写在xml文件或通过注解方式来完成数据库结果集对象到java对象的转换,所以是半自动化的ORM框架。

mybatis源码学习.png

  • mybatis中的一级缓存与二级缓存?

    • mybatis的二级缓存实现了SqlSession之间的数据共享,同时粒度到达namespace级别,也更加可控;但在生成环境不会采用缓存机制(最大的原因就是数据一致性的问题,只当做一个ORM框架进行使用)

    • mybatis提供了大致三类的配置

      • setting配置 cacheEnabled与localCacheScope属性配置,cacheEnabled缓存生效的总开关;localCacheScope默认session级别(一级缓存),会缓存一个会话中执行的所有查询;STATEMENT级别会清空本地缓存;
        <settings>
            <setting name="cacheEnabled" value="true"/>
            <setting name="localCacheScope" value="SESSION"/>
        </settings>
        
      • MappedStatement配置即XML映射文件select标签中的flushCache和useCache属性 flushCache会清空一级缓存和二级缓存,useCache会将本条执行语句结果缓存进二级缓存中
      • cache标签或者cache-ref标签,对给定命名空间启用二级缓存配置
        <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
        
    • 一级缓存

      • flushCache开关开启时,或清空一级缓存或者当缓存范围配置是STATEMEN时,会清空本地缓存(一级缓存PerpetualCache类实际是HashMap)详见BaseExecutor.query()方法
      public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
       ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
       if (closed) {
         throw new ExecutorException("Executor was closed.");
       }
       if (queryStack == 0 && ms.isFlushCacheRequired()) {
         clearLocalCache();
       }
       List<E> list;
       try {
         queryStack++;
         // 从缓存中获取结果
         list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
         if (list != null) {
           handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
         } else {
           // 缓存中获取不到,则调用queryFromDatabase()方法从数据库中查询
           list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
         }
       } finally {
         queryStack--;
       }
       if (queryStack == 0) {
         for (DeferredLoad deferredLoad : deferredLoads) {
           deferredLoad.load();
         }
         // issue #601
         deferredLoads.clear();
         if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
           // issue #482
           clearLocalCache();
         }
       }
       return list;
      }
      
    • 二级缓存

      • cacheEnabled开启时,创建Executor实例时,会生成该实例的装饰者对象CachingExecutor,当设置了cache标签时,就会开启二级缓存,所以二级缓存是基于namespace级别的

        public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
            throws SQLException {
          // 获取MappedStatement对象中维护的二级缓存对象
          Cache cache = ms.getCache();
          if (cache != null) {
            // 判断是否需要刷新二级缓存
            flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
              ensureNoOutParams(ms, boundSql);
              // 从MappedStatement对象对应的二级缓存中获取数据
              @SuppressWarnings("unchecked")
              List<E> list = (List<E>) tcm.getObject(cache, key);
              if (list == null) {
                // 如果缓存数据不存在,则从数据库中查询数据
                list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // 將数据存放到MappedStatement对象对应的二级缓存中
                tcm.putObject(cache, key, list); // issue #578 and #116
              }
              return list;
            }
          }
          return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        }
        

缓存执行流程.png

  • mybatis中的主键策略是怎么样的?

    • mybatis中提供了三种KeyGenerator接口的实现类分别是:Jdbc3KeyGenerator,SelectKeyGenerator,NoKeyGenerator

      • Jdbc3KeyGenerator 主要用于数据库自增主键
      • NoKeyGenerator 默认空实现,不对主键单独处理
      • SelectKeyGenerator 主要解决插入数据不支持主键自动生成的问题
    • KeyGenerator中有两个方法,在创建StatementHandler实例时,在其父类的构造方法中会获取当前MappedStatement当中的主键策略对象KeyGenerator,并执行processBefore方法,完成主键的生成;当Statement执行完数据库操作时StatementHandler会获取当前MappedStatement当中的主键策略对象KeyGenerator,并执行processAfter方法;

      public class NoKeyGenerator implements KeyGenerator {
      
        /**
         * A shared instance.
         * @since 3.4.3
         */
        public static final NoKeyGenerator INSTANCE = new NoKeyGenerator();
      
      
         //processBefore在操作数据库之前时会调用该方法
        @Override
        public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
          // Do Nothing
        }
          
         //在数据执行操作后,会调用该方法
        @Override
        public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
          // Do Nothing
        }
      
      }
      
    • 如何实现自定义主键生成策略

      • 通过实现Mybatis中Interceptor接口实现自定义拦截器插件对Executor.class的update方法进行拦截,对MappedStatement对象中对参数列表进行处理;

      • 通过对Executor.class拦截实现

        /**
         * 自定义主键生成器
         */
        
        @Intercepts({@Signature(type = Executor.class,method = "update",args = {MappedStatement.class, Object.class})})  //通过拦截Executor.update方法
        @Component
        public class MyKeyGeneratorInterceptor implements Interceptor {
        
            //自定义主键生成策略
            private IDStrategy  idStrategy = new IDStrategy();
        
            @Override
            public Object intercept(Invocation invocation) throws Throwable {
                Object[] args = invocation.getArgs();
                if (args.length == 2 && args[1] != null) {
                    //此处判断需要注入的主键的属性即可
                    if(args[1] instanceof Bank){
                        Bank bank = (Bank) args[1];
                        bank.setCoreMerNo(idStrategy.getMyId());
                    }
                }
        
                return invocation.proceed();
            }
        
            @Override
            public Object plugin(Object target) {
                // 调用Plugin类的wrap()方法返回一个动态代理对象
                return Plugin.wrap(target, this);
            }
        
            @Override
            public void setProperties(Properties properties) {
            }
        }
        
  • mybatis中动态SQL的原理是什么,以及#{}与${}的区别是什么?

    • #实现了预编译,会先把#{变量}编译成?然后在执行时取值替换,可以防止sql注入;$是直接进行字符串替换
    • ${}主要应用于传入参数时sql片段的场景下,进行字符串替换,完成sql拼接;但不建议使用,sql注入问题存在风险;

    • #{}与${}符号的解析过程

      • XMLStatementBuilder解析<select|update|...>标签创建MappedStatement对象时通过LanguageDriver进行sql解析并生成SqlSource对象;
      public class XMLLanguageDriver implements LanguageDriver {
      
        @Override
        public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
          return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
        }
        @Override
        public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
          // 该方法用于解析XML文件中配置的SQL信息
          // 创建XMLScriptBuilder对象
          XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
          // 调用 XMLScriptBuilder对象parseScriptNode()方法解析SQL资源
          return builder.parseScriptNode();
        }
      
        @Override
        public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
          // 该方法用于解析Java注解中配置的SQL信息
          // 字符串以<script>标签开头,则以XML方式解析
          if (script.startsWith("<script>")) {
            XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
            return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
          } else {
            // 解析SQL配置中的全局变量
            script = PropertyParser.parse(script, configuration.getVariables());
            TextSqlNode textSqlNode = new TextSqlNode(script);
            // 如果SQL中是否仍包含${}参数占位符,则返回DynamicSqlSource实例,否则返回RawSqlSource
            if (textSqlNode.isDynamic()) {
              return new DynamicSqlSource(configuration, textSqlNode);
            } else {
              return new RawSqlSource(configuration, script, parameterType);
            }
          }
        }
      
      }
      
      • 通过XMLScriptBuilder来解析xml中的sql,先对XNode对象转换为SqlNode,如果存在${}或存在 if,where,trim等标签时则该Sql就为动态sql,如果子元素为<if、<where等标签,则使用对应的NodeHandler处理。

      • 通过是否包含动态SQL,如果是创建DynamicSqlSource对象,否则创建RawSqlSource对象;

        public SqlSource parseScriptNode() {
          // 调用parseDynamicTags()方法將SQL配置转换为SqlNode对象
          MixedSqlNode rootSqlNode = parseDynamicTags(context);
          SqlSource sqlSource = null;
          // 判断Mapper SQL配置中是否包含动态SQL元素,如果是创建DynamicSqlSource对象,否则创建RawSqlSource对象
          if (isDynamic) {
            sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
          } else {
            sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
          }
          return sqlSource;
        }
        
        protected MixedSqlNode parseDynamicTags(XNode node) {
          List<SqlNode> contents = new ArrayList<SqlNode>();
          NodeList children = node.getNode().getChildNodes();
          // 对XML子元素进行遍历
          for (int i = 0; i < children.getLength(); i++) {
            XNode child = node.newXNode(children.item(i));
            // 如果子元素为SQL文本内容,则使用TextSqlNode描述该节点
            if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
              String data = child.getStringBody("");
              TextSqlNode textSqlNode = new TextSqlNode(data);
              // 判断SQL文本中包含${}参数占位符,则为动态SQL
              if (textSqlNode.isDynamic()) {
                contents.add(textSqlNode);
                isDynamic = true;
              } else {
                // 如果SQL文本中不包含${}参数占位符,则不是动态SQL
                contents.add(new StaticTextSqlNode(data));
              }
            } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
              // 如果子元素为<if>、<where>等标签,则使用对应的NodeHandler处理
              String nodeName = child.getNode().getNodeName();
              
              //nodeHandlerMap mybatis中动态sql标签的map集合
              NodeHandler handler = nodeHandlerMap.get(nodeName);
              if (handler == null) {
                throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
              }
              handler.handleNode(child, contents);
              isDynamic = true;
            }
          }
          return new MixedSqlNode(contents);
        }
        
      • 通过SqlSource中的getBoundSql方法,调用StaticSqlSource对象的getBoundSql()方法,获得BoundSql实例;BoundSql是对Executor组件执行sql的再封装,里面存在解析后的sql语句以及Mapper参数映射信息,参数对象信息;

动态sql解析流程.png

  • mybatis是怎么进行分页的

    • 分页查询一般分两种,第一种数据库的分页语句进行物理分页,另外就是将数据全部查询出来在应用程序中进行内存进行;

    • mybatis可以通过插件的方式,修改sql执行语句从而进行物理分页;我们知道Executor组件是通过StatementHandler来进行JDBC的交互的,并且通过StatementHandler.prepare方法生成jdbc的Statement对象的,所以我们只需要拦截StatementHandler的prepare方法进行拦截后,获取到BoundSql对象后得到已经解析设置值后的sql后拼接分页sql片段完成自动分页原理;

      /**
       * 自定义分页插件-mybatis实际是通过RoutingStatementHandler对象(策略模式)
       */
      @Intercepts({@Signature(type = StatementHandler.class,method = "prepare",args = {PreparedStatement.class})}) //ParameterHandler.setParameters方法
      @Component
      public class MyPageInterceptor implements Interceptor {
      
      
          @Override
          public Object intercept(Invocation invocation) throws Throwable {
      
              RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
              BoundSql boundSql = handler.getBoundSql();
              Object pageObject = boundSql.getParameterObject();
              if (pageObject instanceof Bank) {  //此处可以自定义分页对象即可
                  String sql = boundSql.getSql() + "limit 1,10";
                  //通过反射设置对应BoundSql属性 sql值
              }
      
              return invocation.proceed();
          }
      
          @Override
          public Object plugin(Object target) {
              // 调用Plugin类的wrap()方法返回一个动态代理对象
              return Plugin.wrap(target, this);
          }
      
          @Override
          public void setProperties(Properties properties) {
      
          }
      }
      
  • mybatis是怎么实现接口mapper的绑定?

    • mybatis实现接口绑定有两种实现方式;基于Xml文件方式和注解方式

    • 基于XML实现接口绑定原理

      • 在XMLConfigBUilder解析mybatis的主配置文件时,会对<Mappers标签下的mapper进行解析,通过包名(package标签),mapper标签下的resource网络文件或class属性指定的接口路径进行扫描;并生成MapperProxyFctory对象存到Configretion容器中MapperRegistry的Map<Class,MapperProxyFactory> knownMappers里面;
      private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
          for (XNode child : parent.getChildren()) {
            // 通过<package>标签指定包名
            if ("package".equals(child.getName())) {
              String mapperPackage = child.getStringAttribute("name");
              configuration.addMappers(mapperPackage);
            } else {
              String resource = child.getStringAttribute("resource");
              String url = child.getStringAttribute("url");
              String mapperClass = child.getStringAttribute("class");
              // 通过resource属性指定XML文件路径
              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) {
                // 通过url属性指定XML文件路径
                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属性指定接口的完全限定名
                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.");
              }
            }
          }
        }
      }
      
      • 调用XMLMapperBuilder.parse方法对XML文件当中的sql进行解析,解析顺序如下注释;最后XMLStatementBuilder.parseStatementNode完成select|update|insert|delete标签的解析,并生成MappedStatement对象放入configration容器的Map<<String,MappedStatement> mappedStatement map当中,key为namespace + 方法id;这也是为什么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");
          }
          // 设置当前正在解析的Mapper配置的命名空间
          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|insert|update|delete>标签
          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);
        }
      }
      
      //通过XMLStatementBuilder对象,对<select|update|insert|delete>标签进行解析
      public void parseStatementNode() {
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");
      
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
          return;
        }
        // 解析<select|update|delete|insert>标签属性
        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");
        // 获取LanguageDriver对象
        String lang = context.getStringAttribute("lang");
        LanguageDriver langDriver = getLanguageDriver(lang);
        // 获取Mapper返回结果类型Class对象
        Class<?> resultTypeClass = resolveClass(resultType);
        String resultSetType = context.getStringAttribute("resultSetType");
        // 默认Statement类型为PREPARED
        StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType",
                StatementType.PREPARED.toString()));
        ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
      
        String nodeName = context.getNode().getNodeName();
        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);
      
        // 將<include>标签内容,替换为<sql>标签定义的SQL片段
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        includeParser.applyIncludes(context.getNode());
      
        // 解析<selectKey>标签
        processSelectKeyNodes(id, parameterTypeClass, langDriver);
      
        // 通过LanguageDriver解析SQL内容,生成SqlSource对象
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        String resultSets = context.getStringAttribute("resultSets");
        String keyProperty = context.getStringAttribute("keyProperty");
        String keyColumn = context.getStringAttribute("keyColumn");
        KeyGenerator keyGenerator;
        String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        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;
        }
      
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered, 
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
      }
      
      
      • 当利用SqlSession获取某个Mapper对象时是通过MapperProxyFactory.newInstance生成MapperProxy动态代理对象(JDK的动态代理);所以当执行mapper方法时会调用MapperProxy.invoke方法去执行(实际通过MapperMethod.excute方法通过方法的全路径名称找到MappedStatement判断SQL执行类型后,去执行对应SqlSession的api方法,最后通过Execto执行器传入对应的MappedStatement完成一直sql执行)

        public Object execute(SqlSession sqlSession, Object[] args) {
            Object result;
            // 其中command为MapperMethod构造是创建的SqlCommand对象
            // 获取SQL语句类型
            switch (command.getType()) {
                case INSERT: {
                    // 获取参数信息
                    Object param = method.convertArgsToSqlCommandParam(args);
                    // 调用SqlSession的insert()方法,然后调用rowCountResult()方法统计行数
                    result = rowCountResult(sqlSession.insert(command.getName(), param));
                    break;
                }
                case UPDATE: {
                    Object param = method.convertArgsToSqlCommandParam(args);
                    // 调用SqlSession对象的update()方法
                    result = rowCountResult(sqlSession.update(command.getName(), param));
                    break;
                }
                case DELETE: {
                    Object param = method.convertArgsToSqlCommandParam(args);
                    result = rowCountResult(sqlSession.delete(command.getName(), param));
                    break;
                }
                case SELECT:
                    if (method.returnsVoid() && method.hasResultHandler()) {
                        executeWithResultHandler(sqlSession, args);
                        result = null;
                    } else if (method.returnsMany()) {
                        result = executeForMany(sqlSession, args);
                    } else if (method.returnsMap()) {
                        result = executeForMap(sqlSession, args);
                    } else if (method.returnsCursor()) {
                        result = executeForCursor(sqlSession, args);
                    } else {
                        Object param = method.convertArgsToSqlCommandParam(args);
                        result = sqlSession.selectOne(command.getName(), param);
                        if (method.returnsOptional() &&
                                (result == null || !method.getReturnType().equals(result.getClass()))) {
                            result = OptionalUtil.ofNullable(result);
                        }
                    }
                    break;
                case FLUSH:
                    result = sqlSession.flushStatements();
                    break;
                default:
                    throw new BindingException("Unknown execution method for: " + command.getName());
            }
            if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
                throw new BindingException("Mapper method '" + command.getName()
                        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
            }
            return result;
        }
        
        
    • 基于注解实现

      • 当将Mapper接口Class对象存到存到Configretion容器中MapperRegistry的Map<Class,MapperProxyFactory> knownMappers里面是(addMapper方法时)会调用MapperAnnotationBuildr.parse方法对接口方法上的注解进行解析,最终为每个存在注解的方法生成MappedStament对象,存入configration容器的Map<<String,MappedStatement> mappedStatement map当中
    • 结论:mybatis将mapper接口通过MapperProxyFactoy动态代理生成MapperProxy代理对象,同时将对应的SQL文件解析为MappedStatement对象存放着Configration容器中的map里,最有通过类的全路径名+方法名完成mapper接口方法的绑定,所以namespace + sql_id == class报名+方法名称实现sql与方法的绑定;

  • 如何执行批量插入?

    • 使用mybatis执行插入时可以有三种方式,1.单条插入循环执行;2.采用foreach标签构建批量插入语句;3.采用mybatis批处理模式进行插入;
    • 单条插入循环执行

      • 采用单条插入语句,在程序中循环执行插入
    • 采用foreach标签构建批量插入语句

      • 当Executor执行器为Simple(默认)时,会为每一个语句创建一个PreparedStatement对象;
      • 且当数据量大时,解析后的sql语句会特别大会导致一次性插入的数据包特别大(超过max_allowed_packet)时会报错;
      • 并且无法返回自动生成的主键
    • mybatis批处理模式进行插入

      • 将Executor执行器设置为BATCH模式,在同一个SqlSession中会对Statement对象进行缓存,最终调用JDBC中的批处理模式addBatch()方法,减少了网络IO请求损耗。
  • mybatis是怎么整合spring?

    • mybati是怎么将Mapper的动态代理对象注册到Spring容器中的?

      • 在日常开发中,Mapper是作为一个单例的bean注册进spring容器中的;mybatis提供了插件包完成了与spring框架的整合mybatis-spring;
      • mybatis是通过实现自己的MapperFactoryBean实现FactoryBean接口完成mapper注入的,然后通过扫描ClassPathMapperScanner扩展了spring的ClassPathBeanDefinitionScanner扫描器,完成了包扫描,并将Mapper代理对象MapperProxy通过注册BeanDefinition对象完成了容器注入;------ 见后续spring学习
    • mybatis整合spring是怎么实现事务管理的?

      • spring提供了两种事务管理机制;1声明式事务管理(主要通过aop实现一个切面完成)和编程式事务管理。
      • mybatis中可以通过Transaction组件完成获取JDBC的Connection对象和对事务进行提交或者回滚操作;Mybatis中有两个Transaction的实现,分别是JdbcTransaction和ManagedTransaction;
      • JdbcTransaction事务管理器通过JDBC的方式进行简单的事务提交和回滚并不处理异常,所以需要程序自己手动处理这些异常;
      • ManagedTransaction管理器表明mybatis自己不进行事务管理,事务管理交由其他框架进行处理;
      • mybatis整合spring事务时,最主要的时怎么保证操作数据库的Connection和事务提交或回滚的是同一个;而在mybatis-spring插件包中时,spring事务管理器中,Connection对象的获取和释放都是通过DataSourceUtils工具类获取Connection对象,所以可以通过SpringManagedTransaction来整合spring的事务管理;------ 见后续spring学习