1.单独使用MyBatis

在了解Spring 继承Mybatis 之前,先来看下如何单独使用MyBatis

数据表结构

1
2
3
4
5
6
7
8
9
10
CREATE TABLE message (
id bigint unsigned NOT NULL AUTO_INCREMENT,
message_key VARCHAR(255) COMMENT '消息唯一键,用于做回查的标识',
message text COMMENT '消息内容',
message_status INT DEFAULT 1 COMMENT '消息状态 1-prepare 2-commit 3-rollback 4-unknown',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE INDEX idx_messageKey(message_key)
) ENGINE=InnoDB;

mybatis-config.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置全局设置 -->
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

<!-- 配置别名 -->
<typeAliases>
<typeAlias alias="Message" type="com.example.codingInAction.model.Message"/>
</typeAliases>

<!-- 配置环境 -->
<environments default="development">
<environment id="development">
<!-- 配置事务管理器 -->
<transactionManager type="jdbc"/>
<!-- 配置数据源 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/codingInAction"/>
<property name="username" value="root"/>
<property name="password" value="24048@Ms"/>
</dataSource>
</environment>
</environments>

<!-- 对应数据库操作接口的SQL映射 -->
<mappers>
<mapper resource="mappers/MessageMapper.xml"/>
</mappers>

</configuration>

mappers/MessageMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?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.example.codingInAction.mapper.MessageMapper">

<!-- 插入操作 插入数据后获取自动生成的主键 -->
<insert id="insertMessage" parameterType="com.example.codingInAction.model.Message"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO message (
message,
message_key,
message_status
) VALUES (
#{message},
#{messageKey},
#{messageStatus}
)
</insert>

<select id="findByMessageKey" resultType="com.example.codingInAction.model.Message">
SELECT
id,
message AS message,
message_key AS "messageKey",
message_status AS "messageStatus",
create_time AS "createTime",
update_time AS "updateTime"
FROM
message
WHERE
message_key = #{messageKey}
</select>
</mapper>



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class Message {

private int id;
private String message;
private String messageKey;
private int messageStatus;

// getter and setter
}

public interface MessageMapper {

int insertMessage(Message message);

Message findByMessageKey(Map<String, Object> params);
}


public class MyBatisUtil {

private final static SqlSessionFactory sqlSessionFactory;
static {
String resource = "mybatis-config.xml";
Reader reader;

try {
reader = Resources.getResourceAsReader(resource);
} catch (IOException e) {
throw new RuntimeException(e);
}
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
}
public static SqlSessionFactory getSqlSessionFactory() {
return sqlSessionFactory;
}
}

class MessageMapperTest {

static SqlSessionFactory sqlSessionFactory;
static {
sqlSessionFactory = MyBatisUtil.getSqlSessionFactory();
}
@Test
void messageTest1() {

Message m = new Message();
m.setMessage("Test Message");
m.setMessageKey("test key");
m.setMessageStatus(1);
SqlSession sqlSession = sqlSessionFactory.openSession();

try {
MessageMapper messageMapper = sqlSession.getMapper(MessageMapper.class);
messageMapper.insertMessage(m);

Map<String, Object> params = new HashMap<>();
params.put("messageKey", "test key");
Message result = messageMapper.findByMessageKey(params);
assertEquals("findByMessageKey", "test key", result.getMessageKey());
sqlSession.commit();
} finally {
sqlSession.close();
}

}
}

1.1 SqlSessionFactory

SqlSessionFactory是MyBatis的核心, 其作用如下

  1. 创建SqlSession对象SqlSessionFactory通过其方法创建SqlSession实例。SqlSession用于执行数据库操作,如查询、插入、更新和删除等。每次需要进行数据库操作时,都会从SqlSessionFactory中获取一个新的SqlSession对象。
  2. 管理SqlSession生命周期SqlSessionFactory不仅负责创建SqlSession,还负责管理其生命周期,包括资源的分配和释放。SqlSession对象需要在操作完成后关闭,以释放数据库连接等资源。
  3. 加载和解析配置SqlSessionFactory在初始化时会加载和解析MyBatis的配置文件(如mybatis-config.xml)和Mapper XML文件。这些配置文件定义了数据库连接、事务管理、Mapper映射等信息。
  4. 缓存机制SqlSessionFactory配置中可以包含缓存设置,如一级缓存和二级缓存。缓存机制可以提高数据库访问的性能,减少重复查询的开销。

1.2 SqlSession

1
SqlSession sqlSession = sqlSessionFactory.openSession();

