SpringBoot源码解读与原理分析(三十五)SpringBoot整合MyBatis时的核心组件自动装配

前言

在实际SpringBoot项目开发中,更多的场景是整合成熟的持久层框架来完成与数据库的交互。

目前比较流行的持久层框架包括SpringData JPA(底层默认依赖Hibernate)和MyBatis。

SpringData本身属于Spring生态的一部分,而MyBatis是第三方框架。

本章以MyBatis整合SpringBoot的核心场景启动器为切入点,研究MyBatis如何完成与SpringBoot的场景整合。

第11章 SpringBoot整合MyBatis

11.1 MyBatis框架概述

MyBatis是一款优秀的持久层框架,它支持自定义SQL、存储过程以及高级映射。

MyBatis免除了几乎所有的JDBC代码,支持通过简单的XML或注解来配置和映射原始类型、接口、POJO为数据库中的记录。

MyBatis的架构可以分为三层:

  • 接口层:SqlSession是与MyBatis完成交互的核心接口。
  • 核心层:SqlSession执行的方法,包括配置文件的解析、SQL解析、执行SQL时的参数映射、SQL执行、结果集映射等。
  • 支持层:核心层的功能实现是基于支持层的各个模块共同协调完成的。

11.2 SpringBoot整合MyBatis项目搭建

搭建如下图所示的项目:
SpringBoot整合MyBatis项目搭建
实体类:

public class User {

    private Integer id;
    private String name;
    private String tel;
    
    // getter setter toString ...
}

Service类:


@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
        User user = new User();
        user.setName("天蓬元帅");
        user.setTel("12345");
        userMapper.save(user);
        List<User> userList = userMapper.findAll();
        userList.forEach(System.out::println);
    }

}

Mapper接口:

@Mapper
public interface UserMapper {

    public void save(User user);

    public List<User> findAll();
}

XML映射文件 UserMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.star.springboot.mybatis.mapper.UserMapper">

    <insert id="save" parameterType="User">
        insert into t_user (name, tel) values (#{name}, #{tel})
    </insert>

    <select id="findAll" resultType="User">
        select * from t_user
    </select>

</mapper>

全局配置文件 application.properties:

# 数据源配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springboot_demo?characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456

# MyBatis配置
# 配置XML文件的路径
mybatis.mapper-locations=classpath*:**Mapper.xml
# 开启实体类别名映射
mybatis.type-aliases-package=com.star.springboot.mybatis.entity
# 开启驼峰规则
mybatis.configuration.map-underscore-to-camel-case=true

主启动类:

@SpringBootApplication
public class MyBatisApp {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(MyBatisApp.class, args);
        UserService userService = context.getBean(UserService.class);
        userService.test();
    }

}

执行主启动类的main方法,控制台打印结果如下:

User{id=1, name='天蓬元帅', tel='12345'}

说明SpringBoot整合MyBatis项目搭建成功。

11.3 自动装配的核心

在上面的项目搭建中,只需要写简单的几行代码和少量配置,底层的自动配置类完成了大多数组件的装配和配置的应用

11.3.1 mybatis-spring-boot-starter

打开依赖包mybatis-spring-boot-starter,发现它只是一个空的jar包,没有具体的内容:
mybatis-spring-boot-starter
进入该依赖包的pom.xml文件,发现它本身又依赖mybatis-spring-boot-autoconfigure。

源码1:mybatis-spring-boot-starter-2.1.3.jar!/META-INF/maven/org.mybatis.spring.boot/mybatis-spring-boot-starter/pom.xml

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>

打开依赖包mybatis-spring-boot-autoconfigure,发现它包含一个spring.factories文件,这个文件中定义了两个自动配置类:

源码2:mybatis-spring-boot-autoconfigure/2.1.3/mybatis-spring-boot-autoconfigure-2.1.3.jar!/META-INF/spring.factories

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

由此可知,SpringBoot整合MyBatis的核心装配就在这两个自动配置类上。

11.3.2 MybatisLanguageDriverAutoConfiguration

从类名可知,MybatisLanguageDriverAutoConfiguration是一个与“语言驱动”相关的自动配置类。

MyBatis默认使用XML作为编写映射文件的实现,而从MyBatis 3.2版本开始支持第三方模板引擎框架作为编写映射文件的实现。

查阅MybatisLanguageDriverAutoConfiguration的源码可知,MyBatis支持的第三方模板引擎框架包括FreeMarker、Thymeleaf、Velocity等。但默认情况下,MyBatis不会引入这些模板引擎框架的依赖,因此使用IDE打开这个自动配置类时会产生error提示。

