文章目录
前言
在实际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项目搭建
搭建如下图所示的项目:
实体类:
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包,没有具体的内容:
进入该依赖包的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提示。
11.3.3 MybatisAutoConfiguration
相比之下,MybatisAutoConfiguration才是MyBatis整合SpringBoot的核心,MyBatis内部支撑运行的所有组件都在这个配置类中创建。
11.3.3.1 SqlSessionFactory
SqlSessionFactory是MyBatis底层核心支撑,有了SqlSessionFactory就可以创建SqlSession,进而使用SqlSession的CRUD操作。
源码3:MybatisAutoConfiguration.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对象。
源码4:SqlSessionFactoryBean.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的全局配置对象
源码5:SqlSessionFactoryBean.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)处理内置组件
源码6:SqlSessionFactoryBean.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)处理别名
源码7:SqlSessionFactoryBean.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)处理插件、类型处理器
源码8:SqlSessionFactoryBean.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)处理其他内部组件
源码9:SqlSessionFactoryBean.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全局配置文件
源码10:SqlSessionFactoryBean.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)处理数据源和事务工厂
源码11:SqlSessionFactoryBean.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映射文件
源码12:SqlSessionFactoryBean.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。
源码13:SqlSessionTemplate.java
public class SqlSessionTemplate implements SqlSession, DisposableBean {
由 源码13 可知,SqlSessionTemplate本身是一个实现了SqlSession接口的模板类,可以直接调用SqlSession的CRUD方法。
源码14:MybatisAutoConfiguration.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语句执行的类型。
源码15:ExecutorType.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接口扫描注册器。
源码16:MapperScan.java
@Import(MapperScannerRegistrar.class)
public @interface MapperScan {
源码17:MybatisAutoConfiguration.java
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
由 源码16-17 可知,当SpringBoot项目中未标注@MapperScan注解时,MapperScannerRegistrar不会注册,而配置类MapperScannerRegistrarNotFoundConfiguration生效,使用@Import注解导入AutoConfiguredMapperScannerRegistrar组件。
源码18:MybatisAutoConfiguration.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的初始化原理与工作流程解析。