Mybatis源码解析一-mybatis与Spring时如何整合的

35次阅读

共计 15225 个字符,预计需要花费 39 分钟才能阅读完成。

Mybatis 源码解析(一) —— mybatis 与 Spring 时如何整合的?

  从大学开始接触 mybatis 到现在差不多快 3 年了吧,最近寻思着使用 3 年了,我却还不清楚其内部实现细节,比如:

  • 它是如何加载各种 mybatis 相关的 xml?
  • 它是如何仅仅通过一个 Mapper 接口 + Mappe.xml 实现数据库操作的(尽管很多人可能都清楚是通过代理实现,但面试时一旦深入询问:比如 Mapper 的代理类名是什么?是通过 JDK 还是 cglib 实现?)?
  • 在同一个方法中,Mybatis 多次请求数据库,是否要创建多个 SqlSession 会话?
  • 它与 Spring 是如何适配 (整合) 的?
  • 在 Spring 中是如何保障 SqlSession 的生命周期的?
  • 等等一系列的问题。。。

  如果以上问题你自认为无法回答,或者说了解一些,那么就从现在开始,我们来一一揭开这层面纱。

一、Mybatis:最简单测试 Demo

  相信只要用过 Mybatis 的同学看到下面的代码一定不会陌生,如果不清楚的可以看下官网文档


    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    // 1、目前流行方式
    try (SqlSession session = sqlSessionFactory.openSession()) {UserMapper mapper = session.getMapper(UserMapper.class);
        User user = mapper.selectById(101);
    }
    // 2、以前流行方式
    try (SqlSession session = sqlSessionFactory.openSession()) {User user =  sqlSession.selectOne("xxx.UserMapper.selectById", "101");;
    }    
    

  示列代码演示了 Mybatis 进行一次数据库操作的过程,大致分为(针对目前流行方式,其实以前的使用和目前流行的使用方式实现原理一样):

  • 1、通过 SqlSessionFactoryBuilder 将读取到的配置资源 build 生成 SqlSessionFactory
  • 2、通过 SqlSessionFactory 的 openSession() 获取到 SqlSession
  • 3、通过 SqlSession 获取到 Mapper 的代理对象(MapperProxy)
  • 4、通过 Mapper 进行 数据库请求操作

SqlSession、SqlSessionFactory、SqlSessionFactoryBuilder

  我们可以轻易的发现每次去请求数据库操作都需要通过 SqlSessionFactory 去获取到 SqlSession,而 SqlSessionFactory 是通过 SqlSessionFactoryBuilder 构造出来的,并且最后请求操作完成后都关闭了 SqlSession。因此,不难得出:

  • SqlSessionFactory 一个应用程序中最好只有 1 个,即单列。
  • SqlSessionFactoryBuilder 只有一个作用:创建 SqlSessionFactory 对象。
  • 一个 SqlSession 应该仅存活于一个业务请求中,也可以说一个 SqlSession 对应一次数据库会话,它不是永久存活的,每次访问数据库时都需要创建它,并且访问完成后都必须执行会话关闭

  针对这 3 个类以及 mapper 的作用域(Scope)和生命周期的描述,个人觉得官方文档写得很清楚:

SqlSessionFactoryBuilder

这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但是最好还是不要让其一直存在,以保证所有的 XML 解析资源可以被释放给更重要的事情。

SqlSessionFactory

SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏味道(bad smell)”。因此 SqlSessionFactory 的最佳作用域是应用作用域。有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。

SqlSession

每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。如果你现在正在使用一种 Web 框架,要考虑 SqlSession 放在一个和 HTTP 请求对象相似的作用域中。换句话说,每次收到的 HTTP 请求,就可以打开一个 SqlSession,返回一个响应,就关闭它。这个关闭操作是很重要的,你应该把这个关闭操作放到 finally 块中以确保每次都能执行关闭。
依赖注入框架可以创建线程安全的、基于事务的 SqlSession 和映射器,并将它们直接注入到你的 bean 中,因此可以直接忽略它们的生命周期。如果对如何通过依赖注入框架来使用 MyBatis 感兴趣,可以研究一下 MyBatis-Spring 或 MyBatis-Guice 两个子项目。

