java实现表的变更记录

1 基本思路

数据库中某张表新增、修改、删除时,数据发生了变更,我们需要实现通用的变更记录,与业务代码解耦

基于注解+SpEL表达式,实现采集变更前后的数据

学习后,你将解锁SpEL表达式的使用

2 具体实现

2.1 定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface LogField {
    /**
     * 字段展示名称
     * @return
     */
    String name();

    /**
     * 当前字段是否为业务主键
     * @return
     */
    boolean bizIdFlag() default false;
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface OperateLog {
//    /**
//     * 模块
//     */
//    SystemEnum module() default SystemEnum.FINANCE_PLATFORM;

    /**
     * 操作对象
     */
    OperateObjectEnum operateObj();

    /**
     * 操作或接口名称
     *
     * @return
     */
    String name() default "";

    /**
     * 操作类型
     */
    OperateTypeEnum operateType();

    /**
     * 业务主键表达式
     */
    String bizIdEL() default "";


    /**
     * 变更前数据表达式,举例:#{myBean.doSomething()}
     *
     * @return
     */
    String beforeEL() default "";

    /**
     * 变更后数据表达式,需要时,如果为空,则使用变更前表达式获取新数据
     *
     * @return
     */
    String afterEL() default "";

}

2.2 切面实现

@Slf4j
@Aspect
@Component("FinanceOperateLogAspect")
public class OperateLogAspect {

    @Autowired
    private OperationStrategyFactory operationStrategyFactory;

    @Pointcut("@annotation(com.chint.anneng.finance.common.operate.record.annotation.OperateLog)")
    public void pointCut() {
    }

    @Around(value = "pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        OperateLog annotation = null;
        StandardEvaluationContext context = null;

        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            annotation = method.getAnnotation(OperateLog.class);

            Object[] args = joinPoint.getArgs();

            context = new StandardEvaluationContext();
            SpELUtil.setCommonContext(method, args, context);
            context.setBeanResolver(new BeanFactoryResolver(BeanFactoryUtil.getBeanFactory()));
        } catch (Exception e) {
            //todo:发送告警,不影响业务正常执行
            log.warn("generate operate log error", e);
        }
        Object before = SpELUtil.getValue(annotation.beforeEL(), context, Object.class, null);
        Object returnVal = joinPoint.proceed();

        if (annotation == null) {
            return returnVal;
        }

        OperationStrategy strategy = operationStrategyFactory.getStrategy(annotation.operateType());
        if (strategy == null) {
            return returnVal;
        }
        OperateLogPersistReq req = OperateLogPersistReq.builder()
                .before(before)
                .annotation(annotation)
                .context(context)
                .returnVal(returnVal)
                .build();
        SpELUtil.setCustomContext(LogRecordContext.getVariables(), context);

        strategy.apply(req);
        return returnVal;
    }
}

2.3 策略
 

import com.chint.anneng.finance.common.base.enums.OperateTypeEnum;
import com.chint.anneng.finance.common.operate.record.strategy.impl.DefaultOperationStrategy;
import com.chint.anneng.finance.common.operate.record.strategy.impl.DeleteOperationStrategy;
import com.chint.anneng.finance.common.operate.record.strategy.impl.InsertOperationStrategy;
import com.chint.anneng.finance.common.operate.record.strategy.impl.UpdateOperationStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OperationStrategyFactory {
    @Autowired
    private DefaultOperationStrategy defaultOperationStrategy;
    @Autowired
    private InsertOperationStrategy insertOperationStrategy;
    @Autowired
    private UpdateOperationStrategy updateOperationStrategy;
    @Autowired
    private DeleteOperationStrategy deleteOperationStrategy;

    public OperationStrategy getStrategy(OperateTypeEnum operateType) {
        if (operateType == null) {
            return null;
        }
        switch (operateType) {
            case INSERT:
                return insertOperationStrategy;
            case DELETE:
                return deleteOperationStrategy;
            case UPDATE:
                return updateOperationStrategy;
            default:
                return defaultOperationStrategy;
        }
    }
}
import com.chint.anneng.finance.common.operate.record.model.OperateLogPersistReq;

public interface OperationStrategy {

    void apply(OperateLogPersistReq req);
}

