11.Spring整合Mybatis的思路


title: 11.搭建Sql执行器
tag: 笔记 手写SSM ORM

实现了ORM的主要功能后,我们就可以与之前实现的IOC容器实现整合了。

Spring整合Mybatis的思路

最近在仿照一个Spring框架时想要再写一个Mybatis框架整合到里面去,因此对Mybatis-Spring的源码进行了一些粗略的了解。

Spring整合Mybatis大概需要解决一下几个问题:

  • Spring只能扫描到Mapper的接口,如何将这个接口替换为Mapper代理类?

  • 如何将Mybatis动态代理生成的Mapper注册到IOC容器中使用?

  • 获取Mapper需要构建SqlSession,如何构建SqlSession,并添加到IOC容器中?

分析

我们先看SSM框架中用户如何使用注解来整合SpringMybatis,用户需要提供一个Mybatis的配置类:

public class MyBatisConfig {
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean=new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setTypeAliasesPackage("com.duan.domian");
        return sqlSessionFactoryBean;
    }
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer mapperScannerConfigurer=new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("com.duan.dao");
        return mapperScannerConfigurer;
    }
}

这个配置类将SqlSessionFactoryBeanMapperScannerConfigurer作为Bean加入到了IOC容器中。

从名字可以看出:

  • SqlSessionFactoryBean:Spring用于构建SqlSession的工厂方法。
  • MapperScannerConfigurer:Spring配置Mapper扫描并进行扫描的类。

因此这里我们可以知道**SpringFacotryBean的方式将MapperSqlSession注册到IOC容器中。**

但这里还有一个问题,就是如何将Spring扫描到的接口替换为动态代理生成的Mapper呢

我们再进入到MapperScannerConfigurer中可以看到该类实现了一个接口,BeanDefinitionRegistryPostProcessor,那么我们知道实现了XXXXPostProcessor的Bean都是Spring提供的一些扩展点,会在Spring加载的一定时期进行调用,那么BeanDefinitionRegistryPostProcessor是在何时调用的呢?

BeanDefinitionRegistryPostProcessor 是 Spring 框架中的一个接口,它允许开发者在 Spring 容器完成 BeanDefinition 的注册但尚未创建任何 Bean 实例之前进行干预操作。

所以我们可以知道Spring是在BeanDefinition 的注册完成时去扫描Mapper所在的接口,并通过某种方式将这些接口替换成了动态代理生成的Mapper。

下面我们将去源码中简略分析一下这个流程。

Spring-Mybatis源码分析

个人认为源码中最重要的类即下面几个

  • SqlSessionFactoryBean:构建SqlSession的工厂

  • MapperScannerConfigurer和ClassPathMapperScanner:Mapper接口的扫描

  • MapperFactoryBean:构建Mapper的工厂

SqlSessionFactoryBean

首先我们可以看到这个类实现这两个接口:

  • FactoryBean:说明该类是一个工厂类,拿到的Bean是getObject方法的返回值。
  • InitializingBean:Spring会在Bean初始化完成后调用afterPropertiesSet方法

因此该类我们主要看两个getObjectafterPropertiesSet即可大致了解该类:

getObject:

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

    return this.sqlSessionFactory;
}

这个工厂方法会返回一个SqlSessionFactorySqlSessionFactory用于创建SqlSession

public void afterPropertiesSet() throws Exception {
    Assert.notNull(this.dataSource, "Property 'dataSource' is required");
    Assert.notNull(this.sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
    Assert.state(this.configuration == null && this.configLocation == null || this.configuration == null || this.configLocation == null, "Property 'configuration' and 'configLocation' can not specified with together");
    this.sqlSessionFactory = this.buildSqlSessionFactory();
}
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
        XMLConfigBuilder xmlConfigBuilder = null;
        Configuration configuration;
        if (this.configuration != null) {
            configuration = this.configuration;
            if (configuration.getVariables() == null) {
                configuration.setVariables(this.configurationProperties);
            } else if (this.configurationProperties != null) {
                configuration.getVariables().putAll(this.configurationProperties);
            }
        } else if (this.configLocation != null) {
            xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), (String)null, this.configurationProperties);
            configuration = xmlConfigBuilder.getConfiguration();
        } else {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Property `configuration` or 'configLocation' not specified, using default MyBatis Configuration");
                。。。。。。省略
            }

afterPropertiesSet会调用buildSqlSessionFactory方法来进行SqlSessionFactory的构建。而SqlSessionFactory的构建需要Configuration类,Configuration基本包含了SqlSession的全部信息。因此构建SqlSessionFactory实际上就是构建Configuration

因此buildSqlSessionFactory方法的代码中基本都是构建Configuration的逻辑,那么构建Configuration的信息从哪来呢?也就是该类是如何构建Configuration的呢?

在我粗略的看了一下代码,我个人觉得它是按照如下方式构造的:

  • 如果已经存在Configuration,则使用这个Configuration
  • 若设置了configLocation属性,则会从这个XML构建Configuration。(mybatis本就支持XML构建)
  • 接下来会根据用户自定义的属性就行设置。

这些自定义的属性都在该类的属性中,我们可以在将该类加入到IOC容器时进行设置。