SqlSession 是 MyBatis 与数据库交互的核心。
通过 SqlSession,开发者可以执行 SQL 语句、获取映射器(Mapper)、管理事务等操作。可以将 SqlSession 看作是 MyBatis 的会话,类似于 JDBC 中的 Connection 对象。

1.3 mapper 动态代理

1
MessageMapper messageMapper = sqlSession.getMapper(MessageMapper.class)

以下代码获取到的是MessageMapper的动态代理对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 public class DefaultSqlSession implements SqlSession {
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
}

public class Configuration {
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
}

public class MapperRegistry {
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
}

public class MapperProxyFactory<T> {
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);
}
}

Configuration 对象对应的就是mybatis-config.xml 这个配置文件,层层深入源码最后发现是熟悉的JDK 动态代理Proxy.newProxyInstance, 说明最后生成的mapper 对象是一个JDK 动态代理对象。

1.4 SQL执行

再看MapperProxyFactory.newInstance 源码,

1
2
3
4
5
6
7
8
9
10
11
public class MapperProxyFactory<T> {
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);
}
}
public class MapperProxy<T> implements InvocationHandler, Serializable {}

Proxy.newProxyInstance中参数InvocationHandler 传入的是MapperProxy, 看一下MapperProxy实现InvocationHandler 的具体逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MapperProxy<T> implements InvocationHandler, Serializable {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}

2. Spring集成MyBatis

在MyBatis与Spring集成时,可以将MyBatis的配置直接放在Spring的配置文件(如applicationContext.xml)中,从而简化配置管理。通过这种方式,可以在一个配置文件中集中管理数据源、事务管理、MyBatis的SqlSessionFactory和Mapper扫描等配置。

applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd"

<!-- 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/codingInAction"/>
<property name="username" value="root"/>
<property name="password" value="24048@Ms"/>
</bean>

