MyBatis拦截器解析:基于注解的Leaf分布式ID自动填充

295 阅读3分钟

一、MyBatis拦截器机制简介

MyBatis拦截器(Interceptor)是其核心扩展机制之一,允许开发者在SQL执行的不同阶段插入自定义逻辑。通过实现org.apache.ibatis.plugin.Interceptor接口,可拦截以下对象的方法:

  • Executor(执行SQL的核心对象)
  • StatementHandler(处理JDBC Statement)
  • ParameterHandler(处理参数)
  • ResultSetHandler(处理结果集)

本文重点解析如何通过拦截Executor#update方法,结合自定义注解实现分布式ID的自动填充。


二、拦截器核心代码分析

以下为基于GeneratedKeyInterceptor的逐层解析:

1. 拦截器定义与切入点
@Component
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class,Object.class})})
public class GeneratedKeyInterceptor implements Interceptor {
    // ...
}
  • @Intercepts:声明拦截目标为Executorupdate方法,参数类型为MappedStatementObject
  • 作用时机:所有数据变更操作(INSERT/UPDATE/DELETE)均会触发此拦截器。
2. 拦截逻辑(intercept方法)
public Object intercept(Invocation invocation) throws Throwable {
    MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
    SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();

    // 仅处理INSERT操作
    if (SqlCommandType.INSERT != sqlCommandType) {
        return invocation.proceed();
    }

    Object parameter = invocation.getArgs()[1];
    Object dbObject = findDbObject(parameter);

    // 处理单个或批量插入
    if (mappedStatement.getId().contains(INSERT) || mappedStatement.getId().contains(SAVE)) {
        generatedKey(dbObject);
    } else if (mappedStatement.getId().contains(BATCH_INSERT) || mappedStatement.getId().contains(BATCH_SAVE)) {
        // 批量处理逻辑
    }

    return invocation.proceed();
}
  • SQL类型过滤:仅处理INSERT操作,其他操作直接放行。
  • 参数提取:通过findDbObject方法从参数中提取实体对象。
3. 实体对象提取(findDbObject方法)
protected BaseModel findDbObject(Object parameterObj) {
    if (parameterObj instanceof BaseModel) {
        return (BaseModel) parameterObj;
    } else if (parameterObj instanceof Map) {
        // 从Map参数中提取实体
        for (Object val : ((Map<?, ?>) parameterObj).values()) {
            if (val instanceof BaseModel) {
                return (BaseModel) val;
            }
        }
    }
    return null;
}
  • 支持场景:直接传递实体对象或以Map封装(常见于MyBatis批量操作)。
4. 分布式ID生成(generatedKey方法)
private void generatedKey(Object parameter) throws Throwable {
    Field[] fields = parameter.getClass().getDeclaredFields();
    for (Field field : fields) {
        if (field.getType() != Long.class) continue;

        DistributedId annotation = field.getAnnotation(DistributedId.class);
        if (annotation == null) continue;

        field.setAccessible(true);
        if (field.get(parameter) != null) continue;

        // 调用Leaf服务获取ID
        ServerResponseEntity<Long> response = segmentFeignClient.getSegmentId(annotation.value());
        if (response.isSuccess()) {
            field.set(parameter, response.getData());
        } else {
            throw new Mall4cloudException("获取分布式ID失败");
        }
    }
}
  • 反射机制:动态修改被@DistributedId注解标记的Long类型字段。
  • Leaf集成:通过Feign客户端调用Leaf服务,根据biz_tag获取唯一ID。

三、自定义注解@DistributedId的实现

1. 注解定义
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedId {
    String value(); // 对应Leaf的biz_tag
}
  • @Target:限定注解作用于字段。
  • @Retention:确保注解在运行时可通过反射读取。
2. 注解使用示例
public class Order extends BaseModel {
    @DistributedId("order_id")
    private Long orderId;
    // 其他字段...
}
  • 业务标识value值对应Leaf的leaf_alloc表中的biz_tag,确保各业务ID独立。

四、关键技术与难点

1. 批量插入处理
  • 参数结构识别:批量操作通常以Map<String, Object>形式传入,其中list键对应实体列表。
  • 循环注入:遍历列表为每个实体生成独立ID。
2. 反射性能优化
  • 字段缓存:可预先缓存带有注解的字段,避免每次反射遍历。
  • 安全控制:确保field.setAccessible(true)仅在必要时使用。
3. 异常处理
  • Feign调用降级:需考虑Leaf服务不可用时的熔断策略。
  • ID重复风险:确保即便在重试场景下,不会因拦截器重复执行导致ID重复。

五、应用场景与扩展

1. 适用场景
  • 分布式系统下的主键生成。
  • 需要与数据库自增ID解耦的业务。
2. 扩展方向
  • 支持多种ID生成器:如Snowflake算法、Redis自增等。
  • 复合主键处理:通过注解组合多个字段生成唯一标识。

六、总结

通过自定义MyBatis拦截器与注解的结合,可无侵入式地实现分布式ID的自动填充。开发者需重点关注:

  • 拦截器执行点的合理选择
  • 反射操作的性能与安全
  • 分布式服务的容错设计