映射器实例(Mapper 实例)
映射器是一些由你创建的、绑定你映射的语句的接口。映射器接口的实例是从 SqlSession 中获得的。因此从技术层面讲,任何映射器实例的最大作用域是和请求它们的 SqlSession 相同的。尽管如此,映射器实例的最佳作用域是方法作用域。也就是说,映射器实例应该在调用它们的方法中被请求,用过之后即可丢弃。并不需要显式地关闭映射器实例,尽管在整个请求作用域保持映射器实例也不会有什么问题,但是你很快会发现,像 SqlSession 一样,在这个作用域上管理太多的资源的话会难于控制。为了避免这种复杂性,最好把映射器放在方法作用域内。就像示列代码一样。
如果 SqlSession 是注入的,那么映射器实例也可通过依赖注入,并且可忽略其生命周期。

二、Mybatis-Spring:将 MyBatis 代码无缝地整合到 Spring

  前面是学习 mybatis 常看到的一种代码,但缺点也很明显: 每次请求都得创建 SqlSession,并且 Mapper 的代理类是通过 SqlSession 获取(说明耦合度很高),也就意味着每次请求都得创建一个新的 Mapper 代理类。为了整合 Spring,并且解决前面问题,所以 Mybatis-Spring 子项目来袭。

MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中。它将允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapper 和 SqlSession 并注入到 bean 中

  上面是 Mybatis-Spring 的官方介绍,其中 允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapper 和 SqlSession 并注入到 bean 中 是我们本次解析的关键点。那么开始分析吧!

SqlSessionFactoryBean、MapperScannerConfigurer

  在 Spring 项目中应用了 Mybatis 都会有下面的 2 个 bean 配置,这 2 个配置就是实现 xml 加载、mapper 和 SqlSession 注入的起始配置。


    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="mapperLocations" value="classpath:mapper/*.xml"></property>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>

    <!-- DAO 接口所在包名,Spring 会自动查找其下的类 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="xxx.dao"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
    </bean>

一、SqlSessionFactoryBean:加载 xml 及 build SqlSessionFactory 对象

  从配置中我们可以看的 SqlSessionFactoryBean 配置了数据源、mapper 的 xml 路径、mybatis-config 的 xml 路径。因此,不难想象,SqlSessionFactoryBean 内部实现了 xml 配置文件的加载及 SqlSessionFactory 对象的创建。我们来看下 SqlSessionFactoryBean 继承关系图形:

在继承关系图中,我们发现了 InitializingBean、FactoryBean 的身影,可能清楚这个的同学,大概已经猜到了肯定有 afterPropertiesSet() 来创建 SqlSessionFactory 对象 和 getObject() 来获取 SqlSessionFactory 对象。话不多说,先看下 getObject()实现:


  public SqlSessionFactory getObject() throws Exception {if (this.sqlSessionFactory == null) {afterPropertiesSet();
    }

    return this.sqlSessionFactory;
  }
  

  getObject()相对简单,我们都知道 FactoryBean 子类都是通过 getObject()来获取到实际的 Bean 对象,这里也就是 SqlSessionFactory。从源码中我们看到当 sqlSessionFactory 为 null 会去调用 afterPropertiesSet(),所以 SqlSessionFactory 肯定是由 afterPropertiesSet() 来实现创建的。继续看 afterPropertiesSet()实现:

  public void afterPropertiesSet() throws Exception {notNull(dataSource, "Property'dataSource'is required");
    notNull(sqlSessionFactoryBuilder, "Property'sqlSessionFactoryBuilder'is required");

    this.sqlSessionFactory = buildSqlSessionFactory();}

  afterPropertiesSet() 内部首先 验证了 dataSource 和 sqlSessionFactoryBuilder 部位 null,最后调用 buildSqlSessionFactory()方法获取到 SqlSessionFactory 对象,并赋值到类字段属性 sqlSessionFactory。继续查看 buildSqlSessionFactory()源码:


protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

    // 省略了 SqlSessionFactoryBean 的属性(比如:ObjectFactory)赋值到 Configuration 对象中的操作
    //  1 Configuration : Mybatis 的核心类之一,主要存放读取到的 xml 数据, 包括 mapper.xml 
    Configuration configuration;

    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configLocation != null) {
      //  2 创建  xmlConfigBuilder 对象 : 用于解析 mybatis-config.xml 数据
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      configuration = xmlConfigBuilder.getConfiguration();} else {if (logger.isDebugEnabled()) {logger.debug("Property'configLocation'not specified, using default MyBatis Configuration");
      }
      configuration = new Configuration();
      configuration.setVariables(this.configurationProperties);
    }

    if (xmlConfigBuilder != null) {
      try {
        //  3  XmlConfigBuilder 解析方法执行 
        xmlConfigBuilder.parse();} catch (Exception ex) {throw new NestedIOException("Failed to parse config resource:" + this.configLocation, ex);
      } finally {ErrorContext.instance().reset();}
    }
    
    if (!isEmpty(this.mapperLocations)) {for (Resource mapperLocation : this.mapperLocations) {if (mapperLocation == null) {continue;}
        try {
          //  4 创建  XMLMapperBuilder 对象 : 用于解析 mapper.xml 数据
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              configuration, mapperLocation.toString(), configuration.getSqlFragments());
          xmlMapperBuilder.parse();} catch (Exception e) {throw new NestedIOException("Failed to parse mapping resource:'" + mapperLocation + "'", e);
        } finally {ErrorContext.instance().reset();}
      }
    } 
    // 5 通过 SqlSessionFactoryBuilder bulid  SqlSessionFactory 对象
    return this.sqlSessionFactoryBuilder.build(configuration);
  }

   整个 buildSqlSessionFactory() 源码主要有以下几个重要的点:

  • 1、XMLConfigBuilder,通过调用其 parse() 方法来 解析 mybatis-config.xml 配置(如果 配置有 mapper.xml,其会通过 XMLMapperBuilder 进行解析加载),并将解析的数据赋值到 Configuration(Mybatis 的核心类之一,主要存放读取到的 xml 数据, 包括 mapper.xml,该类贯穿整个 mybatis,足以见得其重要性)
  • 2、XMLMapperBuilder : 通过调用其 parse() 方法来 解析 mapper.xml 配置,并将解析的数据赋值到 Configuration
  • 3、将存放有解析数据的 Configuration 作为 sqlSessionFactoryBuilder.build() 参数,创建 sqlSessionFactory 对象。

至此