MyBatis不会引入这些模板引擎框架的依赖

11.3.3 MybatisAutoConfiguration

相比之下,MybatisAutoConfiguration才是MyBatis整合SpringBoot的核心,MyBatis内部支撑运行的所有组件都在这个配置类中创建。

11.3.3.1 SqlSessionFactory

SqlSessionFactory是MyBatis底层核心支撑,有了SqlSessionFactory就可以创建SqlSession,进而使用SqlSession的CRUD操作。

源码3MybatisAutoConfiguration.java

@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    // 设置数据源
    factory.setDataSource(dataSource);
    factory.setVfs(SpringBootVFS.class);
    // 设置MyBatis原生配置文件
    if (StringUtils.hasText(this.properties.getConfigLocation())) {
        factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
    }
    // 应用配置文件
    applyConfiguration(factory);
    if (this.properties.getConfigurationProperties() != null) {
        factory.setConfigurationProperties(this.properties.getConfigurationProperties());
    }
    // 设置插件
    if (!ObjectUtils.isEmpty(this.interceptors)) {
        factory.setPlugins(this.interceptors);
    }
    
    // 更多set操作

    // 实际构建SqlSessionFactory
    return factory.getObject();
}

由 源码3 可知,创建SqlSessionFactory的步骤就是把连接数据库的数据源、MyBatis原生的配置文件、相关插件、IOC容器中注册的MyBatis拦截器等组件一一应用到SqlSessionFactoryBean对象中,并调用其getObject方法实际构建SqlSessionFactory对象。

源码4SqlSessionFactoryBean.java

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

@Override
public void afterPropertiesSet() throws Exception {
    // 一些前置判断...
    this.sqlSessionFactory = buildSqlSessionFactory();
}

由 源码4 可知,getObject方法中会调用afterPropertiesSet方法,该方法会在进行一些前置判断后调用buildSqlSessionFactory方法,以构建实际的SqlSessionFactory对象。

由于buildSqlSessionFactory方法的篇幅较长,下面拆解来看。

(1)预准备MyBatis的全局配置对象
源码5SqlSessionFactoryBean.java

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    
    final Configuration targetConfiguration;
    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configuration != null) {
        // 如果传入了外部Configuration,则直接处理
        targetConfiguration = this.configuration;
        if (targetConfiguration.getVariables() == null) {
            targetConfiguration.setVariables(this.configurationProperties);
        } else if (this.configurationProperties != null) {
            targetConfiguration.getVariables().putAll(this.configurationProperties);
        }
    } else if (this.configLocation != null) {
        // 如果传入全局配置文件的路径,则封装XMLConfigBuilder对象以备加载配置文件
        xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
        targetConfiguration = xmlConfigBuilder.getConfiguration();
    } else {
        // 其他情况,执行默认策略
        targetConfiguration = new Configuration();
        Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
    }
    
    // ......
    
}

由 源码5 可知,buildSqlSessionFactory方法的第一部分逻辑是预准备MyBatis的全局配置对象Configuration。根据事先是否传入外部Configuration对象或是否传入全局配置文件路径configLocation来决定是否准备XMLConfigBuilder对象。

(2)处理内置组件
源码6SqlSessionFactoryBean.java

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    
    // ......
    
    Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
    Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);
    Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);
    
    // ......
}

由 源码6 可知,buildSqlSessionFactory方法的第二部分逻辑是紧接着的3个设置动作,分别对应MyBatis的三个组件。

  • ObjectFactory:对象工厂,负责创建MyBatis查询包装结果集时创建结果对象(模型类对象、Map等)。
  • ObjectWrapperFactory:对象包装工厂,负责对创建出的对象进行包装。
  • Vfs:虚拟文件系统,用于加载项目中的资源文件。
(3)处理别名
源码7SqlSessionFactoryBean.java

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    
    // ......
    
    // 处理包扫描的别名
    if (hasLength(this.typeAliasesPackage)) {
      scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream()
          .filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface())
          .filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
    }

    // 处理@Alias注解的别名
    if (!isEmpty(this.typeAliases)) {
      Stream.of(this.typeAliases).forEach(typeAlias -> {
        targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
        LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
      });
    }
    
    // ......
}

由 源码7 可知,buildSqlSessionFactory方法的第三部分逻辑是处理别名,包括包扫描注册的别名和@Alias注解注册的别名。