import com.chint.anneng.finance.common.operate.record.annotation.OperateLog;
import com.chint.anneng.finance.portal.log.api.model.FinOperateLogCreateDTO;
import org.apache.commons.lang3.StringUtils;

/**
 * @author: menghua.wang
 * @Desc:
 * @create: 2024-08-09 14:46
 **/
public class FinOperateLogBuilder {

    public static FinOperateLogCreateDTO buildByOperateLog(OperateLog annotation, String bizId) {
        FinOperateLogCreateDTO operationLog = new FinOperateLogCreateDTO();

        operationLog.setBizKey(bizId);
        operationLog.setOperateModule(annotation.operateObj().getSystemEnum().getCode());
        operationLog.setOperateObject(annotation.operateObj().code);
        operationLog.setOperateName(StringUtils.isBlank(annotation.name()) ? annotation.operateType().desc : annotation.name());
        operationLog.setOperateType(annotation.operateType().code);

        return operationLog;
    }
}
@Slf4j
@Component
public class UpdateOperationStrategy implements OperationStrategy {
    @Autowired
    private OperateLogClient operationLogClient;

    @Override
    public void apply(OperateLogPersistReq req) {
        try {
            String bizId = SpELUtil.getValue(req.getAnnotation().bizIdEL(), req.getContext(), String.class, null);


            Object after = SpELUtil.getValue(StringUtils.isNotEmpty(req.getAnnotation().afterEL()) ? req.getAnnotation().afterEL() : req.getAnnotation().beforeEL(), req.getContext(), Object.class, null);

            FinOperateLogCreateDTO operationLog = FinOperateLogBuilder.buildByOperateLog(req.getAnnotation(), bizId);

            List diffs = ObjectComparator.compareFields(req.getBefore(), after);
            operationLog.setDataBefore(req.getBefore() == null ? "" : LogFieldUtil.getAnnotatedFieldStrings(req.getBefore(), diffs));
            operationLog.setDataAfter(after == null ? "" : LogFieldUtil.getAnnotatedFieldStrings(after, diffs));
            operationLogClient.create(operationLog);
        } catch (Exception e) {
            log.warn("save operate log error", e);
        }
    }
}
import com.chint.anneng.finance.common.operate.record.model.OperateLogPersistReq;
import com.chint.anneng.finance.common.operate.record.strategy.FinOperateLogBuilder;
import com.chint.anneng.finance.common.operate.record.strategy.OperationStrategy;
import com.chint.anneng.finance.common.operate.record.utils.SpELUtil;
import com.chint.anneng.finance.portal.log.api.client.OperateLogClient;
import com.chint.anneng.finance.portal.log.api.model.FinOperateLogCreateDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Desc: 操作记录策略
 * @Author: menghua.wang
 * @Date: 2024/8/9
 **/
@Slf4j
@Component
public class DefaultOperationStrategy implements OperationStrategy {
    @Autowired
    private OperateLogClient operationLogClient;

    @Override
    public void apply(OperateLogPersistReq req) {
        try {
            String bizId = SpELUtil.getValue(req.getAnnotation().bizIdEL(), req.getContext(), String.class, null);

            FinOperateLogCreateDTO operationLog = FinOperateLogBuilder.buildByOperateLog(req.getAnnotation(), bizId);

            operationLog.setDataBefore("");
            operationLog.setDataAfter("");
            operationLogClient.create(operationLog);
        } catch (Exception e) {
            log.warn("save operate log error", e);
        }
    }
}
import com.chint.anneng.finance.common.operate.record.model.OperateLogPersistReq;
import com.chint.anneng.finance.common.operate.record.strategy.FinOperateLogBuilder;
import com.chint.anneng.finance.common.operate.record.strategy.OperationStrategy;
import com.chint.anneng.finance.common.operate.record.utils.LogFieldUtil;
import com.chint.anneng.finance.common.operate.record.utils.SpELUtil;
import com.chint.anneng.finance.portal.log.api.client.OperateLogClient;
import com.chint.anneng.finance.portal.log.api.model.FinOperateLogCreateDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Desc: 操作记录策略
 * @Author: menghua.wang
 * @Date: 2024/8/9
 **/
@Slf4j
@Component
public class DeleteOperationStrategy implements OperationStrategy {
    @Autowired
    private OperateLogClient operationLogClient;

