数据库中某张表新增、修改、删除时,数据发生了变更,我们需要实现通用的变更记录,与业务代码解耦
基于注解+SpEL表达式,实现采集变更前后的数据
学习后,你将解锁SpEL表达式的使用
@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 "";
}
@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;
}
}
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);
}
}
}
@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
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;
}
}
新增:
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)}")