你知道Spring AOP+自定注解来记录历史变更吗?

概述

项目开发过程中,有很多需要记录修改日志的轨迹,并且需要知道修改了哪些内容。之前一直都是写一个单独的service,在业务逻辑中强依赖这个service,在业务逻辑中加上这个service,这种方法也是可行,但是总感觉依赖太重。思考良久,觉得可以使用AOP结合注解的方式来进行对业务逻辑的解耦。

业务场景

在金融信贷行业,免不了有信贷产品的概念。比如当新建一个产品后,由于参数或者产品性质的变动,导致需要修改产品,那么上个版本和这个版本到底修改了什么内容呢?需要通过页面展示出来,告诉业务人员产品的修改轨迹。那么需要记录详细的修改过程,并通过接口和前端约定好历史记录的结构报文。

思路

通过注解版AOP,自定义注解,标记到相关的controller层,对controller层的逻辑进行增强,增强的内容就是获取controller层方法从前端传递过来的修改的POJO,既然修改那么前端必然会传递唯一标识,本案例的唯一标识是主键ID,通过主键ID,先查询数据库的记录,通过页面传递过来的update的POJO和数据库里查询出来的entity,进行对比,找到修改过的属性,然后记录到数据库中。后台另外提供一个查询接口,就可以查询出来该笔的所有的历史记录,然后通过对数据的渲染就可以渲染出来历史轨迹。

另外关于注解版aop,可参考公众号的另外一遍文章

实现

表结构设计

CREATE TABLE bus_info_modify_his(
    id bigint(20) NOT NULL   COMMENT '主键id' ,
    business_num varchar(40)    COMMENT '业务编号' ,
    module_name varchar(60)    COMMENT '操作模块名称' ,
    business_type char(1)    COMMENT '业务类型' ,
    operation varchar(40)    COMMENT '操作简单描述' ,
    description text    COMMENT '操作详细描述' ,
    tenant_id varchar(32)    COMMENT '租户id' ,
    revison varchar(4)    COMMENT '乐观锁' ,
    is_deleted tinyint(1)   DEFAULT 0 COMMENT '删除标记' ,
    create_by bigint(20)    COMMENT '创建人' ,
    create_time datetime    COMMENT '创建时间' ,
    update_by bigint(20)    COMMENT '更新人' ,
    update_time datetime    COMMENT '修改时间' ,
    remarks varchar(200)    COMMENT '备注' ,
    PRIMARY KEY (id)
) COMMENT = '业务信息变更历史';

添加切面相关Maven依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.1.7.RELEASE</version>
</dependency>

定义自定义注解

自定义注解用来标记哪些修改操作需要记录到历史日志中去。该注解需要标记到controller层,标记在controller层的目的是能够获取到页面传递过来的值和需要修改的主键ID。

package com.kexun.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Description: 自定义注解用来标记哪些修改操作需要记录到历史日志中去。
 *              该注解需要标记到controller层,标记在controller层的目的是能够获取到页面传递过来的值和需要修改的主键ID
 *
 * @author lgs
 * @date 2023/05/16
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HisModifyLog {

    /**
     * 业务类型
     * @return
     */
    String businessType() default "";

    /**
     * 变更模块名称
     * @return
     */
    String module() default "";

    /**
     * 查询修改之前调用的service的bean
     * @return
     */
    String serviceBean() default "";

    /**
     * 查询修改之前需要调用的方法
     * @return
     */
    String queryMethod() default "";

    /**
     * 查询参数的参数类型 例如:Long,String等等
     * @return
     */
    String paramType() default "";

    /**
     * 从方法参数实体中解析出来的参数作为查询的ID
     * @return
     */
    String paramKey() default "";

    /**
     * 是否为批量类型的操作,主要是从页面上传递过来批量修改的数据
     * @return
     */
    boolean paramIsArray() default false;

}

自定义切面类

注意:该自定义切面类,只实现了部分代码,比如当请求参数为list的时候也需要进行处理,方便页面上批量操作。

package com.kexun.aop;

import com.alibaba.fastjson.JSONObject;
import com.kexun.annotation.HisModifyLog;
import com.kexun.entity.BusInfoModifyHis;
import com.kexun.enums.BusinessTypeEnum;
import com.kexun.service.BusInfoModifyHisService;
import com.kexun.utils.CompareObjectUtils;
import com.kexun.utils.SpringContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * Description:通过切面记录修改的相关历史记录
 *
 * @author lgs
 * @date 2023/05/16
 */
@Configuration
@Aspect
@Slf4j
public class HisModifyInfoAspect {

