关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言
上一期,我们介绍了Mybatis-Plus
的自动填充功能,用起来简直不要太爽。我们能不能去自己实现呢?
这一期,我们将介绍两种方案实现字段的自动填充:
- 基于
Mybatis
的拦截器 - 基于
AOP
的自定义注解
02 基于Mybaits
的拦截器
我们先看看官方给的解释:

主要拦截的接口:
org.apache.ibatis.executor.Executor
:SQL执行org.apache.ibatis.executor.parameter.ParameterHandler
:参数处理org.apache.ibatis.executor.resultset.ResultSetHandler
:结果集org.apache.ibatis.executor.statement.StatementHandler
:声明
截图中括号里面表示每一个接口的方法。
2.1 拦截器的使用
主要的注解包括:@org.apache.ibatis.plugin.Intercepts
、@org.apache.ibatis.plugin.Signature
官方案例:
java
// ExamplePlugin.java
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// implement pre processing if need
Object returnObject = invocation.proceed();
// implement post processing if need
return returnObject;
}
}
可以看出:一个@Intercepts
注解可以控制多个@Signature
type
:表示拦截的接口的classmethod
:表示拦截接口的某个方法args
:方法的参数
这几个参数都可以通过org.apache.ibatis.plugin.Invocation
获取到。

2.2 定制自动填充的拦截器
自动填充,我们只需要在SQL
执行之前,填充好数据即可。所以这里我们拦截的是org.apache.ibatis.executor.Executor
的update
方法。
为了演示方便,这里只针对实体的updateTime
字段处理,使用了硬编码。要想按照配置或者随意指定,可以配置自定义注解打标签,这里然后统一处理即可。
java
@Intercepts({@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class,Object.class}
)})
@Component
public class AutoFillInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Map<String, Object> entityMap = (Map<String, Object>)args[1];
Object entity = entityMap.get("param1");
// 获取SQL操作类型
String sqlCommandType = ms.getSqlCommandType().name();
// UPDATE操作填充
if ("UPDATE".equals(sqlCommandType)) {
Class<?> clazz = entity.getClass();
Field updateTimeField = ReflectionUtils.findField(clazz, "updateTime", LocalDateTime.class);
updateTimeField.setAccessible(true);
updateTimeField.set(entity, LocalDateTime.now());
}
return invocation.proceed();
}
}
拦截器的核心就是拿到数据库实体,然后通过反射技术实现参数的赋值。案例中直接使用了Spring
提供的ReflectionUtils
工具类直接事半功倍。
拦截器中只处理了UPDATE
的命令类型,使用者可以根据情况追加。
参数的获取,是根据方法的定义的顺序依次获取的。这里要说明的是:Object.class
对应的结果是一个Map,这个是通过打断点看到的结果,使用者可以自行调整。

在测试过程中发现:param1
和userInfo
是一样的值,修改两个任意一个都会被赋值。所以案例选择了parm1
这个没有特殊含义的字段。
2.3 测试以及结果
客户端:
java
@Test
void contextLoads04() {
UserInfo userInfo = new UserInfo();
userInfo.setId(61);
userInfo.setName("test");
userInfoNativeMapper.updateByKey(userInfo);
}
服务:
java
public interface UserInfoNativeMapper {
@Update("""
<script>
update user_info
<set>
name=#{userInfo.name},
<if test="userInfo.updateTime != null">
update_time=#{userInfo.updateTime}
</if>
</set>
where id=#{userInfo.id}
</script>
""")
void updateByKey(@Param("userInfo") UserInfo userInfo);
}
这里使用了注解的方式,如果updateTime
不为空,才会更新。
结果:

从结果看到,update_time
字段已经被自动填充了。
03 基于AOP
的自定义注解
自定义注解要实现的目标:
- 标记需要填充的字段
- 标记要拦截的方法(切点)
3.1 自定义注解
java
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
/**
* 注解类型
*/
FillTypeEnum type() default FillTypeEnum.FIELD;
/**
* 值
*/
String value() default "";
/**
* 方法类的Class
*/
Class<?> clazz() default Void.class;
/**
* 方法名
*/
String methodName() default "";
}
/** 注解的类型 */
public enum FillTypeEnum {
FIELD, METHOD
}
标记的位置通过FillTypeEnum
指定:
FillTypeEnum.FIELD
标记字段,默认FillTypeEnum.METHOD
标记方法
@AutoFill
注解通过value
指定填充的值,如果为空,则通过方法的class
和对应的方法,最后调用填充值。
3.2 定义切面
Maven
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
切面
java
@Aspect
@Component
public class AutoFillAspect {
@Around("@annotation(autoFill)")
public Object around(ProceedingJoinPoint joinPoint, AutoFill autoFill) throws Throwable {
FillTypeEnum type = autoFill.type();
if (type == FillTypeEnum.METHOD) {
// 拦截方法上的注解
Object entity = joinPoint.getArgs()[0];
Class<?> clazz = entity.getClass();
// 获取所有的字段
Field[] declaredFields = clazz.getDeclaredFields();
int length = declaredFields.length;
if (length > 0) {
for (int i = 0; i < length; i++) {
Field field = declaredFields[i];
AutoFill annotation = field.getAnnotation(AutoFill.class);
if (annotation != null && annotation.type() == FillTypeEnum.FIELD) {
field.setAccessible(true);
String value = annotation.value();
if (StringUtils.hasText(value)) {
Class<?> fieldClazz = field.getType();
if (fieldClazz == LocalDateTime.class) {
field.set(entity, LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}else if (fieldClazz == Integer.class) {
field.set(entity, Integer.valueOf(value));
}else {
field.set(entity, value);
}
continue;
}
Class<?> methodClazz = annotation.clazz();
String s = annotation.methodName();
Method method = methodClazz.getDeclaredMethod(s, null);
Object invoke = method.invoke(methodClazz, null);
field.set(entity, invoke);
}
}
}
}
return joinPoint.proceed();
}
}
核心就是获取方法的参数,解析参数的需要填充的字段,然后通过value
或者调用方法,自动赋值填充。
3.3 注解使用
字段标记:

方法标记:

3.4 测试以及结果
客户端和拦截器的相同,我们直接看结果:

从结果来看,达到了同样的效果。
通过注解标记方法有代码侵入性,可以使用切点的表达式,指定某一类方法即可。
04 小结
无论使用第三方还是自己手撕的代码,最终的结果都是为了解决重复的代码问题。小编以为第三方的自动填充具有一定的局限性,而自定义的更具有扩展性,可能更适合代码不怎么规范的公司,还可以扩展功能,大家更喜欢哪一种呢?