Mybatis与Spring集成时做了哪些事情

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> Mybatis与Spring集成时做了哪些事情

** 这篇博客主要是来分析MyBatis与Spring集成后Spring帮我们做了哪些事情,以及集成后使用MyBatis有什么变化。**

首先来看看集成包下有什么东西吧。

Mybatis与Spring集成时做了哪些事情

第一个模块annotation:这里做了一个注解(MapperScan),用于扫描mapper。以及mapper扫描注册器(MapperScannerRegistrar),此扫描注册器实现了ImportBeanDefinitionRegistrar接口,在Spring容器启动时会运行所有实现了这个接口的实现类,而这个注册器内部会注册一系列MyBatis的信息。

第二个模块batch:这里没有深入研究这个包,估计是为了批量操作使用的?

第三个模块config:主要是为了解析处理读取到的配置信息。

第四个模块mapper:这里就是主要处理mapper的地方了,与编程式的MyBatis不同,与Spring集成后,每个mapper本来的生命周期为method级别,在集成后变成了application级别,主要原因是Spring将扫描到的每一个mapper都存入IOC容器,并且是单例的。

我们进入ClassPathMapperScanner的doScan方法DEBUG来看一下。

Mybatis与Spring集成时做了哪些事情

这里可以看到,所有的mapper都会被放入IOC容器,并且scope=singleton,生命周期提升至容器级别,所以我们在用的时候只需要使用注解@Autowired即可使用Mapper。

第五个模块support:只有一个模板辅助类,是MapperFactoryBean的父类,用于方便创建一个SqlSession(集成后的SqlSession变成了SqlSessionTemplate,下面会介绍)

第六个模块transaction:在集成后,事务的管理交给了Spring来做。

最后一个就是SqlSession的变化了,先来DEBUG看看集成后的mapper是个什么东西吧。

Mybatis与Spring集成时做了哪些事情

这里我随意启动了一个Service层,debug了一个mapper的属性,可以看到,它还是一个熟悉的MapperProxy代理的,但不同点在于MapperProxy中的sqlSession,这里再也不是DefaultsqlSession了,而是一个SqlSessionTemplate,那这个到底是什么呢?

1234567891011121314
  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;    this.sqlSessionProxy = (SqlSession) newProxyInstance(        SqlSessionFactory.class.getClassLoader(),        new Class[] { SqlSession.class },        new SqlSessionInterceptor());  }

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;
this.sqlSessionProxy = (SqlSession) newProxyInstance(
    SqlSessionFactory.class.getClassLoader(),
    new Class[] { SqlSession.class },
    new SqlSessionInterceptor());

}

这个是SqlSessionTemplate主要的构造方法,可以看到,SqlSessionTemplate里还有一个SqlSession。

1
  private final SqlSession sqlSessionProxy;

private final SqlSession sqlSessionProxy;

而这个SqlSession又用动态代理代理了一下,主要代理类为SqlSessionInterceptor,其为SqlSessionTemplate的内部类。

1234567891011121314151617
  private class SqlSessionInterceptor implements InvocationHandler {    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {      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;      }   }

private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
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;
}
}

这里删除了一些东西,保留了关键代码,可以看出来每次运行SqlSessionTemplate的方法都会新创建一个SqlSession,看看getSqlSession这个方法是如何创建SqlSession的。

12345678910111213141516171819202122
  public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {     notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);     SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);     SqlSession session = sessionHolder(executorType, holder);    if (session != null) {      return session;    }     if (LOGGER.isDebugEnabled()) {      LOGGER.debug("Creating a new SqlSession");    }     session = sessionFactory.openSession(executorType);     registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);     return session;  }

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {


notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
  return session;
}

if (LOGGER.isDebugEnabled()) {
  LOGGER.debug("Creating a new SqlSession");
}

session = sessionFactory.openSession(executorType);

registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

return session;

}

这个方法在SqlSessionUtils中,用了静态导包方法直接写方法名调用。可以看到,这里会先从TransactionSynchronizationManager中获取SqlSession(下面会提到一级缓存),如果取不到就用sessionFactory新开启一个SqlSession,而开启的这个SqlSession还是我们之前熟悉的DefaultSqlSession,回到上面内部类的invoke方法。

123456
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);        }

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);
}

由method.invoke(sqlSession, args)可以看出,其实真正执行查询方法的还是DefaultSqlSession,下面又判断了一下事务,然后自动commit。