    @Override
    public void apply(OperateLogPersistReq req) {
        try {
            String bizId = SpELUtil.getValue(req.getAnnotation().bizIdEL(), req.getContext(), String.class, null);

            FinOperateLogCreateDTO operationLog = FinOperateLogBuilder.buildByOperateLog(req.getAnnotation(), bizId);

            operationLog.setDataBefore(LogFieldUtil.getAnnotatedFieldStrings(req.getBefore(), null));
            operationLog.setDataAfter("");

            operationLogClient.create(operationLog);
        } catch (Exception e) {
            log.warn("save operate log error", e);
        }
    }
}
import com.chint.anneng.finance.common.operate.record.model.OperateLogPersistReq;
import com.chint.anneng.finance.common.operate.record.strategy.FinOperateLogBuilder;
import com.chint.anneng.finance.common.operate.record.strategy.OperationStrategy;
import com.chint.anneng.finance.common.operate.record.utils.LogFieldUtil;
import com.chint.anneng.finance.common.operate.record.utils.SpELUtil;
import com.chint.anneng.finance.portal.log.api.client.OperateLogClient;
import com.chint.anneng.finance.portal.log.api.model.FinOperateLogCreateDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Desc: 操作记录策略
 * @Author: menghua.wang
 * @Date: 2024/8/9
 **/
@Slf4j
@Component
public class InsertOperationStrategy implements OperationStrategy {
    private static final String BIZ_ID_KEY = "#bizId";
    @Autowired
    private OperateLogClient operationLogClient;

    @Override
    public void apply(OperateLogPersistReq req) {
        try {
            Object after = null;

            if (StringUtils.isNoneBlank(req.getAnnotation().afterEL())) {
                after = SpELUtil.getValue(req.getAnnotation().afterEL(), req.getContext(), Object.class, null);
            }

            Object bizIdObj = LogFieldUtil.getBizId(after);
            String bizId = bizIdObj == null ? "" : bizIdObj.toString();

            FinOperateLogCreateDTO operationLog = FinOperateLogBuilder.buildByOperateLog(req.getAnnotation(), bizId);

            operationLog.setDataBefore("");
            operationLog.setDataAfter(after == null ? "" : LogFieldUtil.getAnnotatedFieldStrings(after, null));
            operationLog.setBizKey(bizId);
            operationLogClient.create(operationLog);
        } catch (Exception e) {
            log.warn("save operate log error", e);
        }
    }
}

2.4 工具类

@Slf4j
public class SpELUtil {
    private static final String EXPRESSION_PREFIX = "#{";

    private static final String EXPRESSION_SUFFIX = "}";

    private static ExpressionParser parser = new SpelExpressionParser();

    private static TemplateParserContext templateParserContext = new TemplateParserContext();

    public static  T getValue(String spel, EvaluationContext context, Class clazz, T defaultResult) {
        try {
            if (StringUtils.isBlank(spel)) {
                return defaultResult;
            }
            Expression expression = parseExpression(spel);
            return expression.getValue(context, clazz);
        } catch (Exception e) {
            log.warn("SpEL getValue Error", e);
            return defaultResult;
        }
    }


    public static void setCommonContext(Method method, Object[] arguments, EvaluationContext context) {
        LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();

        String[] params = discoverer.getParameterNames(method);
        for (int len = 0; len < params.length; len++) {
            context.setVariable(params[len], arguments[len]);
        }
    }

    public static void setCustomContext(Map variables, EvaluationContext context) {
        if (variables != null && variables.size() > 0) {
            for (Map.Entry entry : variables.entrySet()) {
                context.setVariable(entry.getKey(), entry.getValue());
            }
        }
    }

    /**
     * 解析表达式
     *
     * @param spelExpression spel表达式
     * @return
     */
    private static Expression parseExpression(String spelExpression) {
        // 如果表达式是一个#{}表达式,需要为解析传入模板解析器上下文
        if (spelExpression.startsWith(EXPRESSION_PREFIX) && spelExpression.endsWith(EXPRESSION_SUFFIX)) {
            return parser.parseExpression(spelExpression, templateParserContext);
        }

        return parser.parseExpression(spelExpression);
    }

