aop实现前后数据对比的操作记录
先看效果
定义注解
CoverItem:这个注解用于修饰属性,目的是保存记录时将字段的英文转成注解中的value值,也就是转成中文
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CoverItem {
String value();
boolean isCheck() default true;
}
OperLog:这个注解作为切入点
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperLog {
int type();
Class clazz();
Class convertClass();
}
aspectj业务层
逻辑不难直接上代码了,实现就是查询未修改之前的对象和修改之后的对象,通过对比两个对象的field,判断是否修改值,再将修改的值封装成一条日志保存入库。
这里考虑到我的业务所以并没有做通用的全覆盖,需要用的话,参考着改一下应该可以很轻松的应用到自己的业务场景中。
aop环向切面
import cn.flydiy.cloud.base.context.User;
import cn.flydiy.cloud.common.lang.StringUtils;
import cn.flydiy.cloud.common.utils.SecurityUtils;
import cn.hutool.extra.spring.SpringUtil;
import com.flydiy.sample.auto.dao.OperatorLogMapper;
import com.flydiy.sample.auto.pojo.po.OperatorLogPO;
import com.flydiy.sample.ext.aspectj.annotation.CoverItem;
import com.flydiy.sample.ext.aspectj.annotation.OperLog;
import com.flydiy.sample.ext.convert.CustomerConvert;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
@Aspect
@Component
public class OperatorLogAspectj {
@Pointcut("@annotation(com.flydiy.sample.ext.aspectj.annotation.OperLog)")
public void pointcut(){}
/**
* 操作日志记录切面
* @param point
* @throws Exception
*/
@Around("pointcut()")
public void operatorLog(ProceedingJoinPoint point) throws Exception {
Object obj = getArg(point);
int type = getType(point);
Class clazz = getClazz(point);
Class convertClass = getConvertClass(point);
Long originId = getId(obj);
Object originObj = getObj(clazz,convertClass,originId);
try {
point.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
Object updateObj = getObj(clazz,convertClass,originId);
//构建日志对象
OperatorLogPO operatorLog = createOperLog(originObj,updateObj,type,originId);
if(Objects.isNull(operatorLog)) {
return;
}
//保存日志
saveLog(operatorLog);
}
private Class getClazz(ProceedingJoinPoint point) {
OperLog log = getLog(point);
return log.clazz();
}
private Class getConvertClass(ProceedingJoinPoint point) {
OperLog log = getLog(point);
return log.convertClass();
}
private int getType(ProceedingJoinPoint point) {
OperLog log = getLog(point);
return log.type();
}
private OperLog getLog(ProceedingJoinPoint point) {
OperLog log = getAnnotationLog(point);
if(Objects.isNull(log)) {
throw new RuntimeException("接口缺少OperLog注解");
}
return log;
}
private void saveLog(OperatorLogPO operatorLog) {
OperatorLogMapper mapper = SpringUtil.getBean(OperatorLogMapper.class);
mapper.insert(operatorLog);
}
private OperatorLogPO createOperLog(Object originObj, Object updateObj, int type, Long originId) throws IllegalAccessException {
StringJoiner sj = new StringJoiner("|");
Class<?> clazz = originObj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
Object originField = getField(field,originObj);
Object updateField = getField(field,updateObj);
//如果属性变更,则记录
if(!originField.equals(updateField)) {
CoverItem coverItem = field.getAnnotation(CoverItem.class);
isValidCoverItem(coverItem,field);
if(!coverItem.isCheck()) {
continue;
}
String desc = createLogDesc(field,originField,updateField);
sj.add(desc);
}
}
if(StringUtils.isBlank(sj.toString())) {
return null;
}
OperatorLogPO operatorLog = buildOperLog(sj);
operatorLog.setType(type);
operatorLog.setOriginId(originId);
return operatorLog;
}
private Object getField(Field field,Object originObj) throws IllegalAccessException {
Object obj = field.get(originObj);
if(Objects.isNull(obj)) {
obj = "null";
}
return obj;
}
private OperatorLogPO buildOperLog(StringJoiner sj) {
User user = SecurityUtils.getLoginUser().get();
if(Objects.isNull(user)) {
throw new RuntimeException("获取用户信息异常");
}
String username = user.getUsername();
String userId = user.getId();
String desc = sj.toString();
OperatorLogPO operator = new OperatorLogPO();
operator.setOperatorLog(desc);
operator.setUesrName(username);
operator.setCreatedBy(userId);
operator.setCreatedDate(new Date());
return operator;
}
private String createLogDesc(Field field, Object originField, Object updateField) {
CoverItem annotation = field.getAnnotation(CoverItem.class);
String desc = "将字段[%s],从[%s]变更为[%s]";
desc = String.format(desc,annotation.value(),originField,updateField);
return desc;
}
private void isValidCoverItem(CoverItem coverItem, Field field) {
if(Objects.isNull(coverItem)) {
throw new RuntimeException(field.getName()+"缺少@CoverItem注解");
}
}
private Object getObj(Class clazz, Class convertClass, Long id) throws Exception {
Object obj = SpringUtil.getBean(clazz);
Method method = clazz.getMethod("selectById", Serializable.class);
Object invoke = method.invoke(obj, id);
Object convert = CustomerConvert.convert(invoke, convertClass);
return convert;
}
private Long getId(Object obj) throws Exception {
Field field = obj.getClass().getDeclaredField("id");
field.setAccessible(true);
Long id = (Long) field.get(obj);
return id;
}
private Object getArg(ProceedingJoinPoint point) {
Object[] args = point.getArgs();
if(Objects.isNull(args) || args.length < 1) {
throw new RuntimeException("参数异常");
}
return args[0];
}
private OperLog getAnnotationLog(ProceedingJoinPoint point) {
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if(Objects.nonNull(method)) {
return method.getAnnotation(OperLog.class);
}
return null;
}
}
其中用到的工具类,po2po工具类
import com.flydiy.sample.auto.pojo.po.ColorSamplePO;
import com.flydiy.sample.auto.pojo.po.SampleInfoPO;
import com.flydiy.sample.auto.pojo.vo.ColorSampleExportVO;
import com.flydiy.sample.auto.pojo.vo.SampleInfoExportVO;
import com.flydiy.sample.ext.pojo.vo.ColorSampleExtExportVO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Objects;
public class CustomerConvert {
/**
* 类型转换
* @param obj
* @param clazz
* @return
* @throws Exception
*/
public static Object convert(Object obj, Class clazz) throws Exception {
Object targetObj = clazz.getDeclaredConstructor().newInstance();
Class<?> originClass = obj.getClass();
Field[] fields = originClass.getDeclaredFields();
for (Field field : fields) {
if(notValidField(field) || isStaticModifier(field)) {
continue;
}
field.setAccessible(true);
Object fileAtr = field.get(obj);
Field field1 = null;
try {
field1 = clazz.getDeclaredField(field.getName());
} catch (NoSuchFieldException e) {
continue;
}
if(notValidField(field1)) {
continue;
}
field1.setAccessible(true);
field1.set(targetObj,fileAtr);
}
return targetObj;
}
private static boolean notValidField(Field field) {
return Objects.isNull(field);
}
private static boolean isStaticModifier(Field field) {
return Modifier.isStatic(field.getModifiers());
}
}
使用
在接口加上@OperLog注解
@ApiOperation("单条更新")
@PostMapping({"/api/v1/sample-info/ext/update", Constant.INNER_PATH_PREFIX + "/api/v1/sample-info/ext/update"})
@OperLog(type = OperatorTypeConstant.SAMPLEINFOTYPE, clazz = SampleInfoMapper.class, convertClass = SampleInfoExtPO.class)
public ResponseInfo<Boolean> update(@RequestBody SampleInfoExtPO sampleInfoExtPO) throws Exception {
// 验证入参
ParameterSupport.validateParameter(sampleInfoExtPO);
// 验证表单是否完整
ParameterSupport.validateForm(sampleInfoExtPO);
// 格式化入参
ParameterSupport.formatParameter(sampleInfoExtPO);
SampleInfoPO sampleInfoPO = (SampleInfoPO) CustomerConvert.convert(sampleInfoExtPO, SampleInfoPO.class);
// 调用Service
boolean flag = sampleInfoExtService.updateById(sampleInfoPO);
return ResponseInfo.<Boolean>success().data(flag);
}
状态的日志记录是 [将发布状态从1更新为2] 这种形式,看起来很不直观,
如果能达到 [将发布状态从未发布 更新为 已发布] 这种效果就好了。
那么怎么才能解决呢。当然是有办法的啦:
我们可以在我们的自定义枚举中加一个字段fieldCover
,标识数字对应的状态值。
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CoverItem {
String value();
boolean isCheck() default true;
String fieldCover();
}
这个字段的值我们可以写 数字对应的中文的json字符串。
例如:
public class Person{
@CoverItem(fieldCover="{\n" +
" \"1\": \"未发布\",\n" +
" \"2\": \"已发布\"\n" +
"}")
private Integer status;
}
然后在获取field字段时判断有没有这个注解修饰,并且是否有fieldCover
值,如果有这个属性值,直接用JSONUtil
工具转成JsonObject
对象,然后将field字段的值转为对应的中文。
代码我就不写了,如果实在有需要的话,可以评论区留言。