Mybatis Interceptor 参数 Invocation 解析:公共字段填充设计思路与阿里规约

505 阅读6分钟

Mybatis Interceptor 参数 Invocation 详解:公共字段填充设计思路与阿里规约

Mybatis 是 Java 开发中非常流行的持久层框架,它的拦截器(Interceptor)机制特别强大,可以让我们在 SQL 执行的各个环节“插手”。今天我们要深入聊聊拦截器里的核心参数 Invocation,通过它实现“公共字段填充”(比如自动填入创建时间、更新时间),并且结合阿里开发规约看看怎么写出更规范的代码。

为了让内容通俗易懂,我会从基础开始,逐步展开上下文,尤其是详细解析 Invocation 里有哪些东西,object 是什么,以及如何处理不同类型的参数(比如实体对象和 Map)。准备好了吗?咱们一步步来!


一、Mybatis Interceptor 的基础知识

1. 拦截器是什么?

想象一下,Mybatis 是个火车站,SQL 是火车,拦截器就是安检员。火车要出发(SQL 执行)时,安检员可以检查行李(参数)、改车票(SQL 语句),甚至拦下火车(阻止执行)。Mybatis 拦截器就是干这个活儿的,它可以在以下四个地方“插手”:

  • Executor:执行器,负责执行 SQL(比如增删改查)。
  • ParameterHandler:参数处理器,设置 SQL 的参数。
  • ResultSetHandler:结果集处理器,把查询结果转成 Java 对象。
  • StatementHandler:语句处理器,生成和执行 SQL。

拦截器的核心接口是 org.apache.ibatis.plugin.Interceptor,有三个方法:

  • intercept(Invocation invocation):拦截逻辑写在这儿。
  • plugin(Object target):决定拦截哪些对象。
  • setProperties(Properties properties):接收外部配置。

今天的主角是 intercept 方法里的 Invocation 参数,它就像安检员的“工具箱”,我们得搞清楚里面装了啥,才能干活儿。


二、Invocation 是什么?里面有哪些东西?

Invocation 是 Mybatis 拦截器里的一个关键对象,定义在 org.apache.ibatis.plugin.Invocation 类中。它就像一个“遥控器”,让我们能操控被拦截的方法。它的结构很简单,但功能强大,包含三个核心字段:

1. Invocation 的三个字段

  • target:被拦截的对象。比如我们拦截 Executor,那 target 就是 Executor 的实例。
  • method:被拦截的方法。比如 Executorupdate 方法或 query 方法。
  • args:方法的参数数组。每个参数是个 Object,具体是什么取决于拦截的方法。

可以用代码看看:

public class Invocation {
    private final Object target;
    private final Method method;
    private final Object[] args;

    public Invocation(Object target, Method method, Object[] args) {
        this.target = target;
        this.method = method;
        this.args = args;
    }

    public Object getTarget() { return target; }
    public Method getMethod() { return method; }
    public Object[] getArgs() { return args; }

    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(target, args); // 执行原始方法
    }
}

2. Invocation 的作用

  • 获取上下文:通过 targetmethodargs,你知道“谁在干啥,用啥干”。
  • 修改参数:可以直接操作 args 里的内容。
  • 控制执行:调用 proceed() 放行,不调用就拦截。

比如我们拦截 Executor.updateInvocation 就包含:

  • target:某个 Executor 实现类(比如 SimpleExecutor)。
  • methodupdate 方法。
  • argsupdate 方法的参数。

3. Invocation 的 args 里有什么?

args 是个数组,里面装的是被拦截方法的参数。不同的方法,参数不同。我们今天关注 Executor.update,它的签名是:

int update(MappedStatement ms, Object parameter) throws SQLException;
  • args[0]MappedStatement,描述 SQL 的元信息(比如 SQL 类型、语句内容)。
  • args[1]Object parameter,这就是 SQL 的入参对象,也就是我们常说的 object

三、object 是什么东西?

object 是我们从 invocation.getArgs()[1] 拿到的东西,也就是 Executor.update 的第二个参数 parameter。它具体是什么,取决于你调用 Mapper 接口时传了啥。让我详细拆解一下。

1. object 的来源

假设你有这样的 Mapper 接口:

@Mapper
public interface UserMapper {
    @Insert("INSERT INTO user (name) VALUES (#{name})")
    int insertUser(User user);

    @Update("UPDATE user SET name = #{name} WHERE id = #{id}")
    int updateUser(@Param("id") Long id, @Param("name") String name);
}
  • 调用 insertUser(user) 时,Executor.update 被触发,args[1]User 对象。
  • 调用 updateUser(1L, "张三") 时,args[1] 是一个 Map

2. object 的可能类型

情况 1:单个实体对象(Object)

如果 Mapper 方法接收一个参数,比如:

User user = new User();
user.setName("张三");
userMapper.insertUser(user);

这时 object 就是 User 实例,里面存的是:

User{name='张三', createTime=null, updateTime=null}
情况 2:多参数包装成 Map

如果 Mapper 方法有多个参数:

userMapper.updateUser(1L, "张三");