    @Autowired
    BusInfoModifyHisService busInfoModifyHisService;

    /**
     * 切点表达式,拦截所有注解HisModifyLog标识的方法
     */
    private final String DEFAULT_QUERY_POINT = "@annotation(com.kexun.annotation.HisModifyLog)";

    /**
     * 抽取公共的切点
     */
    @Pointcut(DEFAULT_QUERY_POINT)
    public void pointCut() {
    }
    /**
     * 环绕通知
     */
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) {

        /**
         * 处理思路:
         * 1、通过joinPoint获取到当前controller的实体
         * 2、获取controller传递过来的修改的对象
         * 3、通过注解里设置,获取相应的serviceBean和相应的方法,查询数据库里的信息
         * 4、通过前端传递过来的参数和数据库里查询到的实体,进行一个对比,找到修改了哪些
         * 5、记录到bus_info_modify_his业务信息变更历史记录表中
         */
        // 拦截的实体类,就是当前正在执行的controller
        Object target = joinPoint.getTarget();
        // 拦截的方法名称。当前正在执行的方法
        String methodName = joinPoint.getSignature().getName();
        // 拦截的方法参数
        Object[] args = joinPoint.getArgs();
        //定义请求参数的类对象
        Object newObject = null;
        Map paramMap = new HashMap();
        //循环参数,根据类型进行判断
        for (Object arg : args) {
            //通过该方法可查询对应的object属于什么类型:String type = paramsObj.getClass().getName();
            if (arg instanceof String || arg instanceof JSONObject) {
                String str = (String) arg;
                //TODO
            } else if (arg instanceof Map) {
                //get请求,以map类型传参
                Map<String, Object> map = (Map<String, Object>) arg;
                //TODO
            } else if (arg instanceof Object) {
                try {
                    Map<String, Object> describe = PropertyUtils.describe(arg);
                    paramMap.putAll(describe);
                    newObject = arg;
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                }
            }
        }
        Signature signature = joinPoint.getSignature();
        MethodSignature msig = null;
        if (!(signature instanceof MethodSignature)) {
            throw new IllegalArgumentException("该注解只能用于方法");
        }
        msig = (MethodSignature) signature;
        Class[] parameterTypes = msig.getMethod().getParameterTypes();
        // 获得被拦截的方法
        Method method = null;
        try {
            method = target.getClass().getMethod(methodName, parameterTypes);
        } catch (NoSuchMethodException e1) {
            log.error("ControllerLogAopAspect around error", e1);
        } catch (SecurityException e1) {
            log.error("ControllerLogAopAspect around error", e1);
        }
        if (null != method) {
            // 判断是否包含自定义的注解
            if (method.isAnnotationPresent(HisModifyLog.class) && newObject != null) {
                HisModifyLog hisModifyLog = method.getAnnotation(HisModifyLog.class);
                //获取注解相关属性
                //变更模块名称
                String module = hisModifyLog.module();
                //业务类型
                String businessType = hisModifyLog.businessType();
                //service的bean
                String serviceBean = hisModifyLog.serviceBean();
                //查询方法
                String queryMethod = hisModifyLog.queryMethod();
                //查询参数的参数类型
                String paramType = hisModifyLog.paramType();
                //参数key
                String paramName = hisModifyLog.paramKey();
                //是否为批量操作
                boolean isArray = hisModifyLog.paramIsArray();
                //从请求的参数中解析查询出key对应的value值
                Method mh = ReflectionUtils.findMethod(SpringContextUtil.getBean(serviceBean).getClass(), queryMethod, Long.class);
                //用spring bean获取操作前的参数,此处需要注意:传入的id类型与bean里面的参数类型需要保持一致
                Object id = paramMap.get(paramName);
                Object oldObject = ReflectionUtils.invokeMethod(mh, SpringContextUtil.getBean(serviceBean), id);
                //对新旧的对象进行对比

                String comparResult = CompareObjectUtils.compareTwoObjectForDesc(newObject, oldObject);
                //记录到bus_info_modify_his表中去
                BusInfoModifyHis busInfoModifyHis = new BusInfoModifyHis();
                //业务编号
                busInfoModifyHis.setBusinessNum(String.valueOf(id));
                //业务类型
                busInfoModifyHis.setBusinessType(businessType);
                //操作模块名称
                busInfoModifyHis.setModuleName(module);
                //操作简单描述
                busInfoModifyHis.setOperation(BusinessTypeEnum.findEnumByCode(businessType).name());
                //操作详细描述
                busInfoModifyHis.setDescription(comparResult);
                busInfoModifyHisService.save(busInfoModifyHis);
            }
        }
        Object object = null;
        //执行页面请求模块方法,并返回
        try {
            object = joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return object;
    }
}

