MyBatis 全解析系列(02)Spring 与 MyBatis 集成

02-Spring 与 MyBatis 集成

与 Spring 集成

官方提供了一个集成实现 mybatis-spring,将 MyBatis 集成到 Spring 框架中。

Configuration 的配置或者说 SqlSessionFactory 的初始化,交由 SqlSessionFactoryBean 来完成。

首先,看下 SqlSessionFactoryBean 的类签名:

java 复制代码
public class SqlSessionFactoryBean  
implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ContextRefreshedEvent> {}  

首先,它是一个 FactoryBean,说明当 SqlSessionFactoryBean 注入到容器中,并且当我们向容器请求 SqlSessionFactory,会调用 FactoryBean#getObject 方法,返回一个对象。

其次,它实现了 InitializingBean 接口,即 SqlSessionFactoryBean 初始化后,会回调 InitializingBean#afterPropertiesSet 方法。

最后,它实现了 ApplicationListener,即它会监听容器的生命周期事件,并做相应的动作。

SqlSessionFactoryBean 会根据当前具体情况,创建一个 Configuration 对象,并调用 SqlSessionFactoryBuilder#build 方法,创建一个 SqlSessionFactory 对象。

当调用 getObject 方法时,返回的就是创建好的 SqlSessionFactory 对象。

仅仅使用 SqlSessionFactoryBean 是没办法扫描到 Mapper 接口和定义了 Mapped SQL Statement 的 XML 文件的。

要想 MyBatis 正常工作,通常需要在 SqlSessionFactoryBean 中指定 DataSource 和 mapperLocations,例如:

java 复制代码
@Value("classpath*:mappers/**/*.xml") // XML 文件所在的路径,Ant 格式的路径  
Resource[] mapperLocations;  
  
@Bean  
public DataSource dataSource(){  
// 数据库链接  
}  
@Bean  
public SqlSessionFactoryBean sqlSessionFactoryBean() {  
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();  
    factoryBean.setDataSource(dataSource());  
    factoryBean.addMapperLocations(mapperLocations);  
    return factoryBean;  
}  

有了 SqlSessionFactoryBean,在应用中就可以通过 @Autowired 来获取 SqlSessionFactory 实例对象,然后通过 MyBatis API 来进行数据库操作。

mybatis-spring 做了更进一步的工作,提供了三个便利的接口:

  • SqlSessionTemplate
  • SqlSessionDaoSupport
  • MapperFactoryBean extends SqlSessionDaoSupport

SqlSessionTemplate 是对 SqlSession 的封装,提供了线程安全的访问方法。

而且,它还实现了 SqlSession 接口,所以应用代码中任何使用 SqlSession 的地方,都可以替换为 SqlSessionTemplate。

java 复制代码
@Bean  
public SqlSession sqlSession(@Autowired SqlSessionFactory sqlSessionFactory) {  
    return new SqlSessionTemplate(sqlSessionFactory);  
}  

按照上述方式将 SqlSessionTemplate 注入到容器中,所有需要 SqlSession 的地方,都可以通过 @Autowired Sqlsession session; 来获得这个线程安全的实现。

SqlSessionDaoSupport 是一个工具类,只需要继承这个类,就能在类中使用 getSqlSession().xxx 形式来进行数据库操作。

为了能够正常获得数据库会话,需要传递 SqlSessionFactory 到该类中,如下所示:

java 复制代码
@Bean  
public StudentDao studentDao(@Autowired SqlSessionFactory sqlSessionFactory) {  
    StudentDao dao = new StudentDao();  
    dao.setSqlSessionFactory(sqlSessionFactory);  
    return dao;  
}  
public static class StudentDao extends SqlSessionDaoSupport implements StudentMapper {  
  
    @Override  
    public Student selectByNo(String sno) {  
        return getSqlSession().selectOne("self.samson.example.mybatis.StudentMapper.selectByNo", sno);  
    }  
}  

MapperFactoryBean 是 SqlSessionDaoSupport 的一个子类实现,主要是优化使用方式。