Mybatis 会把参数包装成一个 Mapobject 是个 Map<String, Object>,内容可能是:

{
    "id": 1L,
    "name": "张三"
}

键名来自 @Param 注解,如果没写注解,就是 param1param2 这样默认命名。

情况 3:无参数

如果方法没参数(比如固定的 SQL),objectnull

3. 验证 object 的内容

可以在拦截器里打印:

Object parameter = invocation.getArgs()[1];
System.out.println("parameter: " + parameter);
  • 单参数输出:parameter: User{name='张三', ...}
  • 多参数输出:parameter: {id=1, name=张三}

四、公共字段填充的设计思路

1. 为什么要填充公共字段?

很多表都有“创建时间(create_time)”、“更新时间(update_time)”这样的字段,每次手动填太麻烦。我们可以用拦截器自动填充。

2. 用 Invocation 怎么干?

我们拦截 Executor.update(对应 INSERT 和 UPDATE),用 Invocation 拿到 object,然后塞入公共字段。步骤是:

  1. 判断是 INSERT 还是 UPDATE。
  2. 检查 object 是实体对象还是 Map。
  3. 填充字段(时间、用户等)。
  4. 放行执行。

五、代码实现

假设实体类是:

public class User {
    private Long id;
    private String name;
    private Date createTime;
    private Date updateTime;
    private String createBy;
    private String updateBy;
    // getter 和 setter 省略
}

拦截器代码:

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.util.Date;
import java.util.Map;
import java.util.Properties;

@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class AutoFillInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 拿到 Invocation 的内容
        Object[] args = invocation.getArgs();
        MappedStatement mappedStatement = (MappedStatement) args[0]; // SQL 元信息
        Object parameter = args[1]; // 入参对象(object)

        // 2. 判断是插入还是更新
        String sqlCommandType = mappedStatement.getSqlCommandType().toString();
        if ("INSERT".equals(sqlCommandType) || "UPDATE".equals(sqlCommandType)) {
            String currentUser = "admin"; // 模拟当前用户

            // 3. 处理不同类型的 parameter
            if (parameter != null) {
                if (parameter instanceof Map) {
                    // Map 类型参数
                    Map<String, Object> paramMap = (Map<String, Object>) parameter;
                    if ("INSERT".equals(sqlCommandType)) {
                        paramMap.putIfAbsent("createTime", new Date());
                        paramMap.putIfAbsent("updateTime", new Date());
                        paramMap.putIfAbsent("createBy", currentUser);
                        paramMap.putIfAbsent("updateBy", currentUser);
                    } else if ("UPDATE".equals(sqlCommandType)) {
                        paramMap.putIfAbsent("updateTime", new Date());
                        paramMap.putIfAbsent("updateBy", currentUser);
                    }
                } else {
                    // 实体对象类型(比如 User)
                    MetaObject metaObject = SystemMetaObject.forObject(parameter);
                    if ("INSERT".equals(sqlCommandType)) {
                        setField(metaObject, "createTime", new Date());
                        setField(metaObject, "updateTime", new Date());
                        setField(metaObject, "createBy", currentUser);
                        setField(metaObject, "updateBy", currentUser);
                    } else if ("UPDATE".equals(sqlCommandType)) {
                        setField(metaObject, "updateTime", new Date());
                        setField(metaObject, "updateBy", currentUser);
                    }
                }
            }
        }

        // 4. 放行
        return invocation.proceed();
    }

    // 设置字段值(避免覆盖已有值)
    private void setField(MetaObject metaObject, String fieldName, Object value) {
        try {
            if (metaObject.getValue(fieldName) == null) {
                metaObject.setValue(fieldName, value);
            }
        } catch (Exception e) {
            // 字段不存在就忽略
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 可接收外部配置
    }
}

配置

在 Mybatis 配置中加入:

<plugins>
    <plugin interceptor="com.example.AutoFillInterceptor"/>
</plugins>

代码说明

  • Invocation 的使用:通过 getArgs() 拿到 MappedStatementparameter
  • 区分类型
    • 如果 parameterMap,用 putIfAbsent 添加字段。
    • 如果是实体对象,用 MetaObject 设置字段(比反射更高效)。
  • 填充逻辑:插入时填所有字段,更新时只填更新字段,且只在字段为空时填充。

六、结合阿里规约优化

1. 规约:表必须有 id、create_time、update_time

  • 实现:拦截器已支持自动填充。
  • 优化:可以用注解标记字段,避免硬编码。

2. 规约:避免过多反射

  • 优化:用 MetaObject 替代反射,提高性能。

3. 规约:业务逻辑放应用层

  • 实现:填充逻辑在拦截器,符合要求。

七、总结

通过这次分析,我们搞清楚了:

  • Invocation 是拦截器的“遥控器”,包含 target(谁)、method(干啥)、args(用啥)。
  • objectargs[1],就是 SQL 的入参对象,可能是实体对象或 Map。
  • 公共字段填充 通过区分类型处理 object,实现自动化。

加上阿里规约的约束,代码更规范、高效。希望这篇博客让你对 Invocation 和 Mybatis 拦截器有了更深的理解!有问题欢迎讨论。