MyBatis:SpringBoot结合MyBatis、MyBatis插件机制的原理分析与实战

发布于:2025-04-21 ⋅ 阅读:(42) ⋅ 点赞:(0)

在这里插入图片描述

🪁🍁 希望本文能给您带来帮助,如果有任何问题,欢迎批评指正!🐅🐾🍁🐥



导航参见:

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

  1. findById(Integer id):根据用户ID查询单个用户信息。

  2. findAll():查询所有用户信息并返回用户列表。

  3. DelteById(Integer id):根据用户ID删除用户信息,成功后返回"删除成功"的消息。

  4. AddUser(User user):向数据库中添加新的用户信息,成功后返回"添加成功"的消息。

  5. 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&amp;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插件主要分为以下几个步骤:

  1. 实现Interceptor接口
  2. 使用@Intercepts@Signature注解定义拦截点
  3. 在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);
    }
  }
}

主要做了以下工作:

  1. 遍历解析 plugins 标签下每个 plugin 标签
  2. 根据解析的类信息创建 Interceptor 对象
  3. 调用 setProperties 方法设置属性
  4. 将拦截器添加到 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的核心源码,分析其核心组件的作用以及组件的执行时机。


创作不易,如果有帮助到你的话请给点个赞吧!我是Wasteland,下期文章再见!

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到