一、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:声明拦截目标为
Executor的update方法,参数类型为MappedStatement和Object。 - 作用时机:所有数据变更操作(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的自动填充。开发者需重点关注:
- 拦截器执行点的合理选择
- 反射操作的性能与安全
- 分布式服务的容错设计