记录用户操作字段变更实现方法

1,825 阅读5分钟

前言

之前技术群里有一个大佬提出一个需求:记录用户的操作。一个看似简单又复杂的需求,我开始头脑风暴:

  1. 前端对比记录
  2. 后端进行对比

  其中少不了对比的过程。这时群里那个大佬提出个更有建设性的想法,通配以后的这种需求,而不是每次都去复制一遍之前的逻辑,也就是重复工作。他认为跟业务没有关联的,都可以进行抽象,供以后使用。

  这个是很有意义的idea,值得我们平时工作中去学习。从而提高我们日常开发。就像之前看到一位大佬总结:28原则,把20%的精力处理crud,80%时间处理以后对团队,对整个技术有影响的东西,这个才是更有价值的。

  下面这个是参考肥朝哥的一个想法,使用自定义注解,在需要对比的字段上加上注解,查询的时候写到redis。编辑的时候,将缓存key传过来,跟之前的缓存对比,对比后的差异存数据库,删缓存~

show the code

  我写的代码都不咋滴,其实,西西。看看就好~

自定义注解

注解在方法上,因为我要使用aop。

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)  // 保留到运行时,可通过注解获取
@Documented
public @interface MyMethod {

    Class toClass() ;

}

注解到字段上

@Target(ElementType.FIELD)  //  注解用于字段上
@Retention(RetentionPolicy.RUNTIME)  // 保留到运行时,可通过注解获取
public @interface MyField {

}

切面逻辑

import com.alibaba.fastjson.JSON;
import com.example.demo.annotation.MyField;
import com.example.demo.annotation.MyMethod;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * @author M
 */
@Aspect
@Component
public class TestAspect {

    @Autowired
    private StringRedisTemplate template;

    private static ThreadLocal<String> cacheKey = new ThreadLocal<>();

    @Pointcut("@annotation(com.example.demo.annotation.MyMethod)")

    public void annotationPoinCut() {

    }

    @Before(value = "annotationPoinCut()")
    public void beforeTest(JoinPoint point) throws IllegalAccessException {
        //获取方法签名
        MethodSignature signature = (MethodSignature) point.getSignature();
        //获取切入方法的对象
        Method method = signature.getMethod();
        //获取方法上的Aop注解
        MyMethod myMethod = method.getAnnotation(MyMethod.class);
        String args = JSON.toJSONString(point.getArgs());
        if (args.length() > 1) {
            Object dto = JSON.parseObject(args.substring(1, args.length() - 1), myMethod.toClass());
            xx(dto, myMethod.toClass());
        }


    }

    private void xx(Object dto, Class class1) throws IllegalAccessException {
        // 获取所有字段
        Object lastDto = null;
        for (Field f : class1.getDeclaredFields()) {
            f.setAccessible(true);
            //System.out.println("f:" + f.getName());
            // 判断这个字段是否有MyField注解
            if ("key".equals(f.getName()) && !StringUtils.isEmpty(f.get(dto))) {
                lastDto = JSON.parseObject(template.opsForValue().get(f.get(dto)), class1);
                cacheKey.set(String.valueOf(f.get(dto)));
            }
            System.out.println(lastDto);
            if (f.isAnnotationPresent(MyField.class) && lastDto != null) {
                Object value = f.get(dto);
                Object value1 = f.get(lastDto);
                if (value != value1) {
                    System.out.println("存在不同,之前的值为:" + value1 + ",后面的值:" + value);
                }
            }
        }
    }

    @After(value = "annotationPoinCut()")
    public void afterTest(JoinPoint point) {
        System.out.println(cacheKey.get());
        template.delete(cacheKey.get());
        cacheKey.remove();
    }

}

讲解

  先切到方法上的MyMethod,里面toClass是为后面实例化对象用的,然后获取请求参数,回传给对象里面的值。根据类遍历属性Field,再拿到属性上的有相关的注解@MyField,获取属性的值,进行对比。期间使用ThreadLocal保持redis 的缓存之前的数据,处理之后再删除缓存。