二、MapperScannerConfigurer:扫描 Mapper 接口路径,将 Mapper 偷梁换柱成 MapperFactoryBean

  MapperScannerConfigurer 是 mybatis-spring 项目中为了实现方便加载 Mapper 接口,以及将 Mapper 偷梁换柱成 MapperFactoryBean。查看 MapperScannerConfigurer 源码,先看下其继承关系图:

   从中我们其继承了 BeanDefinitionRegistryPostProcessor 接口,熟悉 Spring 的同学应该 已经大致想到了 其如何将 Mapper 偷梁换柱成 MapperFactoryBean 了。话不多说,我们来看看 MapperScannerConfigurer 是如何实现 BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry 方法:

 public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {if (this.processPropertyPlaceHolders) {processPropertyPlaceHolders();
    }

    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.setAddToConfig(this.addToConfig);
    scanner.setAnnotationClass(this.annotationClass);
    scanner.setMarkerInterface(this.markerInterface);
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
    scanner.setResourceLoader(this.applicationContext);
    scanner.setBeanNameGenerator(this.nameGenerator);
    scanner.registerFilters();
    scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

   我们可以发现整个方法内部其实就是通过 ClassPathMapperScanner 的 scan() 方法,查看 scan() 实现,发现其内部调用了关键方法 doScan(), 那么我们来看下 doScan() 方法实现:


  @Override
  public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    // 1、调用父类 ClassPathBeanDefinitionScanner 的 doScan 方法 加载路径下所有的 mapper 接口生成对应的 BeanDefinition 
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {logger.warn("No MyBatis mapper was found in'" + Arrays.toString(basePackages) + "'package. Please check your configuration.");
    } else {for (BeanDefinitionHolder holder : beanDefinitions) {GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();

        // 2、设置 被代理的 Bean(也就是 Mapper)的 class 信息
        definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());
        // 3、偷梁换柱成 MapperFactoryBean 
        definition.setBeanClass(MapperFactoryBean.class);

        definition.getPropertyValues().add("addToConfig", this.addToConfig);

        boolean explicitFactoryUsed = false;
        // 4、设置 sqlSessionFactory 
        if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
          explicitFactoryUsed = true;
        } else if (this.sqlSessionFactory != null) {definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
          explicitFactoryUsed = true;
        }
        
        // 5、设置 sqlSessionTemplate
        if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
          explicitFactoryUsed = true;
        } else if (this.sqlSessionTemplate != null) {definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
          explicitFactoryUsed = true;
        }
        
        if (!explicitFactoryUsed) {definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        }
      }
    }

    return beanDefinitions;
  }

   整个方法分为 3 个部分:

  • 1、调用父类 ClassPathBeanDefinitionScanner 的 doScan()方法 加载路径下所有的 mapper 接口生成对应的 BeanDefinition
  • 2、通过 definition.setBeanClass(MapperFactoryBean.class) 偷梁换柱成 MapperFactoryBean
  • 3、通过 definition.getPropertyValues().add() 添加 MapperFactoryBean 所需的 字段或者方法参数信息:sqlSessionFactory、mapperInterface 等

   至此 MapperScannerConfigurer 的使命已经完成,至于 MapperFactoryBean 的创建就完全交给 Spring 来完成了。

三、MapperFactoryBean、SqlSessionTemplate:Mapper 与 SqlSession 解耦的利器

   我们知道在 mybatis 中,Mapper 是通过 SqlSession 创建的,而 SqlSession 的生命周期仅仅在一次会话中,那么按照这种设计,每一次会话都要去创建 SqlSession,然后再通过 SqlSession 去创建 Mapper。我们知道 Mapper 其实没有必要每次都去创建,它更加适合作为一个单列对象。那么怎么将 SqlSession 和 Mapper 解耦呢?在 mybatis-spring 项目中通过 MapperFactoryBean、SqlSessionTemplate 来实现的。接下来我们就来解析它们。

MapperFactoryBean

   正如前面我们所看到的一样,MapperFactoryBean 其实可以理解为 Mapper 的代理工厂 Bean,我们可以通过 MapperFactoryBean 的方法获取到 Mapper 的代理对象。先来看下 MapperFactoryBean 继承关系:

   我们可以看到 MapperFactoryBean 实现了 FactoryBean,那么 肯定通过 实现 getObject() 获取到 Mapper 的代理对象,查看源码如下:


  public T getObject() throws Exception {return getSqlSession().getMapper(this.mapperInterface);
  }

   其内部就是我们熟悉的 getSqlSession().getMapper() 创建 Mapper 代理对象的方法。熟悉 Spring 的同学都知道 在 Bean 加载的过程中如果发现当前 Bean 对象是 FactoryBean 会去 调用 getObject() 获取真正的 Bean 对象。不熟悉的同学可以去看下 AbstractBeanFactory 的 getBean() 方法。

   但是似乎还是没有吧 SqlSession 和 Mapper 解耦的迹象呢?不着急,我们继续看下 getSqlSession(), 发现其是 父类 SqlSessionDaoSupport 实现,我们看下 SqlSessionDaoSupport 源码:


public abstract class SqlSessionDaoSupport extends DaoSupport {

