🪁🍁 希望本文能给您带来帮助,如果有任何问题,欢迎批评指正!🐅🐾🍁🐥
文章目录
- 一、背景
- 二、Spring Boot项目中结合MyBatis
- 2.1 数据准备
- 2.2 pom.xml依赖增加
- 2.3 application.yml实现
- 2.4 代码层实现
- 2.4.1 基于注解的Mapper
- 2.4.2 基于XML配置的Mapper
- 三、MyBatis插件机制
- 3.1 插件概述
- 3.2 插件的实现步骤
- 3.2.1 实现Interceptor接口
- 3.2.2 注册插件
- 3.3 自定义插件
- 3.3.1 实现 SQL 执行时间记录插件
- 3.3.1.1 实现 SQL 执行时间记录代码
- 3.3.1.2 注册SQL 执行时间记录插件
- 3.3.1.3 查询时间测试
- 3.3.2 实现查询结果加密插件
- 3.3.2.1 实现查询结果加密插件代码
- 3.3.2.2 注册查询结果加密插件
- 3.3.2.3 加密结果测试
- 3.4 插件机制源码分析
- 3.4.1 插件配置信息加载与解析
- 3.4.2 代理对象的生成
- 3.4.3 拦截逻辑的执行
- 3.5 插件机制的应用场景与注意事项
- 四、总结
一、背景
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.Driverurl: jdbc:mysql://localhost:3306/mybatis_demo?useSSL=falseusername: adminpassword: 123456
server:port: 8082mybatis:# 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 {@Autowiredprivate UserMapper userMapper;@Overridepublic User findById(Integer id) {return userMapper.findById(id);}@Overridepublic List<User> findAll() {return userMapper.findAll();}@Overridepublic String DelteById(Integer id) {userMapper.DelteById(id);return "删除成功";}@Overridepublic String AddUser(User user) {userMapper.AddUser(user);return "添加成功";}@Overridepublic 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 {@Autowiredprivate 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 mapperPUBLIC "-//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 configurationPUBLIC "-//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 {@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 在目标方法执行前的逻辑Object result = invocation.proceed();// 在目标方法执行后的逻辑return result;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic 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;@Overridepublic 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);}// 可以在这里将统计信息存入数据库或监控系统}}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic 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;}@Overridepublic 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);}@Overridepublic <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException {return null;}@Overridepublic 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 {@Overridepublic Object intercept(Invocation invocation) throws Throwable {Statement stmt = (Statement) invocation.getArgs()[0];// 创建自定义的 EncryptingResultSetHandlerEncryptingResultSetHandler customResultSetHandler = new EncryptingResultSetHandler((ResultSetHandler) invocation.getTarget());
// Object result = invocation.proceed();// 使用自定义的 EncryptingResultSetHandler 重新处理结果集return customResultSetHandler.handleResultSets(stmt);}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic 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创建对应的 StatementHandlerStatementHandler 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的核心源码,分析其核心组件的作用以及组件的执行时机。