遍历属性

public static void main(String[] args) {
        // 获取类模板
        Class c = DajituiDTO.class;
        // 获取所有字段
        for (Field f : c.getDeclaredFields()) {
            // 判断这个字段是否有MyField注解
            if (f.isAnnotationPresent(MyField.class)) {
                MyField annotation = f.getAnnotation(MyField.class);
                System.out.println("字段:[" + f.getName() + "]");
            }
        }
    }

dto

@AllArgsConstructor
@Data
@NoArgsConstructor
public class DajituiDTO {

    private String key;

    @MyField
    private String name;

    private Integer age;

    public static void main(String[] args) {
        // 获取类模板
        Class c = DajituiDTO.class;
        // 获取所有字段
        for (Field f : c.getDeclaredFields()) {
            // 判断这个字段是否有MyField注解
            if (f.isAnnotationPresent(MyField.class)) {
                MyField annotation = f.getAnnotation(MyField.class);
                System.out.println("字段:[" + f.getName() + "]");
            }
        }
    }
}

controller

import com.alibaba.fastjson.JSON;
import com.example.demo.annotation.MyMethod;
import com.example.demo.dto.DajituiDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

/**
 * @author M
 */
@RestController
public class TestController {

    @Autowired
    private StringRedisTemplate template;

    @GetMapping("search")
    public String getSomeThing() {
        DajituiDTO dto = new DajituiDTO("大鸡腿", null, 18);
        String key = getTheKey("search");
        dto.setKey(key);
        template.opsForValue().set(key, JSON.toJSONString(dto), 10, TimeUnit.MINUTES);
        return dto.toString();
    }

    @GetMapping("edit")
    //@MyMethod(toClass = DajituiDTO.class)
    public String editSomeThing(DajituiDTO dto) {
        //修改并显示有注解字段的改变情况
        return dto.toString();
    }

    @PostMapping("edit1")
    @MyMethod(toClass = DajituiDTO.class)
    public String editSomeThingPost(@RequestBody DajituiDTO dto) {
        //修改并显示有注解字段的改变情况
        return dto.toString();
    }

    private String getTheKey(String requestUrl) {
        return System.currentTimeMillis() + requestUrl + ThreadLocalRandom.current().nextInt();
    }


}

请求链接

http://localhost:8080/search 保存信息 http://localhost:8080/edit?key=1579857090663search397251701&name=123 这个key的话需要第一个链接回传的key作为参数

或者post请求http://localhost:8080/edit1

{
    "key": "1579858080821search1554559961",
    "name": "123"
}

输出

DajituiDTO(key=1579858080821search1554559961, name=null, age=18) DajituiDTO(key=1579858080821search1554559961, name=null, age=18) 存在不同,之前的值为:null,后面的值:123 DajituiDTO(key=1579858080821search1554559961, name=null, age=18) 1579858080821search1554559961

可以看到之前name没有值,后面修改成123.

改进地方

缓存key的生成

private String getTheKey(String requestUrl) {
        return System.currentTimeMillis() + requestUrl + ThreadLocalRandom.current().nextInt();
    }

  为啥这里使用时间戳?因为当时大家伙在讨论并发操作的时候如何对比,这里有点像一个东西的版本。比如一个iphone 1代,我们要对比就得对比它的1代,如果我们对比其他iphone代,那么久没有对比的意义了。   其次考虑到可能真的什么时间什么的都刚刚好,可以再加个用户名称什么的顶上去。

切面逻辑

  在哪一块逻辑上有些需要判空,以及类型转换这样,还需要改进~

github

大鸡腿的github

最后

  这是本鸡腿在除夕写的一篇博客,有时不足,各位大佬多多指点。其次无论何时何地,只要有空就要不断努力学习,欧力给~