  private SqlSession sqlSession;

  private boolean externalSqlSession;

  //  创建 SqlSession 子类 SqlSessionTemplate 
  public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {if (!this.externalSqlSession) {this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
    }
  }

  public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
    this.sqlSession = sqlSessionTemplate;
    this.externalSqlSession = true;
  }

  
  public SqlSession getSqlSession() {return this.sqlSession;}

  ....

}

   我们发现我们获取到的 SqlSession 其实是其子类SqlSessionTemplate, 我们查看其构造方法源码:


  public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {notNull(sqlSessionFactory, "Property'sqlSessionFactory'is required");
    notNull(executorType, "Property'executorType'is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    // 维护了一个 SqlSession 的代理对象
    this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class},
        new SqlSessionInterceptor());
  }
  

   我们可以清楚的发现,其内部维护了一个 SqlSession 的字段 sqlSessionProxy,其赋值的是代理对象 SqlSessionInterceptor。我们再来看下 SqlSessionInterceptor 的源码:


 private class SqlSessionInterceptor implements InvocationHandler {public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 通过 getSqlSession() 获取一个 SqlSession
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {unwrapped = translated;}
        }
        throw unwrapped;
      } finally {if (sqlSession != null) {closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }
  

  我们发现其代理实现时,通过 getSqlSession() 获取一个 全新的 SqlSession。也就是说创建 Mapper 的 SqlSession 和会话请求的 SqlSession 不是同一个。这里就完美的解耦了 Mapper 和 SqlSession,并且保障了每次会话 SqlSession 的生命周期范围。

  这里超前提下:getSqlSession().getMapper() 其实 是通过 configuration.getMapper() 来获取的,那么就意味着 configuration 内部必须添加了 Mapper 信息,那么 configuration 是何时添加的呢?可以看下 MapperFactoryBean 的 checkDaoConfig()方法,源码如下:


  @Override
  protected void checkDaoConfig() {super.checkDaoConfig();

    notNull(this.mapperInterface, "Property'mapperInterface'is required");

    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {configuration.addMapper(this.mapperInterface);
      } catch (Throwable t) {logger.error("Error while adding the mapper'" + this.mapperInterface + "'to configuration.", t);
        throw new IllegalArgumentException(t);
      } finally {ErrorContext.instance().reset();}
    }
  }
  

  由于父类实现了 InitializingBean 接口,并且其 afterPropertiesSet() 调用了 checkDaoConfig() 方法,所以,至少在初始化创建 MapperFactoryBean 时,就已经向 configuration 内部必须添加了 Mapper 信息。

三、个人总结

  本文解析了 Mybatis 与 Spring 是如何整合的,其中的关键对象包括:

  • SqlSessionFactoryBuilder: 用于创建 SqlSessionFactory
  • SqlSessionFactory:用于创建 SqlSession
  • SqlSession:Mybatis 工作的最顶层 API 会话接口,所有访问数据库的操作都是通过 SqlSession 来的
  • Configuration:存放有所有的 mybatis 配置信息,包括 mapper.xml、mybatis-config.xml 等
  • XMLConfigBuilder:解析 mybatis-config.xml 配置并存放到 Configuration 中
  • XMLMapperBuilder:解析 mapper.xml 配置并存放到 Configuration 中
  • SqlSessionFactoryBean:mybatis 整合 Spring 时的 生成 SqlSessionFactory 的 FactoryBean
  • MapperScannerConfigurer:mybatis 整合 Spring 时的 实现方便加载 Mapper 接口,以及将 Mapper 偷梁换柱成 MapperFactoryBean
  • MapperFactoryBean:生成 Mapper 代理对象的 FactoryBean
  • SqlSessionTemplate:内部维护有 SqlSession 的代理对象,解耦 Mapper 和 SqlSession 的关键对象。

         如果您对这些感兴趣,欢迎 star、follow、收藏、转发给予支持!

本文由博客一文多发平台 OpenWrite 发布!

正文完
 0