对象对比工具

package com.kexun.utils;

import com.kexun.utils.compare.HistoryDesc;
import io.swagger.annotations.ApiModelProperty;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.LinkedList;
import java.util.List;

/**
 * Description:对象比较工具
 *
 * @author lgs
 * @date 2023/05/17
 */
@Slf4j
public class CompareObjectUtils {

    private static final String GETTER_PREFIX = "get";



    /**
     * 对比两个对象同名变量(或者扩展名称)名称,对比内容以字符串呈现
     *
     * @param newObjectWithAnnotation
     *            含有@ApiModelProperty或者@ObjectVariable注解的新对象,对比出现不相同时,需要获取到该变量的中文名称
     * @param oldObject
     *            旧对象
     * @return 对比内容以字符串呈现
     */
    public static String compareTwoObjectForDesc(Object newObjectWithAnnotation, Object oldObject) {
        List<HistoryDesc> list = compareTwoObject(newObjectWithAnnotation, oldObject);
        if (list != null && list.size() > 0) {
            return list.toString();
        }
        return "";
    }

    /**
     * 对比两个对象同名变量(或者扩展名称)名称,对比内容以map呈现
     *
     * @param newObjectWithAnnotation
     *            含有@ApiModelProperty或者@ObjectVariable注解的新对象,对比出现不相同时,需要获取到该变量的中文名称
     * @param oldObject
     *            旧对象
     * @return 对比内容以List呈现
     */
    public static List<HistoryDesc> compareTwoObject(Object newObjectWithAnnotation, Object oldObject) {
        List<HistoryDesc> list = new LinkedList<>();
        loopForSuperClass(list, newObjectWithAnnotation, oldObject, newObjectWithAnnotation.getClass());
        return list;
    }
    /**
     * 遍历对象所有的继承类
     *
     * @param list
     *            内容不相同的list
     * @param newObjectWithAnnotation
     *            含有@ApiModelProperty或者@ObjectVariable注解的新对象,对比出现不相同时,需要获取到该变量的中文名称
     * @param oldObject
     *            旧对象
     * @param newClazz
     *            新对象Class
     */
    private static void loopForSuperClass(List<HistoryDesc> list, Object newObjectWithAnnotation, Object oldObject, Class<?> newClazz) {
        // 获取对象的class
        // Class<?> newClazz = newObjectWithAnnotation.getClass();
        Class<?> oldClazz = oldObject.getClass();
        // 获取对象的属性数组
        Field[] newFields = newClazz.getDeclaredFields();
        Field[] oldFields = oldClazz.getDeclaredFields();
        // 遍历newFields
        if (newFields != null && newFields.length > 0) {
            for (int i = 0; i < newFields.length; i++) {
                ApiModelProperty apiModelProperty = newFields[i].getAnnotation(ApiModelProperty.class);
                if (apiModelProperty != null) {
                    // 直接通过该变量名获取值对比
                    Object value1 = invokeGetter(newObjectWithAnnotation, newFields[i].getName());
                    Object value2 = invokeGetter(oldObject, newFields[i].getName());
                    if (!compareTwoVal(value1, value2)) {
                        // 不相同
                        HistoryDesc historyDesc = new HistoryDesc(newFields[i].getName(),
                                StringUtils.isNotBlank(apiModelProperty.value()) ? apiModelProperty.value() : newFields[i].getName(), value2, value1);
                        list.add(historyDesc);
                    }
                } else {
                    // 无对象变量注解,不处理
                }
            }
        }
        // 往父类找
        if (newClazz.getSuperclass() != Object.class) {
            loopForSuperClass(list, newObjectWithAnnotation, oldObject, newClazz.getSuperclass());
        }
    }


    public static <E> E invokeGetter(Object obj, String propertyName) {
        Object object = obj;
        for (String name : StringUtils.split(propertyName, ".")) {
            String getterMethodName = GETTER_PREFIX + StringUtils.capitalize(name);
            object = invokeMethod(object, getterMethodName, new Class[]{}, new Object[]{});
        }
        return (E) object;
    }

