手撕「字段自动填充」的2种方案

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

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:表示拦截的接口的class
  • method:表示拦截接口的某个方法
  • args:方法的参数

这几个参数都可以通过org.apache.ibatis.plugin.Invocation获取到。

2.2 定制自动填充的拦截器

自动填充,我们只需要在SQL执行之前,填充好数据即可。所以这里我们拦截的是org.apache.ibatis.executor.Executorupdate方法。

为了演示方便,这里只针对实体的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,这个是通过打断点看到的结果,使用者可以自行调整。

在测试过程中发现:param1userInfo是一样的值,修改两个任意一个都会被赋值。所以案例选择了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 小结

无论使用第三方还是自己手撕的代码,最终的结果都是为了解决重复的代码问题。小编以为第三方的自动填充具有一定的局限性,而自定义的更具有扩展性,可能更适合代码不怎么规范的公司,还可以扩展功能,大家更喜欢哪一种呢?

相关推荐
程序员良辰1 小时前
Spring与SpringBoot:从手动挡到自动挡的Java开发进化论
java·spring boot·spring
鹦鹉0071 小时前
SpringAOP实现
java·服务器·前端·spring
练习时长两年半的程序员小胡2 小时前
JVM 性能调优实战:让系统性能 “飞” 起来的核心策略
java·jvm·性能调优·jvm调优
崎岖Qiu2 小时前
【JVM篇11】:分代回收与GC回收范围的分类详解
java·jvm·后端·面试
redreamSo4 小时前
承认吧,我们都活在看脸的世界里
程序员
27669582924 小时前
东方航空 m端 wasm req res分析
java·python·node·wasm·东方航空·东航·东方航空m端
许苑向上4 小时前
Spring Boot 自动装配底层源码实现详解
java·spring boot·后端
喵叔哟4 小时前
31.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--财务服务--收支分类
java·微服务·.net
codu4u13145 小时前
Maven中的bom和父依赖
java·linux·maven
呦呦鹿鸣Rzh5 小时前
微服务快速入门
java·微服务·架构