我们来总结一下,这个SqlSessionTemplate到底在集成后有了什么作用?为什么要一个这样的SqlSession?

首先理一理大致的流程,Spring容器在启动后会扫描所有的Mapper信息,然后还是用MapperProxy给每一个Mapper接口做代理,不同的是这次的MapperProxy中的SqlSession是SqlSessionTemplate代理的,也就是说,每次执行mapper接口的方法,都会先执行MapperProxy的invoke方法,然后去执行SqlSessionTemplate的invoke方法,然后SqlSessionTemplate内部就会新new一个DefaultSqlSession,实际上底层还是使用的DefaultSqlSession的方法。

那为什么要这样一个SqlSession呢?

可以从invoke方法看出,在集成后,设计者将为每一次查询都开启一个新的SqlSession,在集成后的scope为method级别,也就是每调用一次mapper接口的方法,都会创建一个SqlSession,然后会将此SqlSession判断事务,因为是Spring管理事务,所以这里就可以用SqlSessionTemplate这样一个类去很方便的可以管理事务,也方便了Spring管理事务。

介绍到这里,可以抛出一个问题了,我们知道,一级缓存是SqlSession级别的,同一个SqlSession在查询同一条语句时会使用缓存,但集成后将SqlSession设计成每调用一次方法开启一个新的SqlSession。

那么一级缓存是不是失效的呢?

其实也不一定,在一般情况下一级缓存是没有用了,但如果开启了事务,就不一定是新创建SqlSession了,其实上面也有提到,在SqlSesionUtils类中创建SqlSession方法里。

12345678910111213141516171819202122
  public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {     notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);     SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);     SqlSession session = sessionHolder(executorType, holder);    if (session != null) {      return session;    }     if (LOGGER.isDebugEnabled()) {      LOGGER.debug("Creating a new SqlSession");    }     session = sessionFactory.openSession(executorType);     registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);     return session;  }

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {


notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
  return session;
}

if (LOGGER.isDebugEnabled()) {
  LOGGER.debug("Creating a new SqlSession");
}

session = sessionFactory.openSession(executorType);

registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

return session;

}

会尝试先从TransactionSynchronizationManager取SqlSession,而这个类存放了一个ThreadLocal变量。

1
    private static final ThreadLocalSetTransactionSynchronization synchronizations = new NamedThreadLocal("Transaction synchronizations");

private static final ThreadLocalSetTransactionSynchronization synchronizations = new NamedThreadLocal("Transaction synchronizations");

在上面的getSqlSession方法中,倒二行有一个registerSessionHolder,里面会判断是否有开启事务,如有开启事务则将当前的SqlSession保存至一个holder到TransactionSynchronizationManager的ThreadLocal中,也就是保存到线程级别的变量中,然后在同一个Transaction且同一个线程中如果有再次调用mapper的方法,第二次进入getSqlSession方法后会从TransactionSynchronizationManager中取出刚刚存放的线程变量ThreadLocal的holder,再从holder取出SqlSession,这样,两次查询就还是用的同一个SqlSession,一级缓存还是会生效的(一级缓存默认开启)。但在没有事务的情况下,每一个mapper方法的调用都会创建一个新的mapper。

这里写一个测试类来验证以上的想法。

123456789101112131415161718192021222324252627282930313233343536373839404142
@RestController@RequestMapping("")public class LoginControlller {     @Autowired    UserMapper mapper;     @RequestMapping(value = "/test")    @Transactional    public String test(){        long one = System.currentTimeMillis();        long id = 3;        mapper.selectByPrimaryKey(id);        long two = System.currentTimeMillis();        mapper.selectByPrimaryKey(id);        long three = System.currentTimeMillis();        mapper.selectByPrimaryKey(id);        long four = System.currentTimeMillis();         System.out.println("第1次查询耗时:" + (two - one));        System.out.println("第2次查询耗时:" + (three - two));        System.out.println("第3次查询耗时:" + (four - three));        return "test";    }     @RequestMapping(value = "/test2")    public String test2(){        long one = System.currentTimeMillis();        long a = 3;        mapper.selectByPrimaryKey(a);        long two = System.currentTimeMillis();        mapper.selectByPrimaryKey(a);        long three = System.currentTimeMillis();        mapper.selectByPrimaryKey(a);        long four = System.currentTimeMillis();         System.out.println("第1次查询耗时:" + (two - one));        System.out.println("第2次查询耗时:" + (three - two));        System.out.println("第3次查询耗时:" + (four - three));        return "test2";    }}