java 复制代码
@Bean  
public MapperFactoryBean<StudentMapper> userMapper() throws Exception {  
    MapperFactoryBean<StudentMapper> factoryBean = new MapperFactoryBean<>(StudentMapper.class);  
    factoryBean.setSqlSessionFactory(sqlSessionFactory());  
    return factoryBean;  
}  

MapperFactoryBean 实现了 Spring 中的 FactoryBean 接口,它对 getObject 方法的实现如下:

java 复制代码
@Override  
public T getObject() throws Exception {  
    return getSqlSession().getMapper(this.mapperInterface); // this.mapperInterface 是创建时传入的类,例如上面的 StudentMapper.class  
}  

所以,这种方式允许你在引用代码中直接使用 @Autowired StudentMapper mapper; 这种方式直接获得特定类型的 Mapper 对象。

Mapper 发现

进一步优化使用方式,mybatis-spring 从 1.2.0 版本开始,提供了自动 Mapper 发现机制,即 @MapperScan

自动发现方式有两类,共三种:

  • XML 文件中,使用 <mybatis:scan/> 标签

  • Java 文件中,使用 @MapperScan 注解

  • Java 配置中,向容器中注入 MapperScannerConfigurer 对象

这里仅介绍 @MapperScan 方式的实现流程。
@MapperScan 本质上也是通过向容器中注入 MapperScannerConfigurer 对象来实现的。
<mybatis:scan/>@MapperScan 本质上也一样。

使用方式:

java 复制代码
@MapperScan // 默认使用当前类所在的包  
//或  
@MapperScan(basePackages = {"self.samson.example.spring"})  

@MapperScan 通过 @Import(MapperScannerRegistrar.class) 引入了 MapperScannerRegistrar。

MapperScannerRegistrar 是一个 ImportBeanDefinitionRegistrar,会向容器中注入特定的 BeanDefinition。

java 复制代码
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);  
// 省略配置过程  
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());  

向容器中注入 MapperScannerConfigurer 的目的是:

MapperScannerConfigurer 是一个 BeanDefinitionRegistryPostProcessor 实现。

在它的 postProcessBeanDefinitionRegistry 方法中,会通过 ClassPathMapperScanner 扫描 basePackages 中配置的包信息,并将扫描到的 Mapper 加入到容器中,如下所示。

java 复制代码
@Override  
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {  
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);  
    // 省略其他配置  
    scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));  
}  

这里需要补充一下 Spring 容器刷新的过程,能够更好地理解 MapperScannerConfigurer 的流程。

MapperScannerConfigurer 作为一个 BeanDefinitionRegistryPostProcessor 实现,在 1 时被调用。

此时,它使用 ClassPathMapperScanner 扫描所有的候选类,然后添加到容器中。

此时添加的并不是对象实例,仅仅是 BeanDefinition。

实例是在步骤 2 时创建的。

SqlSessionFactoryBean 中的 mapperLocations 方式与 @MapperScan 方式的不同

在使用过程中,发现下述两种方式的细微不同之处:

  • 方式一,使用 SqlSessionFactoryBean 中的 mapperLocations 方式指定 XML 文件位置,会自动解析对应的 XML 文件,并将 namespace 中的 Mapper 添加到 MyBatis 框架中。

    应用层在使用时,可以通过 getMapper(xx.class) 方式获得。

  • 方式二,使用 @MapperScan 方式指定 Mapper 所处的类,会自动解析 Mapper 接口,添加到 MyBatis 框架中。

    应用层在使用时,可以通过 getMapper(xx.class) 方式获得,或使用 Spring 提供的 @Autowired 使用。

通过 SqlSession#getMapper 获得的对象,都是 MapperProxy 对象。

它内部都包含了对 SqlSession、Mapper 接口类的引用。

方式一、二的第一个不同之处是,方式一返回的对象中的 SqlSession 是 MyBatis 中原生的 DefaultSqlSession 实现;

方式二返回的对象中,SqlSession 是 mybatis-spring 提供的 SqlSessionTemplate 实现。

另外一个不同之处,方式一不能通过容器来获得具体的 Mapper 实例。

