MyBatis集成到Spring

使用 MyBatis 的 SqlSession

MyBatis 的 提供了执行 SQL 语句、提交或回滚事务和获取映射器实例的方法。SqlSession 由工厂类 SqlSessionFactory 来创建,SqlSessionFactory 又是构造器类 SqlSessionFactoryBuilder 创建的。

InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();

使用 mybatis-spring 的 SqlSession

使用 mybatis-spring 集成 Spring 时 ,SqlSessionFactory 使用了 Spring 的 FactoryBean 的实现类 SqlSessionFactoryBean 间接地调用 SqlSessionFactoryBuilder 来创建。 SqlSession 由 它的线程安全的实现类 SqlSessionTemplate 替代,它能基于 Spring 的事务机制自动提交、回滚、关闭 session。要在 Spring 容器中使用 SqlSessionTemplate,就要将其注入到容器中。

// 注入 SqlSessionTemplate
@Bean
public SqlSessionTemplate sqlSession() throws Exception {
    return new SqlSessionTemplate(sqlSessionFactory());
}

public SqlSessionFactory sqlSessionFactory() throws Exception {
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    // 指定数据源连接信息
    factoryBean.setDataSource(dataSource());
    // 指定 mapper 文件路径
    InputStream inputStream = Resources.getResourceAsStream("mapper/UserSpringMapper.xml");
    factoryBean.setMapperLocations(new InputStreamResource(inputStream));
    return factoryBean.getObject();
}

// 使用 Spring 事务机制
@Bean
PlatformTransactionManager getTransactionManager() {
    return new DataSourceTransactionManager(dataSource());
}

使用 mybatis-spring-boot-starter 自动注入

如果使用 Springboot,可以通过引入mybatis-spring-boot-starter,将 MyBatis 的组件自动注入到 Spring 容器中,这个 starter 会引入mybatis-spring-boot-autoconfigure(查看如何开发自己的 Springboot starter),这个包里面有一个重要的配置类MybatisAutoConfiguration,通过查看其源码可知,它还有两个静态内部类MapperScannerRegistrarNotFoundConfigurationAutoConfiguredMapperScannerRegistrar,其中,MybatisAutoConfigurationMapperScannerRegistrarNotFoundConfiguration都加了 Spring 的 @Configuration 注解,所以 Spring 启动时会将它们都加载到容器中,而AutoConfiguredMapperScannerRegistrar是通过MapperScannerRegistrarNotFoundConfiguration的注解 @Import 间接地注入容器的。

AutoConfiguredMapperScannerRegistrar实现了 ImportBeanDefinitionRegistrar,所以其方法 registerBeanDefinitions() 会在容器启动时执行,主要有如下两个作用:

  1. 从 BeanFactory 获取包扫描的路径
  2. 初始化和配置 MapperScannerConfigurer (指定注解类型为 @Mapper、指定包路径等),注册到 BeanFactory

MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor,所以其方法 postProcessBeanDefinitionRegistry() 会在容器启动时执行,通过这个方法初始化 ClassPathBeanDefinitionScanner 的子类 ClassPathMapperScanner,调用 scan(String... basePackages),扫描包路径下 @Mapper 注解的所有接口,注册到 BeanFactory,接着进行后置处理:

  1. 将 BeanDefinition 的类型修改为 MapperFactoryBean
  2. 指定 MapperFactoryBean 的构造器参数为 @Mapper 接口类的全类名
  3. 设置 sqlSessionFactory、sqlSessionTemplate、按照类型自动装配等
  4. 利用反射创建 MapperFactoryBean 实例,调用其有参构造器,将 @Mapper 接口传入,缓存到 Class<T> mapperInterface

如下图: MapperFactoryBean 的继承关系

MapperFactoryBean.png

初始化和配置解析

DaoSupport 实现了 InitializingBean.afterPropertiesSet(),通过这个方法,将 Mapper 缓存到 MapperRegistryMap<Class<?>, MapperProxyFactory<?>> knownMappers,key 为 Mapper 接口,value 为 Mapper 代理工厂类 MapperProxyFactory;最后,使用 MapperAnnotationBuilder.parse() 来解析 XML 配置文件或者方法注解,缓存到 ConfigurationMap<String, MappedStatement> mappedStatements,源码流程如下:

DaoSupport.afterPropertiesSet()
->MapperFactoryBean.checkDaoConfig()
->Configuration.addMapper(this.mapperInterface)
->MapperRegistry.addMapper(type)
->knownMappers.put(type, new MapperProxyFactory<>(type))
// 解析 SQL 配置
->MapperAnnotationBuilder.parse()
-->configuration.addMappedStatement(statement)

生成代理对象

MapperFactoryBean 实现了 FactoryBean.getObject(),从 knownMappers 缓存取出 Mapper 接口映射的 MapperProxyFactory,使用这个工厂类来创建 MapperProxy 代理类,从 MapperProxy<T> implements InvocationHandler 可知是使用了 JDK 的动态代理,源码流程如下:

MapperFactoryBean.getObject()
->SqlSessionTemplate.getMapper(mapperInterface)
->Configuration.getMapper(mapperInterface, this)
->MapperRegistry.getMapper(mapperInterface, sqlSession)
->MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
->mapperProxyFactory.newInstance(sqlSession)

public T newInstance(SqlSession sqlSession) {
  final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

到这里,代理对象就生成了,在 Springboot 应用中就可以简单的通过 @Autowired 的注解方便的从容器中获取 Mapper 接口的代理对象(MapperProxy)了。

执行流程

假设存在 @Mapper 注解的类 UserDao。

@Mapper
public interface UserDao {
  @Select("select * from t_user where id = #{id}")
  Optional<UserEntity> findOne(String id);
}

通过 @Autowired 获取 Bean。由上面可知,实际获取到的是代理对象 MapperProxy。

@Autowired
UserDao userDao;

调用 UserDao 的方法实际上执行的是代理对象 MapperProxy 的 invoke() 方法。

// 调用 findOne
userDao.findOne(id);

// 实际执行的方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  return mapperMethod.execute(sqlSession, args);
}

invoke() 方法大致的源码执行流程如下:

MapperMethod.execute(sqlSession, args)
sqlSessionProxy.selectOne(statement, parameter)

需要注意 SqlSession在 SqlSessionTemplate 的有参构造器中初始化,并且它也是个代理类,被 SqlSessionInterceptor 代理

this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
    new Class[] { SqlSession.class }, new SqlSessionInterceptor());

所以 selectOne 方法会被 SqlSessionInterceptor.invoke() 拦截,反射执行 SqlSession.selectOne() 方法,源码流程如下:

private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 打开获取 DefaultSqlSession;
    SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
        SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
    try {
      // 反射执行 SqlSession 的方法 selectOne(String statement, Object parameter) 进行查询
      Object result = method.invoke(sqlSession, args);
      if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
        // 提交
        sqlSession.commit(true);
      }
      // 返回查询结果
      return result;
    } catch (Throwable t) {
      // 异常时释放连接
      closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
    } finally {
      if (sqlSession != null) {
        // 释放连接
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      }
    }
  }
}

注解和配置文件

Springboot 应用同样可以选择使用注解,或者配置文件的方式使用 MyBatis,一般简单的增删改查直接使用注解的方式(比如 @Select、@SelectProvider)即可,可以减少很多配置文件;比较复杂的 SQL 可能还是使用配置文件的方式操作起来更加方便一些,具体还是得看实际情况来选择,需要注意的是,每个 DAO 可以同时存在注解和配置的方式,但是同一个方法不能同时存在注解和配置的方式。

如果是通过配置文件的方式,可以在 application.yml 配置文件指定 DAO 的配置文件所在位置:

# 使用基于配置文件的 MyBatis 时指定 Mapper 配置的路径
mybatis:
  mapper-locations: mapper/*Dao.xml
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,176评论 5 469
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,190评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,232评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,953评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,879评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,177评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,626评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,295评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,436评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,365评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,414评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,096评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,685评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,771评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,987评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,438评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,032评论 2 341

推荐阅读更多精彩内容