private static final Log LOGGER = LogFactory.getLog(SqlSessionFactoryBean.class);
private Resource configLocation;
private Configuration configuration;
private Resource[] mapperLocations;
private DataSource dataSource;
private TransactionFactory transactionFactory;
private Properties configurationProperties;
private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
private SqlSessionFactory sqlSessionFactory;
........

总结:该类用于构建SqlSession,拿到Mybatis需要的各种配置

MapperScannerConfigurer和ClassPathMapperScanner

我们先看MapperScannerConfigurer类,该类实现了接口:

  • BeanDefinitionRegistryPostProcessor :之前介绍过该接口,是在BeanDefinition注册完成后调用postProcessBeanDefinitionRegistry方法。
  • InitializingBean:Spring会在Bean初始化完成后调用该接口的afterPropertiesSet方法

所以我们同样从实现接口的两个方法postProcessBeanDefinitionRegistry方法和afterPropertiesSet方法来了解该类的作用:

  1. afterPropertiesSet方法:
public void afterPropertiesSet() throws Exception {
    Assert.notNull(this.basePackage, "Property 'basePackage' is required");
}

在该Bean初始化完成后,该类只是检测一下是否传入了扫描包basePackage,否则报错。

  1. postProcessBeanDefinitionRegistry方法:
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
        this.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, ",; \t\n"));
}

这个方法配置了ClassPathMapperScanner类,并使用该类扫描了basePackage

因此,接下来我们继续看ClassPathMapperScanner类。

我们首先看到该类继承了Spring的提供的扫描器ClassPathBeanDefinitionScanner,它主要用于扫描类路径(ClassPath)下的指定包及其子包中符合条件的类,并将这些类注册为 Spring 容器中的 Bean。

既然ClassPathBeanDefinitionScanner是用于扫描BeanDefinition的扫描器,那么ClassPathMapperScanner就是用于扫描Mapper的扫描器了。

ClassPathMapperScanner相比与父类扩展了许多许多MapperScannerConfigurer提供的配置信息,其中最重要的应当是该类重写了父类的doScan方法:

public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
    if (beanDefinitions.isEmpty()) {
        LOGGER.warn(() -> {
            return "No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.";
        });
    } else {
        this.processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
}

它首先调用了父类的doScan方法拿到扫描到的beanDefinitions之后又调用了processBeanDefinitions方法处理beanDefinitions。

因此我们可以再继续看processBeanDefinitions方法,但该方法较长,我们主要看关键的地方:

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    .......
 definition.setBeanClass(this.mapperFactoryBeanClass);
            definition.getPropertyValues().add("addToConfig", this.addToConfig);
            definition.setAttribute("factoryBeanObjectType", beanClassName);
    .......

}

我们可以看到这里将definition的BeanClass属性修改为了mapperFactoryBeanClass

private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class;

,该属性的值默认为MapperFactoryBean.class。我们知道这个类是一个工厂类,并且又给这个工厂添加了一个参数为添加了一个参数为beanClassName,这个参数的值就是原本的BeanDefinitionbeanClassName,也就是接口的beanClassName而在这个MapperFactoryBean中就可以为这个接口生成代理对象。

总结:Spring-Mybatis通过配置Mapper扫描器来扫描到Mapper接口,并在扫描完成后将这些接口的BeanClass替换为MapperFactoryBean来生成Mapper代理对象

MapperFactoryBean

最后我们来了解生成Mapper代理的工厂类MapperFactoryBean

  • 该类继承了DaoSupport抽象类:
public abstract class DaoSupport implements InitializingBean {
    protected final Log logger = LogFactory.getLog(this.getClass());

    public DaoSupport() {
    }

    public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
        this.checkDaoConfig();

        try {
            this.initDao();
        } catch (Exception var2) {
            throw new BeanInitializationException("Initialization of DAO failed", var2);
        }
    }

    protected abstract void checkDaoConfig() throws IllegalArgumentException;

    protected void initDao() throws Exception {
    }
}

我们可以看到该类使用了模板方法的设计模式,它在afterPropertiesSet中调用了checkDaoConfiginitDao。之前说过afterPropertiesSet方法会在Bean初始化后调用。因此我们只需要看MapperFactoryBean是如何重写checkDaoConfiginitDao就可以知道这个类做了什么。

通过源码可以看到MapperFactoryBean仅仅重写了checkDaoConfig因此我们只需要看这个方法即可:

protected void checkDaoConfig() {
    super.checkDaoConfig();
    Assert.notNull(this.mapperInterface, "Property 'mapperInterface' is required");
    Configuration configuration = this.getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
        try {
            configuration.addMapper(this.mapperInterface);
        } catch (Exception var6) {
            this.logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", var6);
            throw new IllegalArgumentException(var6);
        } finally {
            ErrorContext.instance().reset();
        }
    }

}

该类首先检测一些需要的属性(SqlSession,mapperInteface)是否为空,检测完成后拿到SqlSession中的Configration调用addMapper将该接口注册到Configration中。

总结

支持我们介绍完了Spring-Mybatis的整合原理大致为以下几步:

  1. SqlSessionFactoryBean拿到配置信息后构建SqlSession到IOC容器。
  2. ClassPathMapperScanner拿到扫描的配置信息后扫描Mapper接口,并将这个Mapper接口替换为MapperFactoryBean生成代理生成的Mapper。
  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值