    public static StandardEvaluationContext getELContext(ProceedingJoinPoint joinPoint, Method method) {
        Object[] args = joinPoint.getArgs();

        StandardEvaluationContext context = new StandardEvaluationContext();
        SpELUtil.setCommonContext(method, args, context);
        context.setBeanResolver(new BeanFactoryResolver(BeanFactoryUtil.getBeanFactory()));
        return context;
    }
}
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.stereotype.Component;

@Component
public class BeanFactoryUtil implements BeanFactoryAware {
    private static BeanFactory beanFactory;

    public static BeanFactory getBeanFactory() {
        return beanFactory;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        BeanFactoryUtil.beanFactory = beanFactory;
    }

}

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONWriter;
import com.chint.anneng.finance.common.operate.record.annotation.LogField;
import com.chint.anneng.finance.common.operate.record.model.BeanDiff;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


/**
 * @Description: 操作日志字段工具
 * @Author: menghua.wang
 * @Date: 2024/4/22
 */
public class LogFieldUtil {

    public static Object getBizId(Object obj) throws IllegalAccessException {
        if (obj == null) {
            return null;
        }
        Class clazz = obj.getClass();
        Field[] fields = clazz.getDeclaredFields();

        for (Field field : fields) {
            field.setAccessible(true);
            LogField annotation = field.getAnnotation(LogField.class);
            if (annotation != null && annotation.bizIdFlag()) {
                return field.get(obj);
            }
        }
        return null;
    }

    public static String getAnnotatedFieldStrings(Object obj, List diffs) throws IllegalAccessException {
        Map annotatedValues = getAnnotatedFieldValues(obj, diffs);
        return annotatedValues.isEmpty() ? "" : JSON.toJSONString(annotatedValues, JSONWriter.Feature.WriteMapNullValue);
    }

    public static Map getAnnotatedFieldValues(Object obj, List diffs) throws IllegalAccessException {
        Map annotatedValues = new HashMap<>();
        Class clazz = obj.getClass();
        Field[] fields = clazz.getDeclaredFields();

        for (Field field : fields) {
            if (diffs == null || (!CollectionUtils.isEmpty(diffs) && diffs.contains(field.getName()))) {
                // 允许访问私有字段
                field.setAccessible(true);
                LogField annotation = field.getAnnotation(LogField.class);
                if (annotation != null) {
                    Object fieldValue = field.get(obj);
                    annotatedValues.put(annotation.name(), fieldValue);
                }
            }
        }
        return annotatedValues;
    }

    public static BeanDiff compareBeans(List list1, List list2, List includeFields) throws IllegalAccessException {
        if (list1.size() != list2.size()) {
            throw new IllegalArgumentException("Lists must be of the same size.");
        }

        BeanDiff res = new BeanDiff();

        for (int i = 0; i < list1.size(); i++) {
            Object bean1 = list1.get(i);
            Object bean2 = list2.get(i);
            Class clazz = bean1.getClass();
            Field[] fields = clazz.getDeclaredFields();

            Map beforeValues = new HashMap<>();
            Map afterValues = new HashMap<>();
            for (Field field : fields) {
                field.setAccessible(true);
                LogField annotation = field.getAnnotation(LogField.class);
                if (annotation == null) {
                    continue;
                }

                Object value1 = field.get(bean1);
                Object value2 = field.get(bean2);

                boolean includeThisField = includeFields.size() > 0 && includeFields.stream().anyMatch(s -> s.equals(annotation.name()));
                if (!value1.equals(value2) || includeThisField) {
                    beforeValues.put(annotation.name(), value1);
                    afterValues.put(annotation.name(), value2);
                }
            }
            setResult(beforeValues, afterValues, includeFields, res);
        }
        return res;
    }

    private static void setResult(Map beforeValues, Map afterValues, List includeFields, BeanDiff res) {
        if (beforeValues.isEmpty()) {
            return;
        }

        List> before = res.getBeforeList();

        if (before == null) {
            before = new ArrayList<>();
            res.setBeforeList(before);
        }
        before.add(beforeValues);

        res.setBeforeList(before.stream().filter(m -> !new ArrayList<>(m.keySet()).equals(includeFields)).collect(Collectors.toList()));

        List> after = res.getAfterList();

        if (after == null) {
            after = new ArrayList<>();
            res.setAfterList(after);
        }

        after.add(afterValues);
        res.setAfterList(after.stream().filter(m -> !new ArrayList<>(m.keySet()).equals(includeFields)).collect(Collectors.toList()));
    }