MyBatis允许在项目中为实体类定义别名,这使得在映射文件mapper.xml中可以简化类型编写,使映射文件更加简洁。

默认情况下,包扫描注册的别名就是类名本身(首字母大写),如果类上 标注了和@Alias注解,则会取注解的属性值。

(4)处理插件、类型处理器
源码8SqlSessionFactoryBean.java

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    
    // ......
    
    // 处理插件
    if (!isEmpty(this.plugins)) {
      Stream.of(this.plugins).forEach(plugin -> {
        targetConfiguration.addInterceptor(plugin);
        LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
      });
    }

    // 处理类型处理器
    if (hasLength(this.typeHandlersPackage)) {
      scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass())
          .filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
          .forEach(targetConfiguration.getTypeHandlerRegistry()::register);
    }

    if (!isEmpty(this.typeHandlers)) {
      Stream.of(this.typeHandlers).forEach(typeHandler -> {
        targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
        LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
      });
    }

    targetConfiguration.setDefaultEnumTypeHandler(defaultEnumTypeHandler);
    
    // ......
}

由 源码8 可知,buildSqlSessionFactory方法的第四部分逻辑是处理MyBatis插件以及类型处理器TypeHandler。

注意,在 源码3 中有这样一段逻辑:

if (!ObjectUtils.isEmpty(this.interceptors)) {
    factory.setPlugins(this.interceptors);
}

可见,此处的插件this.plugins是从IOC容器中获取到的。由此可以得出:SpringBoot整合MyBatis后可以直接向IOC容器中注册MyBatis的关键组件,底层的自动装配可以将这些组件应用到MyBatis中。

(5)处理其他内部组件
源码9SqlSessionFactoryBean.java

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    
    // ......
    
    // 脚本语言驱动器
    if (!isEmpty(this.scriptingLanguageDrivers)) {
        Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> {
            targetConfiguration.getLanguageRegistry().register(languageDriver);
            LOGGER.debug(() -> "Registered scripting language driver: '" + languageDriver + "'");
        });
    }
    Optional.ofNullable(this.defaultScriptingLanguageDriver)
            .ifPresent(targetConfiguration::setDefaultScriptingLanguage);
    // 数据库厂商标识器
    if (this.databaseIdProvider != null) {// fix #64 set databaseId before parse mapper xmls
        try {
            targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
        } catch (SQLException e) {
            throw new NestedIOException("Failed getting a databaseId", e);
        }
    }
    Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache);

    // ......
}

由 源码9 可知,,buildSqlSessionFactory方法的第五部分逻辑是处理其他一些不太重要的组件,如脚本语言驱动器(支持多种映射文件格式的编写)、数据库厂商标识器(为数据库可移植性提供了可能)。

(6)解析MyBatis全局配置文件
源码10SqlSessionFactoryBean.java

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    
    // ......
    
    if (xmlConfigBuilder != null) {
        try {
            xmlConfigBuilder.parse();
            // logger ...
        } // catch finally ...
    }

    // ......
}

由 源码10 可知,buildSqlSessionFactory方法的第六部分逻辑是解析MyBatis全局配置文件。如果项目中传入了configLocation,则此处会使用XmlConfigBuilder解析MyBatis配置文件,并应用于MyBatis全局配置对象Configuration中。

SpringBoot已经将MyBatis中的配置尽可能地移植到了application.properties中,因此这一步骤无需深究。

(7)处理数据源和事务工厂
源码11SqlSessionFactoryBean.java

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    
    // ......
    
    targetConfiguration.setEnvironment(new Environment(this.environment,
            this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,
            this.dataSource));

    // ......
}

由 源码11 可知,buildSqlSessionFactory方法的第七部分逻辑完成了两个组件的初始化:数据源this.dataSource和事务工厂this.transactionFactory。其中事务工厂使用的是SpringManagedTransactionFactory。

(8)解析XML映射文件
源码12SqlSessionFactoryBean.java

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    
    // ......
    
    if (this.mapperLocations != null) {
        if (this.mapperLocations.length == 0) {
            // logger ...
        } else {
            for (Resource mapperLocation : this.mapperLocations) {
                // 遍历XML映射文件的路径
                if (mapperLocation == null) {
                    continue;
                }
                try {
                    // 解析XML映射文件
                    XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                            targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
                    xmlMapperBuilder.parse();
                } // catch finally ...
                // logger ...
            }
        }
    } else {
        // logger ...
    }
    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}

