概述
项目开发过程中,有很多需要记录修改日志的轨迹,并且需要知道修改了哪些内容。之前一直都是写一个单独的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来进行测试