    public static Map getAnnotatedFieldNames(Class clazz) {
        Map annotatedFields = new HashMap<>();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            LogField annotation = field.getAnnotation(LogField.class);
            if (annotation != null) {
                annotatedFields.put(field.getName(), annotation.name());
            }
        }
        return annotatedFields;
    }

} 
  

import com.chint.anneng.finance.common.operate.record.annotation.LogField;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * @Desc: 对象比较器
 * @Author: menghua.wang
 * @Date: 2024/5/27
 **/
public class ObjectComparator {

    public static List compareFields(Object before, Object after) throws IllegalAccessException {
        Class clazz = before.getClass();
        Field[] fields = clazz.getDeclaredFields();
        List differences = new ArrayList<>();

        for (Field field : fields) {
            field.setAccessible(true);
            LogField annotation = field.getAnnotation(LogField.class);
            if (annotation != null) {
                Object oldValue = field.get(before);
                Object newValue = field.get(after);
                if (!Objects.equals(oldValue, newValue)) {
//                    change.put("oldValue", oldValue);
//                    change.put("newValue", newValue);
                    differences.add(field.getName());
                }
            }
        }
        return differences;
    }


    public static List compareList(List before, List after) throws IllegalAccessException {
        Class clazz = before.getClass();
        Field[] fields = clazz.getDeclaredFields();
        List differences = new ArrayList<>();

        for (Field field : fields) {
            field.setAccessible(true);
            LogField annotation = field.getAnnotation(LogField.class);
            if (annotation != null) {
                Object oldValue = field.get(before);
                Object newValue = field.get(after);
                if (!Objects.equals(oldValue, newValue)) {
//                    change.put("oldValue", oldValue);
//                    change.put("newValue", newValue);
                    differences.add(field.getName());
                }
            }
        }
        return differences;
    }
} 
  

3 使用示例

新增:

LogRecordContext.putVariable("bizId",id);使用上下文设置主键信息,用于存储变更的业务主键

@PostMapping("") @OperateLog(operateObj = OperateObjectEnum.FINANCE_INVOICE, name = "金融发票新增", operateType = OperateTypeEnum.INSERT, afterEL = "#{@finInvoiceServiceImpl.getOneById(#bizId)}") public ResponseData add(@Validated @RequestBody FinInvoiceCreateVO createVO) { Long id = finInvoiceService.create(createVO); LogRecordContext.putVariable("bizId",id); return success(id); }

修改:

beforeEL:用于获取变更前的数据,finScfAgentCreditServiceImpl是FinScfAgentCreditServiceImpl类的首字母小写(从spring容器中获取的对应名称的bean)

@OperateLog( operateObj = OperateObjectEnum.SUPPLY_CHAIN_FINANCE_CREDIT_MANAGEMENT, operateType = OperateTypeEnum.UPDATE, bizIdEL = "#param.id", beforeEL = "#{@finScfAgentCreditServiceImpl.getCreditDetailById(#param.id)}")

删除:

@DeleteMapping("/{id}") @OperateLog(operateObj = OperateObjectEnum.FINANCE_INVOICE, name = "金融发票删除", operateType = OperateTypeEnum.DELETE, bizIdEL = "#id", beforeEL = "#{@finInvoiceServiceImpl.getById(#id)}") public ResponseData deleteFinInvoice(@PathVariable @Validated @NotNull Long id) { finInvoiceService.deleteById(id); return success(true); }

批量更新:目前仅platform实现,如有需要请联系我

@OperateLog(operateObj = OperateObjectEnum.REPAYMENT_SCHEDULE, name = "批量更新", bizIdEL = "#updateVO[0].bankLoanSummaryId+ '_' + #updateVO[0].repaymentDate", operateType = OperateTypeEnum.BATCH_UPDATE, resIncludeFields = {"项目名称", "类型"}, beforeEL = "#{@repaymentScheduleServiceImpl.getByUpdates(#updateVO)}")

你可能感兴趣的:(java,数据库,开发语言)