由 源码12 可知,buildSqlSessionFactory方法的第八部分逻辑是解析XML映射文件。通过遍历XML映射文件路径,借助XMLMapperBuilder解析XML映射文件,封装成MappedStatement并注册到MyBatis的全局配置对象Configuration中。

经过八个部分逻辑,buildSqlSessionFactory方法执行完毕,SqlSessionFactory被成功创建,MyBatis的核心初始化完毕。

11.3.3.2 sqlSessionTemplate

自动配置类MybatisAutoConfiguration第二个要注入的组件是SqlSessionTemplate。

源码13SqlSessionTemplate.java

public class SqlSessionTemplate implements SqlSession, DisposableBean {

由 源码13 可知,SqlSessionTemplate本身是一个实现了SqlSession接口的模板类,可以直接调用SqlSession的CRUD方法。

源码14MybatisAutoConfiguration.java

@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    ExecutorType executorType = this.properties.getExecutorType();
    if (executorType != null) {
        return new SqlSessionTemplate(sqlSessionFactory, executorType);
    } else {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

由 源码14 可知,在构建SqlSessionTemplate时,需要传入SqlSessionFactory,因为只有传入SqlSessionFactory,SqlSessionTemplate才能获取到实际的SqlSession并调用其方法。

另外,SqlSessionTemplate构造方法的另一个参数是ExecutorType,这是一个枚举类。由类名可知,它指代SQL语句执行的类型。

源码15ExecutorType.java

public enum ExecutorType {
  SIMPLE, REUSE, BATCH
}

由 源码15 可知,ExecutorType内置的类型有三种:

  • SIMPLE:默认类型,这种模式会在SqlSession执行具体的CRUD操作时为每条SQL语句创建一个预处理对象PreparedStatement并逐条执行;
  • REUSE:该模式会复用同一条SQL语句对应创建的预处理对象PreparedStatement,在一定程度上提高MyBatis的执行效率;
  • BATCH:该模式不仅会复用预处理对象PreparedStatement,还会执行批量操作,这使得BATCH模式的执行效率是最高的。但BATCH模式有一个缺陷,即在执行insert语句时,如果插入的数据库表的主键是自增序列,则在事务提交之前无法从数据库获得实际的自增ID值,这种设计在某些业务场景中是不符合要求的。
11.3.3.3 AutoConfiguredMapperScannerRegistrar

由于SqlSessionFactory的构件中没有处理Mapper接口的扫描,因此在整合时MyBatis专门提供了一个适配SpringBoot项目的Mapper接口扫描注册器。

源码16MapperScan.java

@Import(MapperScannerRegistrar.class)
public @interface MapperScan {
源码17MybatisAutoConfiguration.java

@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {

由 源码16-17 可知,当SpringBoot项目中未标注@MapperScan注解时,MapperScannerRegistrar不会注册,而配置类MapperScannerRegistrarNotFoundConfiguration生效,使用@Import注解导入AutoConfiguredMapperScannerRegistrar组件。

源码18MybatisAutoConfiguration.java

public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {

    private BeanFactory beanFactory;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // logger ...

        // 取出主启动类所在包
        List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
        builder.addPropertyValue("processPropertyPlaceHolders", true);
        // 扫描表示了@Mapper注解的接口
        builder.addPropertyValue("annotationClass", Mapper.class);
        builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
        BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
        Stream.of(beanWrapper.getPropertyDescriptors())
                // Need to mybatis-spring 2.0.2+
                .filter(x -> x.getName().equals("lazyInitialization")).findAny()
                .ifPresent(x -> builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}"));
        registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
    }
}

由 源码18 可知,registerBeanDefinitions默认注册一个MapperScannerConfigurer扫描器,该扫描器的扫描规则是扫描SpringBoot主启动类所在包及其子包下所有标注了@Mapper注解的接口。

MapperScannerConfigurer扫描器注册后,在项目开发者只需要编写Mapper接口并标注@Mapper注解,就可被MapperScannerConfigurer扫描器自动扫描并注册到IOC容器中。

11.4 小结

第11章到此就梳理完毕了,本章的主题是:SpringBoot整合MyBatis。回顾一下本章的梳理的内容:

(三十五)SpringBoot整合MyBatis时的核心组件自动装配

更多内容请查阅分类专栏:SpringBoot源码解读与原理分析

第12章主要梳理:SpringBoot整合WebMvc。主要内容包括:

  • SpringBoot整合WebMvc的核心组件自动装配;
  • WebMvc核心组件的功能剖析;
  • DispatcherServlet的初始化原理与工作流程解析。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰色孤星A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值