🪁🍁 希望本文能给您带来帮助,如果有任何问题,欢迎批评指正!🐅🐾🍁🐥
导航参见:
MyBatis:SpringBoot结合MyBatis、MyBatis插件机制的原理分析与实战
MyBatis源码:MyBatis源码超详细的解析与核心组件总结
一、背景
MyBatis 是一个非常灵活的持久层框架,它内部封装了 jdbc,使开发者只需要关注 sql 语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程,除了提供了丰富的配置选项和强大的 SQL 映射能力外,它还支持插件机制,允许开发者在 SQL 执行的生命周期中自定义逻辑。本文将详细介绍Spring Boot项目中结合MyBatis、以及MyBatis的插件机制应用,希望本文对您工作有所帮助。
二、Spring Boot项目中结合MyBatis
2.1 数据准备
本次演示用到mysql5.7数据库进行操作
create database if not exists mybatis_demo;
use mybatis_demo;
create table user(
id int unsigned primary key auto_increment comment 'ID',
name varchar(100) comment '姓名',
age tinyint unsigned comment '年龄',
gender tinyint unsigned comment '性别, 1:男, 2:女',
phone varchar(11) comment '手机号'
) comment '用户表';
insert into user(id, name, age, gender, phone) VALUES (null,'白眉鹰王',55,'1','18800000000');
insert into user(id, name, age, gender, phone) VALUES (null,'金毛狮王',45,'1','18800000001');
insert into user(id, name, age, gender, phone) VALUES (null,'青翼蝠王',38,'1','18800000002');
insert into user(id, name, age, gender, phone) VALUES (null,'紫衫龙王',42,'2','18800000003');
insert into user(id, name, age, gender, phone) VALUES (null,'光明左使',37,'1','18800000004');
insert into user(id, name, age, gender, phone) VALUES (null,'光明右使',48,'1','18800000005');
2.2 pom.xml依赖增加
parent 是集成了父工程
mysql驱动依赖、mybatis的起步依赖、springboot启动web、 lombok 注解
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.7.10</version>
</parent>
<groupId>com.wasteland</groupId>
<artifactId>BlogSourceCode</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>BlogSourceCode</name>
<description>BlogSourceCode</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<!-- 原本是不需要单独引入mybatis的,只是这个3.4.6版本有source资源方便分析源码 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.3 application.yml实现
数据库配置:启动类、数据库、用户名、密码
开启端口配置:默认为8080
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false
username: admin
password: 123456
server:
port: 8082
mybatis:
# MyBatis全局配置文件路径
config-location: classpath:mybatis-config.xml
# 控制台日志输出记录,开发调试使用
logging:
level:
com.wasteland.blogsourcecode.mybatisdemo.mapper:
debug
2.4 代码层实现
MyBatis 是一个优秀的持久层框架,支持 XML 配置和注解两种方式来实现数据库操作。下面我将分别介绍这两种实现方式,这里先介绍两种实现方式共用的代码部分。
(1)pojo层
建立实体类映射前文中数据表里的字段:
package com.wasteland.blogsourcecode.mybatisdemo.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor //无参构造
@AllArgsConstructor //有参构造
public class User {
private Integer id;
private String name;
private Short age;
private Short gender;
private String phone;
}
(2)Service层
package com.wasteland.blogsourcecode.mybatisdemo.service;
import com.wasteland.blogsourcecode.mybatisdemo.pojo.User;
import java.util.List;
public interface UserService {
User findById(Integer id);
List<User> findAll();
String DelteById(Integer id);
String AddUser(User user);
String UpdateUser(User user);
}
实现类:UserServiceImpl.java
findById(Integer id):根据用户ID查询单个用户信息。
findAll():查询所有用户信息并返回用户列表。
DelteById(Integer id):根据用户ID删除用户信息,成功后返回"删除成功"的消息。
AddUser(User user):向数据库中添加新的用户信息,成功后返回"添加成功"的消息。
UpdateUser(User user):更新用户信息,成功后返回"更新成功"的消息。
package com.wasteland.blogsourcecode.mybatisdemo.service.impl;
import com.wasteland.blogsourcecode.mybatisdemo.mapper.UserMapper;
import com.wasteland.blogsourcecode.mybatisdemo.pojo.User;
import com.wasteland.blogsourcecode.mybatisdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User findById(Integer id) {
return userMapper.findById(id);
}
@Override
public List<User> findAll() {
return userMapper.findAll();
}
@Override
public String DelteById(Integer id) {
userMapper.DelteById(id);
return "删除成功";
}
@Override
public String AddUser(User user) {
userMapper.AddUser(user);
return "添加成功";
}
@Override
public String UpdateUser(User user) {
userMapper.UpdateUser(user);
return "更新成功";
}
}
(3)Controller层
userService 自动注入了实现类,通过实现类来进行操作。
package com.wasteland.blogsourcecode.mybatisdemo.controller;
import com.wasteland.blogsourcecode.mybatisdemo.pojo.User;
import com.wasteland.blogsourcecode.mybatisdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/findById")
public User findById(Integer id){
return userService.findById(id);
}
@RequestMapping("/findAll")
public List<User> findAll() {
return userService.findAll();
}
@RequestMapping("/AddUser")
public String AddUser(User user){
return userService.AddUser(user);
}
@RequestMapping("/DelteById")
public String DelteById(Integer id){
return userService.DelteById(id);
}
@RequestMapping("/UpdateUser")
public String UpdateUser(User user){
return userService.UpdateUser(user);
}
}
2.4.1 基于注解的Mapper
注解实现和xml配置实现这两种方式主要在于它们的mapper层不一样。
package com.wasteland.blogsourcecode.mybatisdemo.mapper;
import com.wasteland.blogsourcecode.mybatisdemo.pojo.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface UserMapper {
@Select("select * from user where id = #{id}")
User findById(Integer id);
@Select("select * from user")
List<User> findAll();
@Delete("delete from user where id = #{id}")
void DelteById(Integer id);
@Insert("insert into user(name,age,gender,phone) values(#{name},#{age},#{gender},#{phone})")
void AddUser(User user);
@Update("update user set name = #{name},age = #{age},gender = #{gender},phone = #{phone} where id = #{id}")
void UpdateUser(User user);
}
2.4.2 基于XML配置的Mapper
如果是xml配置的实现方式,需要编写xml配置文件:一个全局xml配置文件,一个mapper层接口的映射xml文件。
mapper接口
package com.wasteland.blogsourcecode.mybatisdemo.mapper;
import com.wasteland.blogsourcecode.mybatisdemo.pojo.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface UserMapper {
User findById(Integer id);
List<User> findAll();
void DelteById(Integer id);
void AddUser(User user);
void UpdateUser(User user);
}
mapper接口映射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.wasteland.blogsourcecode.mybatisdemo.mapper.UserMapper">
<select id="findById" resultType="com.wasteland.blogsourcecode.mybatisdemo.pojo.User">
select * from user where id = #{id}
</select>
<select id="findAll" resultType="com.wasteland.blogsourcecode.mybatisdemo.pojo.User">
select * from user
</select>
<delete id="DelteById" parameterType="int">
delete from user where id = #{id}
</delete>
<insert id="AddUser" parameterType="com.wasteland.blogsourcecode.mybatisdemo.pojo.User">
insert into user(name,age,gender,phone) values(#{name},#{age},#{gender},#{phone})
</insert>
<update id="UpdateUser" parameterType="com.wasteland.blogsourcecode.mybatisdemo.pojo.User">
update user set name = #{name},age = #{age},gender = #{gender},phone = #{phone} where id = #{id}
</update>
</mapper>
全局xml配置文件
mybatis-config.xml 定义了数据库连接、日志设置、别名等全局配置。
<?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="lazyLoadingEnabled" value="true"/>
<!-- 当启用延迟加载时,任何延迟属性都会加载其所有的关联属性 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 允许单条SQL返回多结果集(需要兼容的驱动) -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 使用列标签代替列名称 -->
<setting name="useColumnLabel" value="true"/>
<!-- 允许 JDBC 支持生成的键值 -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 配置默认的执行器。SIMPLE:普通的执行器;REUSE:执行器会重用预处理语句;BATCH:执行器会重用预处理语句和批量更新 -->
<setting name="defaultExecutorType" value="SIMPLE"/>
<!-- 设置超时时间 -->
<setting name="defaultStatementTimeout" value="25"/>
<!-- 是否开启自动驼峰命名规则(camel case)映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!-- 环境配置 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://101.37.160.246:3306/mybatisdemo?useSSL=false&serverTimezone=UTC"/>
<property name="username" value="admin"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!-- Mapper 映射文件 -->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
<!-- 添加其他映射器文件 -->
</mappers>
</configuration>
值得一提的是:
这里的全局配置文件可以去掉,然后把配置都配在前文的application.yml中,能达到一样的效果。记住一下加载的顺序即可:加载 mybatis-config.xml——>加载 application.properties/yml 中的 MyBatis 配置——>应用编程式配置(通过 Java Config),它的优先级和加载顺序刚好相反。
三、MyBatis插件机制
3.1 插件概述
一般开源框架都会留一个口子去让开发者自行扩展,从而完成逻辑增强,比如说Spring框架里的BeanPostProcessor
接口,开发者实现它可以在对象初始化前后做一些操作;再比如Spring Cloud框架里的PropertySourceLocator
接口,开发者实现它可以做服务配置的外部加载;MyBatis同样留了拓展点,Mybatis留的拓展点我们通常称为Mybatis的插件机制,其实从本质上来说它就是一个拦截器,是JDK动态代理和责任链设计模式的结合而出的产物。
前面也说到了MyBatis插件本质上是一个拦截器,那么它能拦截哪些类和哪些方法呢?MyBatis中针对四大组件提供了扩展机制,这四个组件分别是:
MyBatis中所允许拦截的类和方法如下:
- Executor【SQL 执行器】【update,query,commit,rollback】
- StatementHandler【SQL 语法构建器对象】【prepare,parameterize,batch,update,query等】
- ParameterHandler【参数处理器】【getParameterObject,setParameters等】
- ResultSetHandler【结果集处理器】【handleResultSets,handleOuputParameters等】
3.2 插件的实现步骤
实现一个MyBatis插件主要分为以下几个步骤:
- 实现
Interceptor
接口 - 使用
@Intercepts
和@Signature
注解定义拦截点 - 在Mybatis的全局配置文件中注册插件
补充说明:
由于MyBatis插件是可以对 MyBatis中四大组件对象的方法进行拦截,那拦截器拦截哪个类的哪个方法如何知道,@Intercepts
注解用来标识一个类为MyBatis插件,并指定该插件要拦截的方法。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
Signature[] value();
}
@Signature
注解用来指定要拦截的目标类、目标方法和方法参数。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
// 拦截的类
Class<?> type();
// 拦截的方法
String method();
// 拦截方法的参数
Class<?>[] args();
}
3.2.1 实现Interceptor接口
首先,我们需要实现org.apache.ibatis.plugin.Interceptor
接口:
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import java.util.Properties;
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class ExamplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 在目标方法执行前的逻辑
Object result = invocation.proceed();
// 在目标方法执行后的逻辑
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 设置插件的属性
}
}
在上述代码中,@Intercepts
注解定义了拦截器的拦截点,type
指定了要拦截的对象类型,method
指定了要拦截的方法,args
指定了方法参数类型。intercept
方法是拦截器的核心逻辑,plugin
方法用于创建目标对象的代理,setProperties
方法用于设置插件的属性。
3.2.2 注册插件
在 MyBatis 配置文件(mybatis-config.xml
)中注册插件:
<?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>
<plugins>
<plugin interceptor="com.example.plugin.ExecutionTimePlugin"/>
<property name="someProperty" value="someValue"/>
</plugin>
</plugins>
</configuration>
3.3 自定义插件
3.3.1 实现 SQL 执行时间记录插件
下面是一个实际的插件示例,演示如何使用插件记录 SQL 语句的执行时间。但是其实这个记录并不是特别精准,其中额外包含了 jdbc创建连接和预编译的时间。
3.3.1.1 实现 SQL 执行时间记录代码
PerformanceMonitorPlugin
package com.wasteland.blogsourcecode.mybatisdemo.plugin;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Properties;
/**
* @author wasteland
* @create 2025-04-08
*/
@Intercepts({
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}),
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PerformanceMonitorPlugin implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitorPlugin.class);
private static final String dataFormat = "yyyy-MM-dd HH:mm:ss.SSS";
// 慢查询阈值(毫秒)
private long slowQueryThreshold = 1000;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取执行SQL的相关信息
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
String sqlId = mappedStatement.getId();
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
String sql = boundSql.getSql();
long startTime = System.currentTimeMillis();
try {
// 执行原方法
return invocation.proceed();
} finally {
long costTime = System.currentTimeMillis() - startTime;
// 记录日志
if (costTime > slowQueryThreshold) {
logger.warn("慢SQL执行耗时: {}ms > {}ms, SQL ID: {}, SQL: {}",
costTime, slowQueryThreshold, sqlId, sql);
} else {
logger.debug("SQL执行耗时: {}ms, SQL ID: {}, SQL: {}", costTime, sqlId, sql);
}
// 可以在这里将统计信息存入数据库或监控系统
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可以从配置中读取慢查询阈值
String threshold = properties.getProperty("slowQueryThreshold");
if (threshold != null) {
this.slowQueryThreshold = Long.parseLong(threshold);
}
}
}
3.3.1.2 注册SQL 执行时间记录插件
然后在全局配置文件里注册定义好的拦截器
3.3.1.3 查询时间测试
查询时间如下图
3.3.2 实现查询结果加密插件
实现对查询结果中的电话号码phone进行MD5加密。
3.3.2.1 实现查询结果加密插件代码
a. DigestUtils
加密算法实现
package com.wasteland.blogsourcecode.mybatisdemo.plugin;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* @author wasteland
* @create 2025-04-08
*/
public class DigestUtils {
private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();
/**
* 计算字符串的MD5值
* @param input 输入字符串
* @return 32位小写MD5值
*/
public static String md5(String input) {
return digest(input, "MD5");
}
/**
* 计算字符串的SHA-1值
* @param input 输入字符串
* @return 40位小写SHA-1值
*/
public static String sha1(String input) {
return digest(input, "SHA-1");
}
/**
* 计算字符串的SHA-256值
* @param input 输入字符串
* @return 64位小写SHA-256值
*/
public static String sha256(String input) {
return digest(input, "SHA-256");
}
/**
* 计算字符串的SHA-512值
* @param input 输入字符串
* @return 128位小写SHA-512值
*/
public static String sha512(String input) {
return digest(input, "SHA-512");
}
/**
* 通用摘要计算方法
* @param input 输入字符串
* @param algorithm 算法名称
* @return 摘要字符串
*/
private static String digest(String input, String algorithm) {
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] bytes = md.digest(input.getBytes(StandardCharsets.UTF_8));
return bytesToHex(bytes);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* 字节数组转十六进制字符串
* @param bytes 字节数组
* @return 十六进制字符串
*/
private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int i = 0; i < bytes.length; i++) {
int v = bytes[i] & 0xFF;
hexChars[i * 2] = HEX_CHARS[v >>> 4];
hexChars[i * 2 + 1] = HEX_CHARS[v & 0x0F];
}
return new String(hexChars);
}
/**
* Base64编码
* @param input 输入字符串
* @return Base64编码结果
*/
public static String base64Encode(String input) {
return Base64.getEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8));
}
/**
* Base64解码
* @param input Base64编码字符串
* @return 解码后的原始字符串
*/
public static String base64Decode(String input) {
byte[] decodedBytes = Base64.getDecoder().decode(input);
return new String(decodedBytes, StandardCharsets.UTF_8);
}
/**
* 计算字符串的HMAC-SHA256签名
* @param data 要签名的数据
* @param key 密钥
* @return HMAC-SHA256签名
*/
public static String hmacSha256(String data, String key) {
try {
javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA256");
mac.init(new javax.crypto.spec.SecretKeySpec(
key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] result = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return bytesToHex(result);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
b. EncryptingResultSetHandler
定义EncryptingResultSetHandler对结果集进行加密处理。
package com.wasteland.blogsourcecode.mybatisdemo.plugin;
import com.wasteland.blogsourcecode.mybatisdemo.pojo.User;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import java.sql.CallableStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
/**
* @author wasteland
* @create 2025-04-08
*/
public class EncryptingResultSetHandler implements ResultSetHandler {
private final ResultSetHandler resultSetHandler;
public EncryptingResultSetHandler(ResultSetHandler resultSetHandler) {
this.resultSetHandler = resultSetHandler;
}
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
// 使用委托对象处理结果集
List<Object> result = this.resultSetHandler.handleResultSets(stmt);
// 假设我们有一个User对象,并且知道密码字段名为"password"
// 对密码进行“加密”操作(这里只是示例,实际应该是解密)
if (result instanceof List) {
List<?> resultList = (List<?>) result;
for (Object item : resultList) {
if (item instanceof User) {
User user = (User) item;
String encryptedPassword = encryptPassword(user.getPhone());
user.setPhone(encryptedPassword);
}
}
}
return result;
}
private String encryptPassword(String password) {
// 这里应该是你的加密逻辑,为了演示,我们使用一个简单的替换逻辑
return DigestUtils.md5(password);
}
@Override
public <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException {
return null;
}
@Override
public void handleOutputParameters(CallableStatement cs) throws SQLException {
}
// 其他方法...
}
c. ResultSetHandlerHandleResultSetsPlugin
最后定义拦截器,对ResultSetHandler#handleResultSets进行拦截。
package com.wasteland.blogsourcecode.mybatisdemo.plugin;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;
import java.sql.Statement;
import java.util.Properties;
/**
* @author wasteland
* @create 2025-04-08
*/
@Component
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
public class ResultSetHandlerHandleResultSetsPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Statement stmt = (Statement) invocation.getArgs()[0];
// 创建自定义的 EncryptingResultSetHandler
EncryptingResultSetHandler customResultSetHandler = new EncryptingResultSetHandler((ResultSetHandler) invocation.getTarget());
// Object result = invocation.proceed();
// 使用自定义的 EncryptingResultSetHandler 重新处理结果集
return customResultSetHandler.handleResultSets(stmt);
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可以为插件配置属性
}
}
3.3.2.2 注册查询结果加密插件
然后在全局配置文件里注册定义好的拦截器。
3.3.2.3 加密结果测试
最后加密效果如下图
3.4 插件机制源码分析
在了解了插件机制原理和如何实现自定义插件后,我们这时候去深入到源码去分析,在分析过程中带着3个问题看:对象是如何实例化的? 插件的实例对象如何添加到拦截器链中的? 组件对象的代理对象是如何产生的?
3.4.1 插件配置信息加载与解析
我们定义好了一个拦截器,那我们怎么告诉MyBatis呢?我们会把它注册在全局配置文件中。
对应的解析代码发生在XMLConfigBuilder#pluginsElement
里
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 获取拦截器
String interceptor = child.getStringAttribute("interceptor");
// 获取配置的Properties属性
Properties properties = child.getChildrenAsProperties();
// 根据配置文件中配置的插件类的全限定名 进行反射初始化
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
// 将属性添加到Intercepetor对象
interceptorInstance.setProperties(properties);
// 添加到配置类的InterceptorChain属性,InterceptorChain类维护了一个List<Interceptor>
configuration.addInterceptor(interceptorInstance);
}
}
}
主要做了以下工作:
- 遍历解析 plugins 标签下每个 plugin 标签
- 根据解析的类信息创建 Interceptor 对象
- 调用 setProperties 方法设置属性
- 将拦截器添加到
Configuration 类的 InterceptorChain
拦截器链中
对应的时序图如下:
3.4.2 代理对象的生成
前文也说过,插件机制可以MyBatis中四大组件进行方法拦截,接下来来看具体如何方法拦截生成了代理对象。
Executor 代理对象(Configuration#newExecutor
)
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 生成Executor代理对象逻辑
return (Executor) interceptorChain.pluginAll(executor);
}
ParameterHandler 代理对象(Configuration#newParameterHandler
)
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject,
BoundSql boundSql) {
// 创建ParameterHandler
// 生成ParameterHandler代理对象逻辑
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,
parameterObject, boundSql);
return (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
}
ResultSetHandler 代理对象(Configuration#newResultSetHandler
)
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds,
ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler,
resultHandler, boundSql, rowBounds);
// 生成ResultSetHandler代理对象逻辑
return (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
}
StatementHandler 代理对象(Configuration#newStatementHandler
)
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// => 创建路由功能的StatementHandler,根据MappedStatement中的StatementType创建对应的 StatementHandler
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject,
rowBounds, resultHandler, boundSql);
return (StatementHandler) interceptorChain.pluginAll(statementHandler);
}
通过查看源码会发现,所有代理对象的生成都是通过InterceptorChain#pluginAll
方法来创建的,InterceptorChain#pluginAll
内部通过遍历 Interceptor#plugin
方法来创建代理对象,并将生成的代理对象又赋值给 target,如果存在多个拦截器的话,生成的代理对象会被另一个代理对象所代理,从而形成一个代理链,执行的时候,依次执行所有拦截器的拦截逻辑代码,再跟进去。
// org.apache.ibatis.plugin.InterceptorChain
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
// org.apache.ibatis.plugin.Interceptor
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public static Object wrap(Object target, Interceptor interceptor) {
// 1.解析该拦截器所拦截的所有接口及对应拦截接口的方法
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
// 2.获取目标对象实现的所有被拦截的接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 3.目标对象有实现被拦截的接口,生成代理对象并返回
if (interfaces.length > 0) {
// 通过JDK动态代理的方式实现
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
// 目标对象没有实现被拦截的接口,直接返回原对象
return target;
}
对应的时序图如下:
3.4.3 拦截逻辑的执行
MyBatis 框架中执行Executor、ParameterHandler、ResultSetHandler和StatementHandler中的方法时真正执行的是代理对象对应的方法,所以执行方法实际是调用InvocationHandler#invoke
方法(Plugin类实现InvocationHandler接口),下面是Plugin#invoke
方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
注意:
同一个组件对象的同一个方法是可以被多个拦截器进行拦截的,配置在最前面的拦截器最先被代理,但是执行的时候却是最外层的先执行,即包裹顺序和执行顺序相反
。
3.5 插件机制的应用场景与注意事项
应用场景
- SQL 日志记录:记录 SQL 语句及其执行时间,方便调试和优化。
- 参数验证和修改:在 SQL 执行前对参数进行验证和修改,确保数据的正确性和安全性。
- 查询结果处理:对查询结果进行处理,如数据脱敏、格式转换等。
- 性能监控:监控 SQL 执行时间、执行次数等,帮助优化系统性能。
注意事项
- 插件的实现要尽量简洁高效,避免增加额外的性能开销。
- 插件的配置要合理,避免过度使用插件导致代码复杂度增加。
- 插件的执行顺序是根据配置文件中的顺序决定的,可以根据需要调整插件的执行顺序。
四、总结
本文介绍了SpringBoot项目结合MyBatis的快速构建方式,然后介绍了MyBatis 插件的实现步骤、插件机制的原理以及插件机制实战。MyBatis 插件机制提供了一种灵活的方式,允许开发者在 SQL 执行的各个阶段插入自定义逻辑,极大地增强了 MyBatis 的扩展能力。本文仅介绍了插件机制的源码,在后面的文章中,会详细介绍MyBatis的核心源码,分析其核心组件的作用以及组件的执行时机。