造成这种差异的原因如下:

SqlSessionFactoryBean 通过 XMLMapperBuilder 来解析 XML 文件,并将 Mapper 注册到 MapperRegistry 中。

其实并没有将 Mapper 注册到 Spring 容器中。

@MapperScan 通过 ClassPathMapperScanner 将 Mapper 的定义添加到 Spring 容器中,且 scope 为 singleton。

Spring 容器在刷新时,会初始化 Mapper 单例,并添加到容器中。

ClassPathMapperScanner 向容器中添加 BeanDefinition 的过程如下:

java 复制代码
public Set<BeanDefinitionHolder> doScan(String... basePackages) {  
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages); // 找到所有的接口,并创建 BeanDefinition  

    if (beanDefinitions.isEmpty()) {  
        LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)  
    + "' package. Please check your configuration.");  
    } else {  
        processBeanDefinitions(beanDefinitions); // 修改 BeanDefinition  
    }  

    return beanDefinitions;  
}  

ClassPathMapperScanner 在 processBeanDefinitions 中,把 BeanDefinition 中的 beanClass 设置为 MapperFactoryBean.class。

java 复制代码
definition.setBeanClass(this.mapperFactoryBeanClass);  

并且会尝试设置 MapperFactoryBean 中的 sqlSessionFactory。

如果 MapperScannerConfigurer 中没有配置 sqlSessionFactory,这里就会跳过,并设置 BeanDefinition 中的自动注入方式为按类型注入:

java 复制代码
if (!explicitFactoryUsed) {  
    definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);  
}  

在 Spring 初始化单例 Bean 时,会把 ClassPathMapperScanner 处理的 BeanDefinition 进行实例化。

此时,根据 java.beans.IntrospectorwriteMethod 确定未满足的依赖,并尝试设置:

java 复制代码
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {  
if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {  
    this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory); // 这里其实就是简单的 new SqlSessionTemplate(sqlSessionFactory);  
}  
}  

然后,MapperFactoryBean 又实现了 InitializingBean 接口,所有会在实例化后调用它的 afterPropertiesSet:

java 复制代码
@Override  
protected void checkDaoConfig() {  
    super.checkDaoConfig(); // 检查 sqlSessionTemplate 非空  

    notNull(this.mapperInterface, "Property 'mapperInterface' is required"); // 检查 mapperInterface 非空  

    Configuration configuration = getSqlSession().getConfiguration();  
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) { // addToConifg 在 ClassPathMapperScanner 中设置为 true  
        try {  
            configuration.addMapper(this.mapperInterface); // 注册到 MyBatis 框架中  
        } catch (Exception e) {  
            logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);  
            throw new IllegalArgumentException(e);  
        } finally {  
            ErrorContext.instance().reset();  
        }  
    }  
}  

到这里位置,Spring 容器中的 Bean 与 MyBatis 中的 Mapper 实例就映射起来了。

在应用层中,既可以使用 SqlSession#getMapper 获取指定 Mapper 对象,也可以通过 @Autowired 直接注入指定的对象。

相关推荐
爬山算法20 分钟前
Maven(28)如何使用Maven进行依赖解析?
java·maven
hlsd#27 分钟前
go mod 依赖管理
开发语言·后端·golang
陈大爷(有低保)32 分钟前
三层架构和MVC以及它们的融合
后端·mvc
亦世凡华、32 分钟前
【启程Golang之旅】从零开始构建可扩展的微服务架构
开发语言·经验分享·后端·golang
河西石头33 分钟前
一步一步从asp.net core mvc中访问asp.net core WebApi
后端·asp.net·mvc·.net core访问api·httpclient的使用
2401_857439691 小时前
SpringBoot框架在资产管理中的应用
java·spring boot·后端
怀旧6661 小时前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
李老头探索1 小时前
Java面试之Java中实现多线程有几种方法
java·开发语言·面试
芒果披萨1 小时前
Filter和Listener
java·filter
qq_4924484461 小时前
Java实现App自动化(Appium Demo)
java