    /**
     * 对比两个数据是否内容相同
     *
     * @param object1,object2
     * @return boolean类型
     */
    public static boolean compareTwoVal(Object object1, Object object2) {
        if (object1 == null && object2 == null) {
            return true;
        }
        if (object1 == null && object2 != null) {
            return false;
        }
        if (StringUtils.equals(ObjectUtils.toString(object1), ObjectUtils.toString(object2))) {
            return true;
        }
        //比较的类型如果是浮点数类的话
        if ((object1 instanceof BigDecimal || object1 instanceof Double || object1 instanceof Float)
                && (object2 instanceof BigDecimal || object2 instanceof Double || object2 instanceof Float)
        ) {
            BigDecimal bigDecimal1 = new BigDecimal(object1.toString());
            BigDecimal bigDecimal2 = new BigDecimal(object2.toString());
            if (bigDecimal1.compareTo(bigDecimal2) == 0) {
                return true;
            } else {
                return false;
            }
        }
        return false;
    }

    public static <E> E invokeMethod(final Object obj, final String methodName, final Class<?>[] parameterTypes,
                                     final Object[] args) {
        if (obj == null || methodName == null) {
            return null;
        }
        Method method = getAccessibleMethod(obj, methodName, parameterTypes);
        if (method == null) {
            //throw new IllegalArgumentException("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 ");
            log.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 ");
            return null;
        }
        try {
            return (E) method.invoke(obj, args);
        } catch (Exception e) {
            String msg = "method: " + method + ", obj: " + obj + ", args: " + args + "";
            throw new RuntimeException(msg, e);
        }
    }

    /**
     * 循环向上转型, 获取对象的DeclaredMethod,并强制设置为可访问.
     * 如向上转型到Object仍无法找到, 返回null.
     * 匹配函数名+参数类型。
     * 用于方法需要被多次调用的情况. 先使用本函数先取得Method,然后调用Method.invoke(Object obj, Object... args)
     */
    public static Method getAccessibleMethod(final Object obj, final String methodName,
                                             final Class<?>... parameterTypes) {
        // 为空不报错。直接返回 null
        // Validate.notNull(obj, "object can't be null");
        if (obj == null) {
            return null;
        }
        Validate.notNull(methodName, "methodName can't be blank");
        for (Class<?> searchType = obj.getClass(); searchType != Object.class; searchType = searchType.getSuperclass()) {
            try {
                Method method = searchType.getDeclaredMethod(methodName, parameterTypes);
                makeAccessible(method);
                return method;
            } catch (NoSuchMethodException e) {
                // Method不在当前类定义,继续向上转型
                continue;// new add
            }
        }
        return null;
    }

    public static void makeAccessible(Method method) {
        if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers()))
                && !method.isAccessible()) {
            method.setAccessible(true);
        }
    }
}

注解使用

在controller层使用注解进行标记

 @ApiOperation("变更历史记录测试")
    @PostMapping("reportUpdate")
    @HisModifyLog(businessType = "1",module = "产品变更",serviceBean = "reportInfoServiceImpl",queryMethod = "getReportInfo",paramType = "Long",paramKey = "id")
    public Result userInfo(@RequestBody  ReportInfoRo reportInfoRo) {

        reportInfoService.updateReportInfo(reportInfoRo);
        return Result.success("ok", "");

    }

测试

使用apifox来进行测试

入库结果

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
首先,我们需要定义一个自定注解 `@RequiresPermissions`,用于标识需要授权访问的方法,例如: ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequiresPermissions { String[] value(); // 权限值 } ``` 然后,我们需要实现一个切面,用于拦截被 `@RequiresPermissions` 标识的方法,并进行权限校验,例如: ```java @Component @Aspect public class PermissionCheckAspect { @Autowired private AuthService authService; @Around("@annotation(requiresPermissions)") public Object checkPermission(ProceedingJoinPoint joinPoint, RequiresPermissions requiresPermissions) throws Throwable { // 获取当前用户 User user = authService.getCurrentUser(); if (user == null) { throw new UnauthorizedException("用户未登录"); } // 获取当前用户的权限列表 List<String> permissions = authService.getUserPermissions(user); // 校验权限 for (String permission : requiresPermissions.value()) { if (!permissions.contains(permission)) { throw new ForbiddenException("没有访问权限:" + permission); } } // 执行目标方法 return joinPoint.proceed(); } } ``` 在切面中,我们首先通过 `AuthService` 获取当前用户及其权限列表,然后校验当前用户是否拥有被 `@RequiresPermissions` 标识的方法所需的所有权限,如果没有则抛出 `ForbiddenException` 异常,如果有则继续执行目标方法。 最后,我们需要在 Spring 配置文件中启用 AOP 自动代理,并扫描切面所在的包,例如: ```xml <aop:aspectj-autoproxy /> <context:component-scan base-package="com.example.aspect" /> ``` 这样,我们就通过 Spring AOP自定注解模拟实现了类似 Shiro 权限校验的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值