@RestController
@RequestMapping(“”)
public class LoginControlller {


@Autowired
UserMapper mapper;

@RequestMapping(value = "/test")
@Transactional
public String test(){
    long one = System.currentTimeMillis();
    long id = 3;
    mapper.selectByPrimaryKey(id);
    long two = System.currentTimeMillis();
    mapper.selectByPrimaryKey(id);
    long three = System.currentTimeMillis();
    mapper.selectByPrimaryKey(id);
    long four = System.currentTimeMillis();

    System.out.println("第1次查询耗时:" + (two - one));
    System.out.println("第2次查询耗时:" + (three - two));
    System.out.println("第3次查询耗时:" + (four - three));
    return "test";
}

@RequestMapping(value = "/test2")
public String test2(){
    long one = System.currentTimeMillis();
    long a = 3;
    mapper.selectByPrimaryKey(a);
    long two = System.currentTimeMillis();
    mapper.selectByPrimaryKey(a);
    long three = System.currentTimeMillis();
    mapper.selectByPrimaryKey(a);
    long four = System.currentTimeMillis();

    System.out.println("第1次查询耗时:" + (two - one));
    System.out.println("第2次查询耗时:" + (three - two));
    System.out.println("第3次查询耗时:" + (four - three));
    return "test2";
}

}

/test映射test方法,此方法开启了事务,/test2映射test2方法,此方法没有开启事务。

首先访问/test,控制台打印。

Mybatis与Spring集成时做了哪些事情

从日志可以看出来,这里只创建了一次SqlSession,并且将此SqlSession注册进了transaction synchronization中,也就是上面提到的那个地方,也可以从下面看出,只执行了一次SQL语句的查询,以至于查询耗时变快,感觉存在缓存也许是因为此时三个查询都是同一个SqlSession,一级缓存生效了。所以可以得出结论,被事务管理的方法中,调用mapper的方法,不会new多次SqlSession,同一个线程同一个mapper都只会使用同一个SqlSession,所以此时的一级缓存是生效的。

第二个测试,访问/test2 (没有注解上@Transactional)

Mybatis与Spring集成时做了哪些事情

第三次查询耗时:0,控制台有点短截取不到下面的内容了。这里关键信息我框了出来,可以看出第一次查询使用了SQL语句进行查询,然后关闭一个没有事务的SqlSession,归还JDBC连接给数据库,在第二次查询之前,又创建了一个新的SqlSession,然后又进行相同的SQL语句的查询,接着又关闭了没有事务的SqlSession,并且归还了JDBC连接给数据库。以至于为什么查询耗时还是越来越短,感觉到缓存的存在,这是因为数据库上也是有缓存的。所以到这里可以得出结论,没有被事务管理的方法例如/test2,在每次mapper查询的时候,都会新创建SqlSession,以至于一级缓存失效,但底层数据库缓存还是有在的。我在写这篇文章时曾一度以为MyBatis与Spring集成后一级缓存是失效的,在查阅资料以及动手验证才知道底层还有这么个原理,在测试的时候没有开启事务也能让查询耗时变短,曾一度以为是MapperProxy中的methodCache属性在作怪,感兴趣的读者可以去这个类看一看,这里的methodCache只是缓存了Mapper的method信息,例如command的type(INSERT、UPDATE等等),查询时还是照样要去数据库查询,误解了以为是这里使查询耗时变快,后来仔细看了一下这里并不缓存查询结果…

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> Mybatis与Spring集成时做了哪些事情


 上一篇
SpringBoot 连载(三) —— 修改默认配置以及使用yml SpringBoot 连载(三) —— 修改默认配置以及使用yml
上两期我们讲了SpringBoot环境搭建,HelloWorld问世,打包运行,以及表层解释了非常重要的SpringBoot场景启动器.没有看过的同学可以通过下面的链接去看一下上两期的内容. 聊一下如何修改SpringBoot的一些默
2021-04-05
下一篇 
深入Spring事务源码剖析事务(一) 深入Spring事务源码剖析事务(一)
深入Spring事务源码剖析事务之事务介绍与事务开启  文章目录 - - - - -   事务简介此篇文章需要有SpringAOP基础,知道AOP底层原理可以更好的理解Spring的事务处理。首先事务是什么呢?必须了
2021-04-05