<!-- MyBatis SqlSessionFactory配置 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath*:/mappers/*.xml"/>
</bean>

<!-- MyBatis MapperScannerConfigurer配置 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.codingInAction.mapper"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>


<bean id="messageMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="com.example.codingInAction.mapper.MessageMapper"></property>
<property name="sqlSessionFactory" ref="sqlSessionFactory"></property>
</bean>
</beans>

Spring整合MyBatis的优势主要在于使用上,我们来看看Spring中使用MyBatis的用法。 测试中我们看到,在Spring中使用MyBatis非常方便,用户甚至无法察觉自己正在使用MyBatis,而这一切相对于独立使用MyBatis时必须要做的各种冗余操作来说无非是大大简化了我们的工作量。

就是从Spring 容器中获取想要的mapper bean, 然后就可以执行对应的SQL 操作了

1
2
3
4
5
6
7
8
9
10
11
12
class MessageMapperTest {
@Test
void messageTest2() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MessageMapper messageMapper = (MessageMapper) context.getBean("messageMapper");
Map<String, Object> params = new HashMap<>();
params.put("messageKey", "test key");
Message result = messageMapper.findByMessageKey(params);
System.out.println(result.getMessage());
}

}

2.1 SqlSessionFactoryBean

从配置文件中可以看到, sqlSessionFactory 是通过SqlSessionFactoryBean配置的。

通过Spring配置SqlSessionFactoryBean,可以将SqlSessionFactory作为Spring的一个Bean进行管理。

1
2
public class SqlSessionFactoryBean  
implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {}

SqlSessionFactoryBean 实现了InitializingBeanFactoryBean两个接口

2.1.1 InitializingBean

实现此接口的bean会在初始化时调用重写的InitializingBean.afterPropertiesSet方法来进行bean的逻辑初始化

获取SqlSessionFactoryBean实例,跳过getBean 的其他代码直接来到init 阶段, InitionlizingBean 相关的逻辑

2.1.2 FactoryBean

SqlSessionFactoryBean.getObject() 中就是把init 阶段实例化的SqlSessonFactory实例返回

2.2 MapperFactoryBean

1
MessageMapper messageMapper = (MessageMapper) context.getBean("messageMapper");

从配置文件中可以看到,messageMapper对应的MessageFactoryBean 也是一个FactoryBean, 重点看下getObject方法

1
2
3
4
5
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {@Override  
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
}

Spring中获取的名为messageMapper的bean,其实是与单独使用MyBatis完成了一样的功能,从源码上看,在bean的创建过程也是使用了MyBatis中的原生方法sqlSession.getMapper(MessageMapper.class)进行了再一次封装。
和不集成直接使用MyBatis 获取对应mapper实例其实本质上是一样的逻辑,从getMapper 进去 是完全一样的JDK 动态代理逻辑

1
MessageMapper messageMapper = sqlSession.getMapper(MessageMapper.class);

2.2 MapperScannerConfigurer

在Spring项目中,通过MapperScannerConfigurer类自动扫描所有的Mapper接口,并通过Spring的依赖注入机制直接使用这些接口,是一种简化并推荐的方式。这种方法不需要开发者手动实现Mapper接口的具体方法,也不需要手动管理SqlSession,从而大大简化了代码和配置。

1
2
3
4
5
6
7
8
9
10
11
    <!-- MyBatis MapperScannerConfigurer配置 -->
<!-- <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">-->
<!-- <property name="basePackage" value="com.example.codingInAction.mapper"/>-->
<!-- <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>-->
<!-- </bean>-->


<bean id="messageMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="com.example.codingInAction.mapper.MessageMapper"></property>
<property name="sqlSessionFactory" ref="sqlSessionFactory"></property>
</bean>

3. Spring Boot集成MyBatis

在Spring Boot项目中集成MyBatis通常使用mybatis-spring-boot-starter,这个由MyBatis 提供的Starter简化了配置和集成过程。使用Spring Boot集成MyBatis时,你不需要显式地配置SqlSessionFactoryBean,通过SpringBoot 的自动配置机制, 这些配置均可以自动完成。你只需要

  1. 添加依赖
  2. 在application.properties 中配置DataSource 即可
1
2
3
4
5
6
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>

关于SpringBoot 自动配置机制的实现原理,可以点击Spring boot 自动配置实现原理阅读了解。 下面将忽略MybatisAutoConfiguration配置的加载和BeanDefinition 注册流程,直接进入实例化过程进行分析

3.1 MybatisAutoConfiguration的实例化

org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

Spring boot 自动配置实现原理一文中,已经详细介绍过自动配置类的加载与BeanDefinition 注册过程, 在其中9.5 小节中讲了在ConfigurationClassPostProcessor中重写的BeanFactoryPostProcessor.postProcessBeanFactory中,实现了对@Configuration 注解动态代理类的生成。 这里讲以MybatisAutoConfiguration 的实例化为例详细讲解这一过程



来看下拦截逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class ConfigurationClassEnhancer {
private static final Callback[] CALLBACKS = new Callback[] {
new BeanMethodInterceptor(),
new BeanFactoryAwareMethodInterceptor(),
NoOp.INSTANCE
};

private static class BeanMethodInterceptor implements MethodInterceptor, ConditionalCallback {
@Override
@Nullable
public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs,
MethodProxy cglibMethodProxy) throws Throwable {

ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance);
String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod);

// Determine whether this bean is a scoped-proxy
if (BeanAnnotationHelper.isScopedProxy(beanMethod)) {
String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName);
if (beanFactory.isCurrentlyInCreation(scopedBeanName)) {
beanName = scopedBeanName;
}
}

if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) &&
factoryContainsBean(beanFactory, beanName)) {
Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName);
if (factoryBean instanceof ScopedProxyFactoryBean) {
// Scoped proxy factory beans are a special case and should not be further proxied
}
else {
// It is a candidate FactoryBean - go ahead with enhancement
return enhanceFactoryBean(factoryBean, beanMethod.getReturnType(), beanFactory, beanName);
}
}

if (isCurrentlyInvokedFactoryMethod(beanMethod)) {

if (logger.isInfoEnabled() &&
BeanFactoryPostProcessor.class.isAssignableFrom(beanMethod.getReturnType())) {
}
return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
}

return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName);
}

}


最后生成的代理类重新设置给了BeanDefinition 的一个属性。

3.2 sqlSessionFactory 实例化

3.2.1 instantiateUsingFactoryMethod

在 Spring 中,有几种常见的 Bean 实例化方式:

  1. 使用构造函数实例化。
  2. 使用Supplier实例化。
  3. 使用工厂方法实例化。

针对sqlSessionFactory ,其实例化过程使用的是 instantiateUsingFactoryMethod工厂方法实例化



最后解析出, 使用instantiateUsingFactoryMethod实例化sqlSessionFactory 时, 用到的factoryBean 是 MybatisAutoConfiguration 的动态代理对象,用的方法是sqlSessionFactory

3.2.2 动态代理拦截

这里来到经典的反射逻辑来调用实例对象上的方法,此时factoryBean是CGLIB 动态代理对象, 所以在调用方法时会先走在拦截逻辑上