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

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

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

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

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

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

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

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

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

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

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

扫描二维码关注公众号,回复: 4392477 查看本文章

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

这里我随意启动了一个Service层,debug了一个mapper的属性,可以看到,它还是一个熟悉的MapperProxy代理的,但不同点在于MapperProxy中的sqlSession,这里再也不是DefaultsqlSession了,而是一个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;
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }

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

  private final SqlSession sqlSessionProxy;

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

  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的。

  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方法。

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方法里。

  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变量。

    private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");

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

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

@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,控制台打印。

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

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

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

猜你喜欢

转载自blog.csdn.net/qq_41737716/article/details/83685309