
MyBatis作为国内最流行的持久层框架之一,其核心设计精巧且实用。本文将从底层原理出发,结合实战代码,深入拆解MyBatis中最核心的三个机制:Mapper接口的动态代理实现、一级缓存的生效机制以及二级缓存的生效机制。通过本文,你将不仅知其然,更知其所以然,能够在实际开发中灵活运用这些机制解决问题。
在MyBatis中,我们只需要编写Mapper接口,不需要编写实现类,就能直接调用接口方法执行SQL。这背后的核心原理就是JDK动态代理。MyBatis会在运行时为Mapper接口生成一个动态代理对象,当我们调用接口方法时,实际上是调用代理对象的invoke方法,在该方法中完成SQL的解析、参数绑定、执行和结果映射。

MapperProxy是动态代理的核心类,实现了InvocationHandler接口。它持有SqlSession、Mapper接口和方法缓存三个核心对象。当调用代理对象的方法时,会进入invoke方法,该方法会判断是否为Object类的方法(如toString、hashCode),如果是则直接执行;否则会从缓存中获取MapperMethod对象并执行。
MapperMethod封装了Mapper接口方法的完整信息,包括方法名、参数类型、返回值类型、SQL语句类型等。它的execute方法是SQL执行的入口,会根据SQL类型(SELECT/INSERT/UPDATE/DELETE)调用SqlSession的对应方法完成数据库操作。
MapperProxyFactory是Mapper代理对象的工厂类,负责创建MapperProxy实例并生成动态代理对象。每个Mapper接口对应一个MapperProxyFactory实例。
首先创建一个Maven项目,pom.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/>
</parent>
<groupId>com.jam</groupId>
<artifactId>mybatis-demo</artifactId>
<version>1.0.0</version>
<name>mybatis-demo</name>
<description>MyBatis核心机制实战演示</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<fastjson2.version>2.0.43</fastjson2.version>
<swagger.version>2.3.0</swagger.version>
<guava.version>33.0.0-jre</guava.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yml配置如下:
spring:
datasource:
driver-class-name:com.mysql.cj.jdbc.Driver
url:jdbc:mysql://localhost:3306/mybatis_demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
username:root
password:root
mybatis-plus:
configuration:
log-impl:org.apache.ibatis.logging.stdout.StdOutImpl
cache-enabled:true
global-config:
db-config:
id-type:auto
SQL脚本如下:
CREATE DATABASEIFNOTEXISTS mybatis_demo DEFAULTCHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci;
USE mybatis_demo;
CREATETABLEIFNOTEXISTS`user` (
`id`BIGINTNOTNULL AUTO_INCREMENT COMMENT'主键ID',
`username`VARCHAR(50) NOTNULLCOMMENT'用户名',
`password`VARCHAR(100) NOTNULLCOMMENT'密码',
`email`VARCHAR(100) DEFAULTNULLCOMMENT'邮箱',
`create_time` DATETIME DEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',
`update_time` DATETIME DEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',
PRIMARY KEY (`id`),
UNIQUEKEY`uk_username` (`username`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='用户表';
INSERTINTO`user` (`username`, `password`, `email`) VALUES
('zhangsan', '123456', 'zhangsan@example.com'),
('lisi', '123456', 'lisi@example.com'),
('wangwu', '123456', 'wangwu@example.com');
实体类User.java:
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 用户实体类
* @author ken
*/
@Data
@TableName("user")
@Schema(description = "用户实体")
publicclass User implements Serializable {
privatestaticfinallong serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID")
private Long id;
/**
* 用户名
*/
@Schema(description = "用户名")
private String username;
/**
* 密码
*/
@Schema(description = "密码")
private String password;
/**
* 邮箱
*/
@Schema(description = "邮箱")
private String email;
/**
* 创建时间
*/
@Schema(description = "创建时间")
private LocalDateTime createTime;
/**
* 更新时间
*/
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
Mapper接口UserMapper.java:
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 用户Mapper接口
* @author ken
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**
* 根据用户名查询用户
* @param username 用户名
* @return 用户信息
*/
@Select("SELECT * FROM user WHERE username = #{username}")
User selectByUsername(@Param("username") String username);
}
测试类MybatisProxyTest.java:
package com.jam.demo;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* MyBatis动态代理测试类
* @author ken
*/
@Slf4j
@SpringBootTest
publicclass MybatisProxyTest {
@Autowired
private SqlSessionFactory sqlSessionFactory;
/**
* 测试Mapper动态代理
*/
@Test
public void testMapperProxy() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
log.info("Mapper代理对象类型:{}", userMapper.getClass().getName());
log.info("Mapper代理对象是否为Proxy:{}", java.lang.reflect.Proxy.isProxyClass(userMapper.getClass()));
User user = userMapper.selectByUsername("zhangsan");
log.info("查询结果:{}", user);
}
}
}
运行测试类,你会看到如下关键日志:
Mapper代理对象类型:com.sun.proxy.$Proxy87
Mapper代理对象是否为Proxy:true
这证明了UserMapper的实例确实是JDK动态代理生成的对象。
一级缓存是SqlSession级别的缓存,默认开启且无法关闭(只能调整缓存范围)。它的作用范围是同一个SqlSession实例,当同一个SqlSession执行相同的SQL查询时,会直接从缓存中获取结果,而不会再次查询数据库。

