diff --git a/bizlog-sdk/src/main/java/com/mzt/logapi/service/impl/DiffParseFunction.java b/bizlog-sdk/src/main/java/com/mzt/logapi/service/impl/DiffParseFunction.java index 43e17fe..3c7357f 100644 --- a/bizlog-sdk/src/main/java/com/mzt/logapi/service/impl/DiffParseFunction.java +++ b/bizlog-sdk/src/main/java/com/mzt/logapi/service/impl/DiffParseFunction.java @@ -2,7 +2,9 @@ import com.mzt.logapi.context.LogRecordContext; import com.mzt.logapi.starter.diff.IDiffItemsToLogContentService; +import com.mzt.logapi.util.diff.ArrayDiffer; import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.comparison.ComparisonService; import de.danielbechler.diff.node.DiffNode; import lombok.extern.slf4j.Slf4j; @@ -43,7 +45,12 @@ public String diff(Object source, Object target) { log.error("diff的两个对象类型不同, source.class={}, target.class={}", source.getClass().toString(), target.getClass().toString()); return ""; } - DiffNode diffNode = ObjectDifferBuilder.buildDefault().compare(target, source); + ObjectDifferBuilder objectDifferBuilder = ObjectDifferBuilder.startBuilding(); + DiffNode diffNode = objectDifferBuilder + .differs().register((differDispatcher, nodeQueryService) -> + new ArrayDiffer(differDispatcher, (ComparisonService) objectDifferBuilder.comparison(), objectDifferBuilder.identity())) + .build() + .compare(target, source); return diffItemsToLogContentService.toLogContent(diffNode, source, target); } diff --git a/bizlog-sdk/src/main/java/com/mzt/logapi/starter/diff/DefaultDiffItemsToLogContentService.java b/bizlog-sdk/src/main/java/com/mzt/logapi/starter/diff/DefaultDiffItemsToLogContentService.java index 5b4853d..eba96a8 100644 --- a/bizlog-sdk/src/main/java/com/mzt/logapi/starter/diff/DefaultDiffItemsToLogContentService.java +++ b/bizlog-sdk/src/main/java/com/mzt/logapi/starter/diff/DefaultDiffItemsToLogContentService.java @@ -17,6 +17,7 @@ import org.springframework.util.StringUtils; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; /** @@ -61,11 +62,11 @@ private void generateAllFieldLog(Object sourceObject, Object targetObject, Strin if (StringUtils.isEmpty(filedLogName)) { return; } - //是否是List类型的字段 - boolean valueIsCollection = valueIsCollection(node, sourceObject, targetObject); + // 是否是容器类型的字段 + boolean valueIsContainer = valueIsContainer(node, sourceObject, targetObject); //获取值的转换函数 DiffNode.State state = node.getState(); - String logContent = getDiffLogContent(filedLogName, node, state, sourceObject, targetObject, diffLogFieldAnnotation.function(), valueIsCollection); + String logContent = getDiffLogContent(filedLogName, node, state, sourceObject, targetObject, diffLogFieldAnnotation.function(), valueIsContainer); if (!StringUtils.isEmpty(logContent)) { stringBuilder.append(logContent).append(logRecordProperties.getFieldSeparator()); } @@ -80,15 +81,16 @@ private String getFieldLogName(DiffNode node, DiffLogField diffLogFieldAnnotatio return filedLogName; } - private boolean valueIsCollection(DiffNode node, Object sourceObject, Object targetObject) { + private boolean valueIsContainer(DiffNode node, Object sourceObject, Object targetObject) { if (sourceObject != null) { Object sourceValue = node.canonicalGet(sourceObject); if (sourceValue == null) { if (targetObject != null) { - return node.canonicalGet(targetObject) instanceof Collection; + return node.canonicalGet(targetObject) instanceof Collection || node.canonicalGet(targetObject).getClass().isArray(); } + } else { + return sourceValue instanceof Collection || sourceValue.getClass().isArray(); } - return sourceValue instanceof Collection; } return false; } @@ -137,6 +139,9 @@ public String getDiffLogContent(String filedLogName, DiffNode node, DiffNode.Sta private Collection getListValue(DiffNode node, Object object) { Object fieldSourceValue = getFieldValue(node, object); //noinspection unchecked + if (fieldSourceValue != null && fieldSourceValue.getClass().isArray()) { + return new ArrayList<>(Arrays.asList((Object[]) fieldSourceValue)); + } return fieldSourceValue == null ? Lists.newArrayList() : (Collection) fieldSourceValue; } diff --git a/bizlog-sdk/src/main/java/com/mzt/logapi/util/diff/ArrayDiffer.java b/bizlog-sdk/src/main/java/com/mzt/logapi/util/diff/ArrayDiffer.java new file mode 100644 index 0000000..54514b8 --- /dev/null +++ b/bizlog-sdk/src/main/java/com/mzt/logapi/util/diff/ArrayDiffer.java @@ -0,0 +1,135 @@ +package com.mzt.logapi.util.diff; + +import de.danielbechler.diff.access.Accessor; +import de.danielbechler.diff.access.Instances; +import de.danielbechler.diff.comparison.ComparisonStrategy; +import de.danielbechler.diff.comparison.ComparisonStrategyResolver; +import de.danielbechler.diff.differ.Differ; +import de.danielbechler.diff.differ.DifferDispatcher; +import de.danielbechler.diff.identity.IdentityStrategy; +import de.danielbechler.diff.identity.IdentityStrategyResolver; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.util.Assert; + +import java.util.*; + +/** + * @author: wulang + * @date: 2022-08-27 20:26 + **/ +public class ArrayDiffer implements Differ { + private final DifferDispatcher differDispatcher; + private final ComparisonStrategyResolver comparisonStrategyResolver; + private final IdentityStrategyResolver identityStrategyResolver; + + public ArrayDiffer(DifferDispatcher differDispatcher, ComparisonStrategyResolver comparisonStrategyResolver, 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(final Class type) { + return !type.isPrimitive() && type.isArray(); + } + + @Override + public final DiffNode compare(final DiffNode parentNode, final Instances collectionInstances) { + final DiffNode collectionNode = newNode(parentNode, collectionInstances); + final IdentityStrategy identityStrategy = identityStrategyResolver.resolveIdentityStrategy(collectionNode); + if (identityStrategy != null) { + collectionNode.setChildIdentityStrategy(identityStrategy); + } + if (collectionInstances.hasBeenAdded()) { + final Collection addedItems = findCollection(collectionInstances.getWorking()); + compareItems(collectionNode, collectionInstances, addedItems, identityStrategy); + collectionNode.setState(DiffNode.State.ADDED); + } else if (collectionInstances.hasBeenRemoved()) { + final Collection removedItems = findCollection(collectionInstances.getBase()); + compareItems(collectionNode, collectionInstances, removedItems, identityStrategy); + collectionNode.setState(DiffNode.State.REMOVED); + } else if (collectionInstances.areSame()) { + collectionNode.setState(DiffNode.State.UNTOUCHED); + } else { + final ComparisonStrategy comparisonStrategy = comparisonStrategyResolver.resolveComparisonStrategy(collectionNode); + if (comparisonStrategy == null) { + compareInternally(collectionNode, collectionInstances, identityStrategy); + } else { + compareUsingComparisonStrategy(collectionNode, collectionInstances, comparisonStrategy); + } + } + return collectionNode; + } + + private Collection findCollection(Object source) { + return source == null ? new ArrayList<>() : new LinkedList<>(Arrays.asList((Object[]) source)); + } + + private static DiffNode newNode(final DiffNode parentNode, + final Instances collectionInstances) { + final Accessor accessor = collectionInstances.getSourceAccessor(); + final Class type = collectionInstances.getType(); + return new DiffNode(parentNode, accessor, type); + } + + private void compareItems(final DiffNode collectionNode, + final Instances collectionInstances, + final Iterable items, + final IdentityStrategy identityStrategy) { + for (final Object item : items) { + final Accessor itemAccessor = new ArrayItemAccessor(item, identityStrategy); + differDispatcher.dispatch(collectionNode, collectionInstances, itemAccessor); + } + } + + private void compareInternally(final DiffNode collectionNode, + final Instances collectionInstances, + final IdentityStrategy identityStrategy) { + final Collection working = Arrays.asList((Object[]) collectionInstances.getWorking()); + final Collection base = Arrays.asList((Object[]) collectionInstances.getBase()); + + final Iterable added = new LinkedList(working); + final Iterable removed = new LinkedList(base); + final Iterable known = new LinkedList(base); + + remove(added, base, identityStrategy); + remove(removed, working, identityStrategy); + remove(known, added, identityStrategy); + remove(known, removed, identityStrategy); + + compareItems(collectionNode, collectionInstances, added, identityStrategy); + compareItems(collectionNode, collectionInstances, removed, identityStrategy); + compareItems(collectionNode, collectionInstances, known, identityStrategy); + } + + private static void compareUsingComparisonStrategy(final DiffNode collectionNode, + final Instances collectionInstances, + final ComparisonStrategy comparisonStrategy) { + comparisonStrategy.compare(collectionNode, + collectionInstances.getType(), + collectionInstances.getWorking(Collection.class), + collectionInstances.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; + } +} diff --git a/bizlog-sdk/src/main/java/com/mzt/logapi/util/diff/ArrayItemAccessor.java b/bizlog-sdk/src/main/java/com/mzt/logapi/util/diff/ArrayItemAccessor.java new file mode 100644 index 0000000..0fdae31 --- /dev/null +++ b/bizlog-sdk/src/main/java/com/mzt/logapi/util/diff/ArrayItemAccessor.java @@ -0,0 +1,115 @@ +package com.mzt.logapi.util.diff; + +import de.danielbechler.diff.access.Accessor; +import de.danielbechler.diff.access.TypeAwareAccessor; +import de.danielbechler.diff.identity.EqualsIdentityStrategy; +import de.danielbechler.diff.identity.IdentityStrategy; +import de.danielbechler.diff.selector.CollectionItemElementSelector; +import de.danielbechler.diff.selector.ElementSelector; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; + +/** + * @author: wulang + * @date: 2022-08-27 20:34 + **/ +public class ArrayItemAccessor implements TypeAwareAccessor, Accessor { + + private final Object referenceItem; + private final IdentityStrategy identityStrategy; + + /** + * Default implementation uses IdentityService.EQUALS_IDENTITY_STRATEGY. + * + * @param referenceItem + */ + public ArrayItemAccessor(final Object referenceItem) { + this(referenceItem, EqualsIdentityStrategy.getInstance()); + } + + /** + * Allows for custom IdentityStrategy. + * + * @param referenceItem + * @param identityStrategy + */ + public ArrayItemAccessor(final Object referenceItem, + final IdentityStrategy identityStrategy) { + Assert.notNull(identityStrategy, "identityStrategy"); + this.referenceItem = referenceItem; + this.identityStrategy = identityStrategy; + } + + @Override + public Class getType() { + return referenceItem != null ? referenceItem.getClass() : null; + } + + @Override + public String toString() { + return "collection item " + getElementSelector(); + } + + @Override + public ElementSelector getElementSelector() { + final CollectionItemElementSelector selector = new CollectionItemElementSelector(referenceItem); + return identityStrategy == null ? selector : selector.copyWithIdentityStrategy(identityStrategy); + } + + @Override + public Object get(final Object target) { + final Collection targetCollection = objectAsCollection(target); + if (targetCollection == null) { + return null; + } + for (final Object item : targetCollection) { + if (item != null && identityStrategy.equals(item, referenceItem)) { + return item; + } + } + return null; + } + + @Override + public void set(final Object target, final Object value) { + final Collection targetCollection = objectAsCollection(target); + if (targetCollection == null) { + return; + } + final Object previous = get(target); + if (previous != null) { + unset(target); + } + targetCollection.add(value); + } + + @SuppressWarnings("unchecked") + private static Collection objectAsCollection(final Object object) { + if (object == null) { + return null; + } else if (object.getClass().isArray()) { + return new ArrayList<>(Arrays.asList((Object[]) object)); + } + throw new IllegalArgumentException(object.getClass().toString()); + } + + @Override + public void unset(final Object target) { + final Collection targetCollection = objectAsCollection(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; + } + } + } +} diff --git a/bizlog-server/src/main/java/com/mzt/logserver/function/ExtInfoParseFunction.java b/bizlog-server/src/main/java/com/mzt/logserver/function/ExtInfoParseFunction.java new file mode 100644 index 0000000..bd3cce9 --- /dev/null +++ b/bizlog-server/src/main/java/com/mzt/logserver/function/ExtInfoParseFunction.java @@ -0,0 +1,34 @@ +package com.mzt.logserver.function; + +import com.mzt.logapi.service.IParseFunction; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +/** + * @author: wulang + * @date: 2022-08-27 21:01 + **/ +@Slf4j +@Component +public class ExtInfoParseFunction implements IParseFunction { + @Override + public boolean executeBefore() { + return false; + } + + @Override + public String functionName() { + return "extInfo"; + } + + @Override + public String apply(Object value) { + log.info("==========="); + if (StringUtils.isEmpty(value)) { + return ""; + } + log.info("当前拓展信息值为,{}", value); + return value.toString(); + } +} diff --git a/bizlog-server/src/main/java/com/mzt/logserver/pojo/Order.java b/bizlog-server/src/main/java/com/mzt/logserver/pojo/Order.java index fe7e516..e3c8e84 100644 --- a/bizlog-server/src/main/java/com/mzt/logserver/pojo/Order.java +++ b/bizlog-server/src/main/java/com/mzt/logserver/pojo/Order.java @@ -32,6 +32,9 @@ public class Order { @DiffLogField(name = "列表项", function = "ORDER") private List items; + @DiffLogField(name = "拓展信息", function = "extInfo") + private String[] extInfo; + @Data public static class UserDO { @DiffLogField(name = "用户ID") diff --git a/bizlog-server/src/test/java/com/mzt/logserver/IOrderServiceTest.java b/bizlog-server/src/test/java/com/mzt/logserver/IOrderServiceTest.java index 0d0e074..2a1f030 100644 --- a/bizlog-server/src/test/java/com/mzt/logserver/IOrderServiceTest.java +++ b/bizlog-server/src/test/java/com/mzt/logserver/IOrderServiceTest.java @@ -342,6 +342,118 @@ public void testDiff2() { logRecordService.clean(); } + @Test + @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + public void testCondition_数组修改() { + Order order = new Order(); + order.setOrderId(99L); + order.setOrderNo("MT0000099"); + order.setProductName("超值优惠红烧肉套餐"); + order.setPurchaseName("张三"); + Order.UserDO userDO = new Order.UserDO(); + userDO.setUserId(9001L); + userDO.setUserName("用户1"); + order.setCreator(userDO); + order.setItems(Lists.newArrayList("123", "bbb")); + order.setExtInfo(new String[]{"p", "k"}); + + Order order1 = new Order(); + order1.setOrderId(88L); + order1.setOrderNo("MT0000099"); + order1.setProductName("麻辣烫套餐"); + order1.setPurchaseName("赵四"); + Order.UserDO userDO1 = new Order.UserDO(); + userDO1.setUserId(9002L); + userDO1.setUserName("用户2"); + order1.setCreator(userDO1); + order1.setItems(Lists.newArrayList("123", "aaa")); + order1.setExtInfo(new String[]{"q", "k"}); + orderService.diff(order, order1); + + List logRecordList = logRecordService.queryLog(order.getOrderNo(), LogRecordType.ORDER); + Assert.assertEquals(1, logRecordList.size()); + LogRecord logRecord = logRecordList.get(0); + Assert.assertEquals(logRecord.getAction(), "更新了订单【创建人的用户ID】从【9001】修改为【9002】;【创建人的用户姓名】从【用户1】修改为【用户2】;【拓展信息】添加了【q】删除了【p】;【列表项】添加了【xxxx(aaa)】删除了【xxxx(bbb)】;【订单ID】从【xxxx(99)】修改为【xxxx(88)】"); + Assert.assertNotNull(logRecord.getExtra()); + Assert.assertEquals(logRecord.getOperator(), "111"); + Assert.assertEquals(logRecord.getBizNo(), order1.getOrderNo()); + logRecordService.clean(); + } + + @Test + @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + public void testCondition_数组增加() { + Order order = new Order(); + order.setOrderId(99L); + order.setOrderNo("MT0000099"); + order.setProductName("超值优惠红烧肉套餐"); + order.setPurchaseName("张三"); + Order.UserDO userDO = new Order.UserDO(); + userDO.setUserId(9001L); + userDO.setUserName("用户1"); + order.setCreator(userDO); + order.setItems(Lists.newArrayList("123", "bbb")); + + Order order1 = new Order(); + order1.setOrderId(88L); + order1.setOrderNo("MT0000099"); + order1.setProductName("麻辣烫套餐"); + order1.setPurchaseName("赵四"); + Order.UserDO userDO1 = new Order.UserDO(); + userDO1.setUserId(9002L); + userDO1.setUserName("用户2"); + order1.setCreator(userDO1); + order1.setItems(Lists.newArrayList("123", "aaa")); + order1.setExtInfo(new String[]{"q", "k"}); + orderService.diff(order, order1); + + List logRecordList = logRecordService.queryLog(order.getOrderNo(), LogRecordType.ORDER); + Assert.assertEquals(1, logRecordList.size()); + LogRecord logRecord = logRecordList.get(0); + Assert.assertEquals(logRecord.getAction(), "更新了订单【创建人的用户ID】从【9001】修改为【9002】;【创建人的用户姓名】从【用户1】修改为【用户2】;【拓展信息】添加了【q,k】;【列表项】添加了【xxxx(aaa)】删除了【xxxx(bbb)】;【订单ID】从【xxxx(99)】修改为【xxxx(88)】"); + Assert.assertNotNull(logRecord.getExtra()); + Assert.assertEquals(logRecord.getOperator(), "111"); + Assert.assertEquals(logRecord.getBizNo(), order1.getOrderNo()); + logRecordService.clean(); + } + + @Test + @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + public void testCondition_数组删除() { + Order order = new Order(); + order.setOrderId(99L); + order.setOrderNo("MT0000099"); + order.setProductName("超值优惠红烧肉套餐"); + order.setPurchaseName("张三"); + Order.UserDO userDO = new Order.UserDO(); + userDO.setUserId(9001L); + userDO.setUserName("用户1"); + order.setCreator(userDO); + order.setItems(Lists.newArrayList("123", "bbb")); + order.setExtInfo(new String[]{"p", "k"}); + + Order order1 = new Order(); + order1.setOrderId(88L); + order1.setOrderNo("MT0000099"); + order1.setProductName("麻辣烫套餐"); + order1.setPurchaseName("赵四"); + Order.UserDO userDO1 = new Order.UserDO(); + userDO1.setUserId(9002L); + userDO1.setUserName("用户2"); + order1.setCreator(userDO1); + order1.setItems(Lists.newArrayList("123", "aaa")); + orderService.diff(order, order1); + + List logRecordList = logRecordService.queryLog(order.getOrderNo(), LogRecordType.ORDER); + Assert.assertEquals(1, logRecordList.size()); + LogRecord logRecord = logRecordList.get(0); + Assert.assertEquals(logRecord.getAction(), "更新了订单【创建人的用户ID】从【9001】修改为【9002】;【创建人的用户姓名】从【用户1】修改为【用户2】;【拓展信息】删除了【p,k】;【列表项】添加了【xxxx(aaa)】删除了【xxxx(bbb)】;【订单ID】从【xxxx(99)】修改为【xxxx(88)】"); + Assert.assertNotNull(logRecord.getExtra()); + Assert.assertEquals(logRecord.getOperator(), "111"); + Assert.assertEquals(logRecord.getBizNo(), order1.getOrderNo()); + logRecordService.clean(); + } + @Test @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testCondition_打印日志() { diff --git a/bizlog-server/src/test/resources/sql/create.sql b/bizlog-server/src/test/resources/sql/create.sql index f6be026..18ebbb4 100644 --- a/bizlog-server/src/test/resources/sql/create.sql +++ b/bizlog-server/src/test/resources/sql/create.sql @@ -1,16 +1,15 @@ create table t_logrecord ( `id` bigint(11) unsigned not null auto_increment comment 'id', - `tenant` varchar(63) not null default '' comment '租户标识', - `type` varchar(63) not null default '' comment '保存的操作日志的类型,比如:订单类型、商品类型', - `sub_type` varchar(63) not null default '' comment '日志的子类型,比如订单的C端日志,和订单的B端日志,type都是订单类型,但是子类型不一样', - `biz_no` varchar(63) not null default '' comment '日志绑定的业务标识', - `operator` varchar(63) not null default '' comment '操作人', - `action` varchar(1023) not null default '' comment '日志内容', + `tenant` varchar(63) not null default '' comment '租户标识', + `type` varchar(63) not null default '' comment '保存的操作日志的类型,比如:订单类型、商品类型', + `sub_type` varchar(63) not null default '' comment '日志的子类型,比如订单的C端日志,和订单的B端日志,type都是订单类型,但是子类型不一样', + `biz_no` varchar(63) not null default '' comment '日志绑定的业务标识', + `operator` varchar(63) not null default '' comment '操作人', + `action` varchar(1023) not null default '' comment '日志内容', `fail` tinyint(1) unsigned not null default 0 comment '记录是否是操作失败的日志', - `create_time` datetime(3) not null default current_timestamp(3) comment '创建时间', - `extra` varchar(2000) not null default '' comment '扩展信息', - `code_variable` varchar(2000) not null default '' comment '代码变量信息', + `create_time` datetime(3) not null default current_timestamp (3) comment '创建时间', + `extra` varchar(2000) not null default '' comment '扩展信息', + `code_variable` varchar(2000) not null default '' comment '代码变量信息', primary key (id) -) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 comment '操作日志表'; \ No newline at end of file +); \ No newline at end of file