Biz-Logger: Iteration #1 Diff Log capability

系列文章目录

Biz-Logger操作日志框架


文章目录

  • 系列文章目录
  • 前言
  • [一、设计 / 对比 / 实现](#一、设计 / 对比 / 实现)
    • [1.1 注解](#1.1 注解)
      • [1.1.1 BizDiffer](#1.1.1 BizDiffer)
      • [1.1.2 BizDiffField](#1.1.2 BizDiffField)
    • [1.2 解析注解 BizLoggerBasedBizLogCreatorFactory](#1.2 解析注解 BizLoggerBasedBizLogCreatorFactory)
    • [1.3 BizDiffDTO](#1.3 BizDiffDTO)
    • [1.4 DIFF函数](#1.4 DIFF函数)
  • 二、用例
    • [2.1 系统配置](#2.1 系统配置)
    • [2.2 Java Bean](#2.2 Java Bean)
    • [2.3 业务接口](#2.3 业务接口)
    • [2.4 测试方法](#2.4 测试方法)
      • [2.4.1 输出BizLogDTO日志对象](#2.4.1 输出BizLogDTO日志对象)

前言

记录Diff日志也是很多系统必须的一部分, 例如很多CRM系统, 记录表单更新等.

前文提到的两个框架都实现了自己的diff-log操作, 但是使用方式各有不同, 并且对于复杂类型数组/集合的处理都没有银弹.

使用者应考虑是否需要记录复杂类型数组/集合的变更.


一、设计 / 对比 / 实现

  • mzt-biz-log: 基于java-object-diff, 增加了数组类型的支持, 但是主要目的是把diff日志写在操作日志的属性上, 如果变更记录多的话, 感觉压根儿不够放的.
  • log-record: 自主解析, 利用日志对象保存多个diff记录. 但其直接转JSON再比较的方式也挺随意的.

biz-logger则考虑使用log-record的做法, 但是基于java-object-diff实现功能. 不论哪种diff工具, 最终对于复杂类型的集合或数组的比较结果, 在可视化上都比较蛋疼, 上述两个框架也没有很好的处理这种情况.

例如 mzt-biz-log 仅针对简单类型的数组/集合(如 String[], List<String>) 做处理. 本文则针对复杂对象采取只处理子节点第一层的方式, 不再继续深入. 但使用者可以通过继承DIFF函数, 根据实际需求重写逻辑.

1.1 注解

1.1.1 BizDiffer

作为@BizLogger内部的一个注解, 直接指定需要diff的两个变量. 不直接参与操作日志的拼接.

  • mzt-biz-log: 为了实现拼接在日志文本里的目的, 定义了_DIFF自定义函数方便使用, 但限制了操作日志内容的长度.
  • log-record: 与上述基本相同, 只是在diff的实现上不一样. 但测试用例上, diff操作更像是简单记录一下变化而已.

框架自定义的东西, 需要使用者明确知道用途用法, 不太方便. 而且在实际使用上看, 业务变更字段或多或少, 这将导致直接拼接到日志文本中的内容过长被截取后变的不可读. 而且这种diff-log的特性就不适合将所有变更记录在一行文字中. 所以本文参考了销帮帮CRM系统的journey接口设计, 考虑将变更记录作为列表展示. 故这里直接定义一个diff注解, 使用者只需关心传递变量, 无需关心是怎么diff, 用哪个函数进行diff, 最终diff结果将保存在BizLogDTO中, 由使用者决定如何处理.

当然还是那句话, 三个框架的方式都是取舍的结果.

java 复制代码
/**
 * @author hp
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BizDiffer {

    @MethodDesc("未修改的对象, 使用#+变量名称格式, 如, #order")
    @Language("SpEL")
    String before() default "";

    @MethodDesc("修改后的对象, 使用#+变量名称格式, 一般这个修改后的值需要通过BizLoggerContext手动设置变量, 如, #changedOrder")
    @Language("SpEL")
    String after() default "";

    @MethodDesc("是否忽略")
    boolean ignored() default false;
}

1.1.2 BizDiffField

属性注解, 为参与diff的对象属性提供别名和数据转换的能力

  • mzt-biz-log: 提供别名和函数操作
  • log-record: 仅提供别名操作, 如果仅提取一个id出来没意义, 人也读不懂.

本文类似mzt-biz-log, 同样提供 别名 和 函数 操作. 通过value字段的SpEL提供数据转换能力.

java 复制代码
 /**
 * @author hp
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BizDiffField {
		
	@MethodDesc("别名")
    String alias() default "";

    @Language("SpEL")
    String value() default "";

    @MethodDesc("是否忽略")
    boolean ignored() default false;
}

1.2 解析注解 BizLoggerBasedBizLogCreatorFactory

前文中提到过的BizLoggerBasedBizLogCreatorFactory类, 工厂式的构造解析方法.

  • mzt-biz-log: 事务脚本式编码的解析, 方法线程串行
  • log-record: 事务脚本式编码的解析, 方法线程串行

因为考虑将diffLog作为BizLogDTO的一部分, 所以本文则是通过工厂构建解析逻辑, 并固定自定义函数名称.

同时, 在解析时增加了通过日志上下文BizLoggerContext直接在业务方法中设置需要diff的对象的方式, 配合@BizLogger作为入口进行diff解析. 此方式优先级低于@BizDiffer. 解析将生成BizDiffDTO集合

java 复制代码
@Override
protected Function<EvaluationContext, List<BizDiffDTO>> createForDiffs(Class<?> targetClass, Method method, BizLogger annotation) {
    return context -> {
        final BizDiffer diff = annotation.diff();
        String expression;
        if (diff.ignored()) {
            expression = "#{#BIZ_DIFFER(#" + BizLoggerContext.DIFF_OBJECTS_KEY + ")}";
        } else {
            final String wrapperClassName = BizDiffObjectWrapper.class.getName();
            expression = "#{#BIZ_DIFFER( new " + wrapperClassName + "(" + diff.before() + ", " + diff.after() + "))}";
        }
        return bizLoggerExpressionEvaluator.getValue(expression, targetClass, method, context);
    };
}

1.3 BizDiffDTO

  • mzt-biz-log: 没有类似设计, 并且需要根据预设模版构造信息, 模版上的关键字变量需要校验
  • log-record: 多了对类的别名设计, 需要根据预设模版构造信息, 模版上的关键字变量需要校验

两者的模版关键字是固定的, 但是提供了配置文件加载能力, 实际使用上, 这种个字符串拼接关键字的模版, 如果校验不够完善, 极其容易出现模版解析问题.

本文设计主要是保留了属性原本的值, 和调用自定义函数后的值, 并且记录diff的操作状态, 例如新增, 删除, 修改等. 使用者可以根据实际情况, 在同步时根据DTO的数据构建自定义的日志信息.

java 复制代码
/**
 * @author hp
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BizDiffDTO {

    @FieldDesc("属性")
    private String attr;

    @FieldDesc("属性名")
    private String name;

    private List<DiffAction> actions;

    public BizDiffDTO(String attr, String name) {
        this.attr = attr;
        this.name = name;
    }

    @Data
    public static class DiffAction {

        @FieldDesc("操作状态: ADDED/CHANGED/REMOVED")
        private DiffState state;

        @FieldDesc("修改之前的值")
        private String before;

        @FieldDesc("如果存在转换, 这里是解析后的值, 如果没有转换, 则等于before")
        private String parsedBefore;

        @FieldDesc("修改之后的值")
        private String after;

        @FieldDesc("如果存在转换, 这里是解析后的值, 如果没有转换, 则等于after")
        private String parsedAfter;

        public void setState(String state) {
            this.state = DiffState.valueOf(state);
        }

        public void setState(DiffState state) {
            this.state = state;
        }

        public DiffAction(DiffState state, String before, String after) {
            this.state = state;
            this.before = before;
            this.after = after;
        }

        public DiffAction(String state, String before, String after) {
            this.state = DiffState.valueOf(state);
            this.before = before;
            this.after = after;
            this.parsedBefore = before;
            this.parsedAfter = after;
        }

        public boolean isAdded() {
            return Objects.equals(this.state, DiffState.ADDED);
        }

        public boolean isChanged() {
            return Objects.equals(this.state, DiffState.CHANGED);
        }

        public boolean isRemoved() {
            return Objects.equals(this.state, DiffState.REMOVED);
        }

        public String getAddedValue(boolean parsed) {
            if (isAdded()) {
                return parsed ? this.parsedAfter : this.after;
            }
            throw new IllegalStateException("The action is not a added action.");
        }

        public String getRemoveValue(boolean parsed) {
            if (isRemoved()) {
                return parsed ? this.parsedBefore : this.before;
            }
            throw new IllegalStateException("The action is not a removed action.");
        }
    }

    @AllArgsConstructor
    @Getter
    public enum DiffState implements BaseEnum<DiffState, Integer> {
        ADDED(1, "新增") {
            @Override
            public boolean meaningful() {
                return true;
            }
        },
        CHANGED(2, "修改") {
            @Override
            public boolean meaningful() {
                return true;
            }
        },
        REMOVED(3, "删除") {
            @Override
            public boolean meaningful() {
                return true;
            }
        },
        UNTOUCHED(4, "The value is identical in the working and base object"),
        CIRCULAR(5, "Special state to mark circular references"),
        IGNORED(6, "The value has not been looked at and has been ignored"),
        INACCESSIBLE(7, "When a comparison was not possible because the underlying value was not accessible"),
        ;
        private final Integer code;
        private final String name;

        public boolean meaningful() {
            return false;
        }
    }
}

1.4 DIFF函数

三个框架都在某种程度上实现了自定义函数的设计, 所以DIFF功能就可以作为一个自定义函数, 插件式的引入框架当中.

  • mzt-biz-log: 使用java-object-diff实现功能, 其原生不支持数组类型的对比, 由log框架自行实现. 函数定义则根据自定义函数接口完成实现.
  • log-record: 直接通过反射+转JSON再对比的方式自主实现. 函数则根据自定义函数注解标记+扫描的方式实现.

本文在前文中展示过自定义函数设计, 类似log-record的注解标记方式, 相对更灵活些. 并利用java-object-diff能力完成实现.

其中针对复杂对象的数组或集合的处理, 三者都有取舍, mzt-biz-log则直接忽略对此场景的处理(实际也没啥好办法). log-record则是转JSON对比, 并将diff记录保存为集合. 本文的取舍是对复杂对象数组/集合仅处理元素的变化, 不处理元素内部属性的变化, 并在构建diff记录集合前使用标准SpEL解析的方式处理自定义函数, 类似前文提到的操作. DIFF函数实现为全可继承方式, 方便使用者细化处理逻辑.

同时在处理DIFF属性存在使用自定义函数的情况, 如@BizDiffField(alias = "消费者", value = "#{#BUYER_NAME(purchaseName)}")当中的#BUYER_NAME自定义函数, 使框架标准模版解析器BizLoggerExpressionEvaluator解析SpEL表达式. 但SpEL上下文仅基于参与DIFF的对象, 以及注册的自定义函数.

java 复制代码
public interface IBizDifferComponent {
    Collection<BizDiffDTO> diff(@Nullable BizDiffObjectWrapper diffObjectWrapper);
}
/**
 * @author hp
 */
@ConditionalOnMissingBean(IBizDifferComponent.class)
@BizLoggerComponent("javaObjectDiffBasedBizDifferComponent")
public class JavaObjectDiffBasedBizDifferComponent implements IBizDifferComponent {

    protected final BizLoggerProperties.DiffProperties diffProperties;
    protected final BeanResolver beanResolver;
    protected BizLoggerExpressionEvaluator bizLoggerExpressionEvaluator;

    public JavaObjectDiffBasedBizDifferComponent(
            BizLoggerProperties bizLoggerProperties,
            BeanFactory beanFactory
    ) {
        this.diffProperties = bizLoggerProperties.getDiffConfig();
        this.beanResolver = new BeanFactoryResolver(beanFactory);
    }

    @BizLoggerFunction("BIZ_DIFFER")
    @Override
    public Collection<BizDiffDTO> diff(@Nullable BizDiffObjectWrapper diffObjectWrapper) {
        if (Objects.isNull(diffObjectWrapper)) {
            return Collections.emptyList();
        }
        final Object before = diffObjectWrapper.getInitValue();
        final Object after = diffObjectWrapper.getChangedValue();
        final DiffNode rootNode = getObjectDiffer().compare(after, before);
        if (!rootNode.hasChanges()) {
            return Collections.emptyList();
        }
        return createDiffLogs(before, after, rootNode);
    }

    @NonNull
    protected ObjectDiffer getObjectDiffer() {
        final ObjectDifferBuilder objectDifferBuilder = ObjectDifferBuilder.startBuilding();
        return objectDifferBuilder
                .differs()
                .register((differDispatcher, nodeQueryService) -> new ArrayDiffer(differDispatcher, (ComparisonService) objectDifferBuilder.comparison(), objectDifferBuilder.identity()))
                .build();
    }

    @NonNull
    protected Collection<BizDiffDTO> createDiffLogs(Object before, Object after, DiffNode rootNode) {
        final List<BizDiffDTO> diffs = Lists.newArrayList();
        rootNode.visitChildren((diffNode, visit) -> {
            // Do not process root node or nodes without changes.
            if (!diffNode.hasChanges()) {
                return;
            }
            if (diffNode.hasChildren()) {
                // Processing Array or Collection explicitly.
                if (isArrayOrCollection(diffNode)) {
                    createForArrayOrCollection(before, after, diffNode).ifPresent(diffs::add);
                }
                return;
            }
            if (isArrayOrCollectionRecursively(diffNode.getParentNode())) {
                visit.dontGoDeeper();
                return;
            }
            createForDiffNode(before, after, diffNode).ifPresent(diffs::add);
        });
        return diffs;
    }

    protected boolean isArrayOrCollectionRecursively(DiffNode diffNode) {
        DiffNode node = diffNode;
        while (node != null) {
            if (isArrayOrCollection(node)) {
                return true;
            }
            node = node.getParentNode();
        }
        return false;
    }

    protected Optional<BizDiffDTO> createForArrayOrCollection(Object before, Object after, DiffNode diffNode) {
        final List<BizDiffDTO> childDiffs = Lists.newArrayListWithCapacity(diffNode.childCount());
        diffNode.visitChildren((childNode, childVisit) -> {
            if (!Classes.isSimpleType(childNode.getValueType())) {
                childVisit.dontGoDeeper();
            }
            createForDiffNode(before, after, childNode).ifPresent(childDiffs::add);
        });
        if (CollUtil.isEmpty(childDiffs)) {
            return Optional.empty();
        }
        final List<BizDiffDTO.DiffAction> childActions = childDiffs.stream()
                .flatMap(diffDTO -> diffDTO.getActions().stream())
                .filter(diffDTO -> diffDTO.getState().meaningful())
                .toList();
        return createForDiffNode(before, after, diffNode)
                .map(i -> {
                    i.setActions(groupingActions(childActions));
                    return i;
                });
    }

    // Users can rewrite this method to avoid grouping.
    protected List<BizDiffDTO.DiffAction> groupingActions(List<BizDiffDTO.DiffAction> actions) {
        return actions.stream()
                .collect(Collectors.groupingBy(BizDiffDTO.DiffAction::getState))
                .entrySet()
                .stream()
                .map(entry ->
                        new BizDiffDTO.DiffAction(
                                entry.getKey(),
                                groupingActionValues(entry.getValue(), BizDiffDTO.DiffAction::getBefore),
                                groupingActionValues(entry.getValue(), BizDiffDTO.DiffAction::getAfter)
                        )
                )
                .toList();
    }

    protected String groupingActionValues(List<BizDiffDTO.DiffAction> diffActions, Function<BizDiffDTO.DiffAction, String> mapper) {
        final String values = diffActions.stream()
                .map(mapper)
                .filter(StrUtil::isNotEmpty)
                .collect(Collectors.joining(StrUtil.COMMA));
        return StrUtil.isEmpty(values) ? null : values;
    }

    protected boolean isArrayOrCollection(DiffNode diffNode) {
        return !diffNode.getValueType().isPrimitive() && (diffNode.getValueType().isArray()) || Collection.class.isAssignableFrom(diffNode.getValueType());
    }

        protected Optional<BizDiffDTO> createForDiffNode(Object before, Object after, DiffNode diffNode) {
        final BizDiffDTO diffDTO = new BizDiffDTO(diffNode.getPropertyName(), this.getTraversalName(diffNode));

        final BizDiffField bizDiffField = diffNode.getFieldAnnotation(BizDiffField.class);

        final BizDiffDTO.DiffAction action = new BizDiffDTO.DiffAction(
                diffNode.getState().name(),
                Optional.ofNullable(diffNode.canonicalGet(before)).map(Objects::toString).orElse(null),
                Optional.ofNullable(diffNode.canonicalGet(after)).map(Objects::toString).orElse(null)
        );
        if (Objects.nonNull(bizDiffField) && StrUtil.isNotEmpty(bizDiffField.value())) {
            action.setParsedBefore(Optional.ofNullable(getExpressionValue(bizDiffField, before, diffNode)).map(Object::toString).orElse(null));
            action.setParsedAfter(Optional.ofNullable(getExpressionValue(bizDiffField, after, diffNode)).map(Object::toString).orElse(null));
        }
        diffDTO.setActions(Collections.singletonList(action));

        if (Objects.isNull(bizDiffField) || !bizDiffField.ignored()) {
            return Optional.of(diffDTO);
        }
        return Optional.empty();
    }

    protected Object getExpressionValue(BizDiffField bizDiffField, Object object, DiffNode diffNode) {
        final String expression = bizDiffField.value();
        if (StrUtil.isEmpty(expression)) {
            return object;
        }
        
        final Class<?> targetClass = object.getClass();
        final AnnotatedElement field = ClassUtil.getDeclaredField(targetClass, diffNode.getPropertyName());
        final EvaluationContext evaluationContext = BizLoggerEvaluationContextFactory.createStandardEvaluationContext(object, beanResolver);

        return lazyLoadExpressionEvaluator().getValue(expression, targetClass, field, evaluationContext);
    }

    // To avoid the circular reference issue.
    protected BizLoggerExpressionEvaluator lazyLoadExpressionEvaluator() {
        if (this.bizLoggerExpressionEvaluator != null) {
            return this.bizLoggerExpressionEvaluator;
        }
        final IBizLoggerFunctionResolver bizLoggerFunctionResolver = SpringUtil.getBean(IBizLoggerFunctionResolver.class);
        this.bizLoggerExpressionEvaluator = new BizLoggerExpressionEvaluator(
                bizLoggerFunctionResolver,
                new SpelExpressionParser()
        );
        return this.bizLoggerExpressionEvaluator;
    }

    @NonNull
    protected String getTraversalName(@NonNull DiffNode node) {
        String name = Optional.ofNullable(node.getFieldAnnotation(BizDiffField.class))
                .map(BizDiffField::alias)
                .orElse(node.getPropertyName());
        DiffNode parent = node.getParentNode();
        while (parent != null) {
            if (parent.isRootNode()) {
                parent = parent.getParentNode();
                continue;
            }
            String finalName = name;
            DiffNode finalParent = parent;
            name = Optional.ofNullable(parent.getFieldAnnotation(BizDiffField.class))
                    .map(annotation -> annotation.alias().concat(diffProperties.getWords().getOf()).concat(finalName))
                    .orElse(finalParent.getPropertyName().concat(diffProperties.getWords().getOf()).concat(finalName));
            parent = parent.getParentNode();
        }
        return name;
    }

    protected static class ArrayDiffer implements Differ {
        private final DifferDispatcher differDispatcher;
        private final ComparisonStrategyResolver comparisonStrategyResolver;
        private final IdentityStrategyResolver identityStrategyResolver;

        public ArrayDiffer(final DifferDispatcher differDispatcher,
                           final ComparisonStrategyResolver comparisonStrategyResolver,
                           final IdentityStrategyResolver identityStrategyResolver) {
            Assert.notNull(differDispatcher, "differDispatcher");
            this.differDispatcher = differDispatcher;

            Assert.notNull(comparisonStrategyResolver, "comparisonStrategyResolver");
            this.comparisonStrategyResolver = comparisonStrategyResolver;

            Assert.notNull(identityStrategyResolver, "identityStrategyResolver");
            this.identityStrategyResolver = identityStrategyResolver;
        }

        @Override
        public boolean accepts(Class<?> type) {
            return !type.isPrimitive() && type.isArray();
        }

        @Override
        public DiffNode compare(DiffNode parentNode, Instances arrayInstances) {
            final DiffNode arrayNode = newNode(parentNode, arrayInstances);
            final IdentityStrategy identityStrategy = identityStrategyResolver.resolveIdentityStrategy(arrayNode);
            if (identityStrategy != null) {
                arrayNode.setChildIdentityStrategy(identityStrategy);
            }
            if (arrayInstances.hasBeenAdded()) {
                final Collection<?> addedItems = arrayAsCollection(arrayInstances.getWorking());
                compareItems(arrayNode, arrayInstances, addedItems, identityStrategy);
                arrayNode.setState(DiffNode.State.ADDED);
            } else if (arrayInstances.hasBeenRemoved()) {
                final Collection<?> removedItems = arrayAsCollection(arrayInstances.getBase());
                compareItems(arrayNode, arrayInstances, removedItems, identityStrategy);
                arrayNode.setState(DiffNode.State.REMOVED);
            } else if (arrayInstances.areSame()) {
                arrayNode.setState(DiffNode.State.UNTOUCHED);
            } else {
                final ComparisonStrategy comparisonStrategy = comparisonStrategyResolver.resolveComparisonStrategy(arrayNode);
                if (comparisonStrategy == null) {
                    compareInternally(arrayNode, arrayInstances, identityStrategy);
                } else {
                    compareUsingComparisonStrategy(arrayNode, arrayInstances, comparisonStrategy);
                }
            }
            return arrayNode;
        }

        private static Collection<?> arrayAsCollection(Object object) {
            return object == null ? Lists.newArrayList() : Lists.newArrayList((Object[]) object);
        }

        private static DiffNode newNode(DiffNode parentNode, Instances arrayInstances) {
            final Accessor accessor = arrayInstances.getSourceAccessor();
            final Class<?> type = arrayInstances.getType();
            return new DiffNode(parentNode, accessor, type);
        }

        private void compareItems(final DiffNode arrayNode,
                                  final Instances arrayInstances,
                                  final Iterable<?> items,
                                  final IdentityStrategy identityStrategy) {
            for (final Object item : items) {
                final Accessor itemAccessor = new ArrayItemAccessor(item, identityStrategy);
                differDispatcher.dispatch(arrayNode, arrayInstances, itemAccessor);
            }
        }

        private void compareInternally(final DiffNode arrayNode,
                                       final Instances arrayInstances,
                                       final IdentityStrategy identityStrategy) {
            final Collection<?> working = arrayAsCollection(arrayInstances.getWorking());
            final Collection<?> base = arrayAsCollection(arrayInstances.getBase());

            final Iterable<?> added = new LinkedList<Object>(working);
            final Iterable<?> removed = new LinkedList<Object>(base);
            final Iterable<?> known = new LinkedList<Object>(base);

            remove(added, base, identityStrategy);
            remove(removed, working, identityStrategy);
            remove(known, added, identityStrategy);
            remove(known, removed, identityStrategy);

            compareItems(arrayNode, arrayInstances, added, identityStrategy);
            compareItems(arrayNode, arrayInstances, removed, identityStrategy);
            compareItems(arrayNode, arrayInstances, known, identityStrategy);
        }

        private static void compareUsingComparisonStrategy(final DiffNode arrayNode,
                                                           final Instances arrayInstances,
                                                           final ComparisonStrategy comparisonStrategy) {
            comparisonStrategy.compare(arrayNode,
                    arrayInstances.getType(),
                    arrayInstances.getWorking(Collection.class),
                    arrayInstances.getBase(Collection.class));
        }

        private void remove(final Iterable<?> from, final Iterable<?> these, final IdentityStrategy identityStrategy) {
            final Iterator<?> iterator = from.iterator();
            while (iterator.hasNext()) {
                final Object item = iterator.next();
                if (contains(these, item, identityStrategy)) {
                    iterator.remove();
                }
            }
        }

        private boolean contains(final Iterable<?> haystack, final Object needle, final IdentityStrategy identityStrategy) {
            for (final Object item : haystack) {
                if (identityStrategy.equals(needle, item)) {
                    return true;
                }
            }
            return false;
        }
    }

    protected static class ArrayItemAccessor implements TypeAwareAccessor, Accessor {
        private final Object referenceItem;
        private final IdentityStrategy identityStrategy;

        /**
         * Default implementation uses IdentityService.EQUALS_IDENTITY_STRATEGY.
         */
        public ArrayItemAccessor(final Object referenceItem) {
            this(referenceItem, EqualsIdentityStrategy.getInstance());
        }

        /**
         * Allows for custom IdentityStrategy.
         */
        public ArrayItemAccessor(final Object referenceItem,
                                 final IdentityStrategy identityStrategy) {
            Assert.notNull(identityStrategy, "identityStrategy");
            this.referenceItem = referenceItem;
            this.identityStrategy = identityStrategy;
        }

        public Class<?> getType() {
            return referenceItem != null ? referenceItem.getClass() : null;
        }

        @Override
        public String toString() {
            return "Array item " + getElementSelector();
        }

        public ElementSelector getElementSelector() {
            final CollectionItemElementSelector selector = new CollectionItemElementSelector(referenceItem);
            return identityStrategy == null ? selector : selector.copyWithIdentityStrategy(identityStrategy);
        }

        public Object get(final Object target) {
            final Collection<?> targetCollection = arrayAsCollection(target);
            if (targetCollection == null) {
                return null;
            }
            for (final Object item : targetCollection) {
                if (item != null && identityStrategy.equals(item, referenceItem)) {
                    return item;
                }
            }
            return null;
        }

        public void set(final Object target, final Object value) {
            final Collection<Object> targetCollection = arrayAsCollection(target);
            if (targetCollection == null) {
                return;
            }
            final Object previous = get(target);
            if (previous != null) {
                unset(target);
            }
            targetCollection.add(value);
        }

        private static Collection<Object> arrayAsCollection(final Object object) {
            if (object == null) {
                return null;
            } else if (object.getClass().isArray()) {
                return Lists.newArrayList((Object[]) object);
            }
            throw new IllegalArgumentException(object.getClass().toString());
        }

        public void unset(final Object target) {
            final Collection<?> targetCollection = arrayAsCollection(target);
            if (targetCollection == null) {
                return;
            }
            final Iterator<?> iterator = targetCollection.iterator();
            while (iterator.hasNext()) {
                final Object item = iterator.next();
                if (item != null && identityStrategy.equals(item, referenceItem)) {
                    iterator.remove();
                    break;
                }
            }
        }
    }
}

二、用例

2.1 系统配置

yaml 复制代码
biz:
  logger:
    return-value-key: _return
    throwable-key: _throwable
    diff-config:
      words:
        of: "的"

2.2 Java Bean

java 复制代码
/**
 * @author hp
 */
@Data
public class OrderDTO {

    private Long id;

    private Long orderId;
    private String orderNo;
    @BizDiffField(alias = "消费者", value = "#{#BUYER_NAME(purchaseName)}")
    private String purchaseName;
    @BizDiffField(alias = "产品")
    private String productName;
    private Instant createTime = Instant.now();
    @BizDiffField(alias = "创建人")
    private UserDO creator;
    @BizDiffField(alias = "修改人")
    private UserDO updater;
    @BizDiffField(alias = "订单项")
    private List<String> items;

    private String[] extInfo;

    @BizDiffField(ignored = true)
    private List<UserDO> users;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class UserDO {
        private Long userId;
        private String userName;
    }
}

2.3 业务接口

java 复制代码
    @BizLogger(
            title = "修改订单2",
            bizNo = "#{#order.orderNo}",
            type = "order",
            subType = "subType",
            scope = "MANAGER_VIEW",
            errorLog = "修改订单失败,失败原因:#{#_throwable.message}",
            successLog = "[#{#order.purchaseName}]下了一个订单. 订单名称: [#{#ORDER_NAME(#order.orderId)}], 购买商品: [#{#order.productName}], 修改结果: [#{#_return}]",
            diff = @BizDiffer(before = "#order", after = "#changedOrder")
    )
    @Override
    public boolean updateOrder(OrderDTO order) {

        final OrderDTO orderDTO = new OrderDTO();
        orderDTO.setOrderId(order.getOrderId());
        orderDTO.setOrderNo(order.getOrderNo());
        orderDTO.setProductName("MacBook");
        orderDTO.setPurchaseName("Tim Cook");
        final ArrayList<String> items = Lists.newArrayList(order.getItems());
        items.remove("item1");
        items.add("item3");
        items.add("item4");
        orderDTO.setItems(items);
        orderDTO.setCreateTime(Instant.now().plus(1, ChronoUnit.DAYS));
        final OrderDTO.UserDO updater = new OrderDTO.UserDO();
        updater.setUserId(1L);
        updater.setUserName("Steve Jobs");
        orderDTO.setUpdater(updater);

        orderDTO.setExtInfo(new String[]{"extraInfo"});

        orderDTO.setUsers(Lists.newArrayList(new OrderDTO.UserDO(22L,"22User")));

        BizLoggerContext.putVariable("changedOrder", orderDTO);
        return true;
    }

2.4 测试方法

java 复制代码
@SpringBootTest(classes = Application.class)
public class BizLoggerTests {

    @Resource
    IOrderService orderService;

    OrderDTO orderDTO;

    @BeforeEach
    public void createOrderDto() {
        // given
        orderDTO = new OrderDTO();
        orderDTO.setOrderId(RandomUtil.randomLong(100));
        orderDTO.setOrderNo(UUID.fastUUID().toString());
        orderDTO.setProductName("IPhone");
        orderDTO.setPurchaseName("User001");

        orderDTO.setItems(Lists.newArrayList("item1","item2"));
    }
    
    @Test
    public void givenOrder_whenUpdateOrder_thenCreateDiffLogs() {
        // given
        final OrderDTO order = this.orderDTO;

        // then
        assertThatNoException().isThrownBy(() -> orderService.updateOrder(order));
    }
}

2.4.1 输出BizLogDTO日志对象

json 复制代码
{
  "tenant": "com.luban.biz",
  "title": "修改订单2",
  "bizNo": "f8e3617c-3f3c-4c0b-a110-f8156ae1c4a9",
  "type": "order",
  "subType": "subType",
  "scope": "MANAGER_VIEW",
  "action": "[User001]下了一个订单. 订单名称: [静态方法订单名称:15], 购买商品: [IPhone], 修改结果: [true]",
  "operator": {
    "operatorId": "FakeID",
    "operatorName": "FakeName"
  },
  "operatedAt": 1721181641076,
  "succeed": true,
  "diffs": [
    {
      "attr": "epochSecond",
      "name": "createTime的epochSecond",
      "actions": [
        {
          "state": "ADDED",
          "before": "1721181641",
          "parsedBefore": "1721181641",
          "after": "1721268041",
          "parsedAfter": "1721268041"
        }
      ]
    },
    {
      "attr": "nano",
      "name": "createTime的nano",
      "actions": [
        {
          "state": "CHANGED",
          "before": "37026000",
          "parsedBefore": "37026000",
          "after": "64216000",
          "parsedAfter": "64216000"
        }
      ]
    },
    {
      "attr": "extInfo",
      "name": "extInfo",
      "actions": [
        {
          "state": "ADDED",
          "after": "extraInfo"
        }
      ]
    },
    {
      "attr": "items",
      "name": "订单项",
      "actions": [
        {
          "state": "ADDED",
          "after": "item3,item4"
        },
        {
          "state": "REMOVED",
          "before": "item1"
        }
      ]
    },
    {
      "attr": "productName",
      "name": "产品",
      "actions": [
        {
          "state": "CHANGED",
          "before": "IPhone",
          "parsedBefore": "IPhone",
          "after": "MacBook",
          "parsedAfter": "MacBook"
        }
      ]
    },
    {
      "attr": "purchaseName",
      "name": "消费者",
      "actions": [
        {
          "state": "CHANGED",
          "before": "User001",
          "parsedBefore": "BuyName:User001",
          "after": "Tim Cook",
          "parsedAfter": "BuyName:Tim Cook"
        }
      ]
    },
    {
      "attr": "userId",
      "name": "修改人的userId",
      "actions": [
        {
          "state": "ADDED",
          "after": "1",
          "parsedAfter": "1"
        }
      ]
    },
    {
      "attr": "userName",
      "name": "修改人的userName",
      "actions": [
        {
          "state": "ADDED",
          "after": "Steve Jobs",
          "parsedAfter": "Steve Jobs"
        }
      ]
    }
  ],
  "invocationInfo": {
    "targetClass": "class com.luban.biz.service.OrderServiceImpl",
    "method": "public boolean com.luban.biz.service.OrderServiceImpl.updateOrder(com.luban.biz.bean.OrderDTO)",
    "args": [
      {
        "orderId": 15,
        "orderNo": "f8e3617c-3f3c-4c0b-a110-f8156ae1c4a9",
        "purchaseName": "User001",
        "productName": "IPhone",
        "createTime": 1721181641037,
        "items": [
          "item1",
          "item2"
        ]
      }
    ],
    "success": true,
    "result": true,
    "timeCost": 0
  }
}
相关推荐
明月望秋思2 小时前
力扣面试经典算法150题:除自身以外数组的乘积
java·算法·leetcode·面试
项目題供诗3 小时前
尚品汇-订单接口实现(四十)
java
此去经年ToT4 小时前
登录失败时刷新验证码
java·前端
赛博末影猫5 小时前
Spring理论知识(Ⅱ)——Spring核心容器模块
java·spring·spring-context·spring core模块·spring-beans·spring-core
weixin_463217615 小时前
给自己复盘的随想录笔记-链表
java·数据结构·笔记·链表
夜月行者6 小时前
如何使用ssm实现基于JAVA的网上药品售卖系统
java·后端·ssm
听雨~~~8 小时前
线程面试题
java·jvm·面试
小北5348 小时前
JVM的内存模型和垃圾回收
java·服务器·jvm
舍予大可8 小时前
尚硅谷Java面试题第四季-Java基本功
java·面试
啵啵薯条8 小时前
面试题Java版(含大厂校招面试题)
java·面试