一级缓存的底层实现类是PerpetualCache,它内部使用一个HashMap来存储缓存数据,key是CacheKey对象(由SQL语句、参数、分页信息等组成),value是查询结果。
测试类FirstLevelCacheTest.java:
package com.jam.demo;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.support.TransactionTemplate;
/**
* 一级缓存测试类
* @author ken
*/
@Slf4j
@SpringBootTest
publicclass FirstLevelCacheTest {
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
private UserMapper userMapper;
/**
* 测试一级缓存生效:同一个SqlSession,相同查询
*/
@Test
public void testFirstLevelCacheHit() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
log.info("第一次查询...");
User user1 = mapper.selectById(1L);
log.info("第一次查询结果:{}", user1);
log.info("第二次查询...");
User user2 = mapper.selectById(1L);
log.info("第二次查询结果:{}", user2);
log.info("两次查询结果是否为同一对象:{}", user1 == user2);
}
}
/**
* 测试一级缓存失效:不同SqlSession
*/
@Test
public void testFirstLevelCacheMissDifferentSession() {
try (SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession()) {
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
log.info("SqlSession1查询...");
User user1 = mapper1.selectById(1L);
log.info("SqlSession1查询结果:{}", user1);
log.info("SqlSession2查询...");
User user2 = mapper2.selectById(1L);
log.info("SqlSession2查询结果:{}", user2);
log.info("两次查询结果是否为同一对象:{}", user1 == user2);
}
}
/**
* 测试一级缓存失效:执行增删改操作
*/
@Test
public void testFirstLevelCacheMissAfterUpdate() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
log.info("第一次查询...");
User user1 = mapper.selectById(1L);
log.info("第一次查询结果:{}", user1);
log.info("执行更新操作...");
User updateUser = new User();
updateUser.setId(1L);
updateUser.setEmail("new_email@example.com");
mapper.updateById(updateUser);
sqlSession.commit();
log.info("第二次查询...");
User user2 = mapper.selectById(1L);
log.info("第二次查询结果:{}", user2);
log.info("两次查询结果是否为同一对象:{}", user1 == user2);
}
}
/**
* 测试一级缓存失效:手动清空缓存
*/
@Test
public void testFirstLevelCacheMissAfterClear() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
log.info("第一次查询...");
User user1 = mapper.selectById(1L);
log.info("第一次查询结果:{}", user1);
log.info("手动清空一级缓存...");
sqlSession.clearCache();
log.info("第二次查询...");
User user2 = mapper.selectById(1L);
log.info("第二次查询结果:{}", user2);
log.info("两次查询结果是否为同一对象:{}", user1 == user2);
}
}
}
运行测试类,你会发现:
SqlSession执行相同查询时,只执行一次SQL,第二次直接从缓存获取,且两次结果是同一对象。SqlSession、执行增删改操作、手动清空缓存都会导致一级缓存失效。二级缓存是Mapper级别的缓存,默认关闭,需要手动开启。它的作用范围是同一个Mapper接口的namespace,不同的SqlSession可以共享二级缓存。
在application.yml中开启二级缓存:
mybatis-plus:
configuration:
cache-enabled: true
在Mapper接口上添加@CacheNamespace注解:
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 用户Mapper接口
* @author ken
*/
@Mapper
@CacheNamespace
publicinterface UserMapper extends BaseMapper<User> {
/**
* 根据用户名查询用户
* @param username 用户名
* @return 用户信息
*/
@Select("SELECT * FROM user WHERE username = #{username}")
User selectByUsername(@Param("username") String username);
}
注意:实体类必须实现Serializable接口,因为二级缓存可能会将数据序列化到磁盘或网络传输。

二级缓存的一个重要特点是:只有当SqlSession提交或关闭时,查询结果才会被写入二级缓存。这是为了避免脏读,确保只有提交后的数据才会被缓存。
测试类SecondLevelCacheTest.java:
package com.jam.demo;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* 二级缓存测试类
* @author ken
*/
@Slf4j
@SpringBootTest
publicclass SecondLevelCacheTest {
@Autowired
private SqlSessionFactory sqlSessionFactory;
/**
* 测试二级缓存生效:不同SqlSession,相同查询,SqlSession提交
*/
@Test
public void testSecondLevelCacheHit() {
try (SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession()) {
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
log.info("SqlSession1查询...");
User user1 = mapper1.selectById(1L);
log.info("SqlSession1查询结果:{}", user1);
log.info("SqlSession1提交...");
sqlSession1.commit();
log.info("SqlSession2查询...");
User user2 = mapper2.selectById(1L);
log.info("SqlSession2查询结果:{}", user2);
log.info("两次查询结果是否为同一对象:{}", user1 == user2);
log.info("两次查询结果内容是否相同:{}", user1.equals(user2));
}
}
/**
* 测试二级缓存失效:SqlSession未提交
*/
@Test
public void testSecondLevelCacheMissWithoutCommit() {
try (SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession()) {
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
log.info("SqlSession1查询...");
User user1 = mapper1.selectById(1L);
log.info("SqlSession1查询结果:{}", user1);
log.info("SqlSession2查询...");
User user2 = mapper2.selectById(1L);
log.info("SqlSession2查询结果:{}", user2);
log.info("两次查询结果是否为同一对象:{}", user1 == user2);
}
}
/**
* 测试二级缓存失效:执行增删改操作
*/
@Test
public void testSecondLevelCacheMissAfterUpdate() {
try (SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession()) {
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
log.info("SqlSession1查询并提交...");
User user1 = mapper1.selectById(1L);
sqlSession1.commit();
log.info("SqlSession1查询结果:{}", user1);
log.info("SqlSession2执行更新操作并提交...");
User updateUser = new User();
updateUser.setId(1L);
updateUser.setEmail("second_level_cache@example.com");
mapper2.updateById(updateUser);
sqlSession2.commit();
log.info("SqlSession3查询...");
User user3 = mapper3.selectById(1L);
log.info("SqlSession3查询结果:{}", user3);
}
}
}
运行测试类,你会发现:
SqlSession提交后,第二个SqlSession执行相同查询时,会直接从二级缓存获取结果(注意:两次结果不是同一对象,因为二级缓存会反序列化对象)。SqlSession未提交、执行增删改操作都会导致二级缓存失效。对比项 | 一级缓存 | 二级缓存 |
|---|---|---|
作用范围 | SqlSession级别 | Mapper namespace级别 |
默认状态 | 默认开启 | 默认关闭 |
开启方式 | 无需配置 | 全局配置+Mapper注解 |
存储介质 | 内存(HashMap) | 内存(可扩展至磁盘) |
失效场景 | 不同SqlSession、增删改、手动清空 | 增删改、缓存超时、内存不足 |
事务要求 | 无 | 需SqlSession提交/关闭才写入 |
序列化要求 | 无 | 实体类需实现Serializable |
SqlSessionsqlSession.clearCache()SqlSession未提交或关闭本文深入拆解了MyBatis的三个核心机制:Mapper动态代理、一级缓存和二级缓存。通过底层原理分析、流程图展示和实战代码示例,我们不仅理解了这些机制的工作原理,更掌握了它们在实际开发中的使用方法和注意事项。在实际项目中,我们应该根据业务场景合理使用缓存,避免缓存带来的脏读问题,同时也要注意缓存的性能优化。希望本文能对你有所帮助,让你在MyBatis的使用上更加得心应手。