Repository: hs-web/hsweb-framework
Pull request #310: fix: 修复TreeUtils.list2tree转换可能失效问题
Author: login: kyouji
Created: 2025-03-12T11:53:41Z | Merged: 2025-03-14T02:18:45Z

当传入的List是一个可变集合时：每次遍历生成新的值。由于遍历了2次dataList，会导致子节点设置失效。


Diff:
diff --git a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeUtils.java b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeUtils.java
index 5d0b46e55..2c3e7f123 100644
--- a/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeUtils.java
+++ b/hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeUtils.java
@@ -105,13 +105,13 @@ public static <N, PK> List<N> list2tree(Collection<N> dataList,
             return new ArrayList<>(0);
         }
         // id,node
-        Map<PK, N> cache = Maps.newHashMapWithExpectedSize(size);
+        Map<PK, N> cache = Maps.newLinkedHashMapWithExpectedSize(size);
         // parentId,children
         Map<PK, List<N>> treeCache = dataList
-            .stream()
-            .peek(node -> cache.put(idGetter.apply(node), node))
-            .filter(e -> parentIdGetter.apply(e) != null)
-            .collect(Collectors.groupingBy(parentIdGetter));
+                .stream()
+                .peek(node -> cache.put(idGetter.apply(node), node))
+                .filter(e -> parentIdGetter.apply(e) != null)
+                .collect(Collectors.groupingBy(parentIdGetter));
 
         TreeSupportEntity.TreeHelper<N, PK> helper = new TreeSupportEntity.TreeHelper<N, PK>() {
             @Override
@@ -127,7 +127,7 @@ public N getNode(PK id) {
 
         List<N> list = new ArrayList<>(treeCache.size());
 
-        for (N node : dataList) {
+        for (N node : cache.values()) {
             //设置每个节点的子节点
             childConsumer.accept(node, treeCache.get(idGetter.apply(node)));
 
@@ -139,4 +139,5 @@ public N getNode(PK id) {
         return list;
     }
 
+
 }
diff --git a/hsweb-commons/hsweb-commons-api/src/test/java/org/hswebframework/web/api/crud/entity/TreeUtilsTest.java b/hsweb-commons/hsweb-commons-api/src/test/java/org/hswebframework/web/api/crud/entity/TreeUtilsTest.java
index 670475f9d..3f6fc995c 100644
--- a/hsweb-commons/hsweb-commons-api/src/test/java/org/hswebframework/web/api/crud/entity/TreeUtilsTest.java
+++ b/hsweb-commons/hsweb-commons-api/src/test/java/org/hswebframework/web/api/crud/entity/TreeUtilsTest.java
@@ -1,14 +1,18 @@
 package org.hswebframework.web.api.crud.entity;
 
+import com.google.common.collect.Collections2;
 import lombok.Getter;
 import lombok.Setter;
+import org.apache.commons.collections4.CollectionUtils;
 import org.junit.Test;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 
 public class TreeUtilsTest {
 
@@ -28,12 +32,49 @@ public void testTreeToList() {
 
     }
 
+    @Test
+    public void testListToTree() {
+        int size = 5;
+        List<Node> nodes = new ArrayList<>(size);
+        for (int i = 0; i < size; i++) {
+            Node node = new Node();
+            node.setId(String.valueOf(i));
+            node.setParenTId(i == 0 ? null : String.valueOf(i - 1));
+            nodes.add(node);
+        }
+        // 打乱顺序
+        Collections.shuffle(nodes);
+
+        // 并发执行，并且创建新的节点
+        List<Node> tree = TreeUtils
+                .list2tree(Collections2.transform(nodes, e -> {
+                               Node copy = new Node();
+                               copy.setId(e.id);
+                               copy.setParenTId(e.parenTId);
+                               copy.setChildren(e.children);
+                               return copy;
+                           }),
+                           Node::getId,
+                           Node::getParenTId,
+                           Node::setChildren,
+                           // 自定义根节点判断
+                           (helper, e) -> "2".contains(e.getId()));
+        assertNotNull(tree);
+        Node children = tree.get(0);
+        assertNotNull(children);
+        while (CollectionUtils.isNotEmpty(children.getChildren())) {
+            children = children.getChildren().get(0);
+        }
+        assertEquals("4", children.getId());
+    }
 
     @Getter
     @Setter
     static class Node {
         private String id;
 
+        private String parenTId;
+
         private List<Node> children;
     }
 }
\ No newline at end of file


Reviews:
user: login: zhou-hao
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T01:13:54Z
user: login: kyouji
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T11:01:43Z

Review comments:
diff_hunk: @@ -125,18 +132,26 @@ public N getNode(PK id) {
             }
         };
 
-        List<N> list = new ArrayList<>(treeCache.size());
+        List<N> list = new ArrayList<>(treeIdCache.size());
+
+        cache.values().forEach(node -> {
path: hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeUtils.java
user: login: zhou-hao
body: 1. 直接遍历。避免forEach() 中多行操作。
2. 单元测试
created_at: 2025-03-13T01:13:50Z
author_association: MEMBER
diff_hunk: @@ -125,18 +132,26 @@ public N getNode(PK id) {
             }
         };
 
-        List<N> list = new ArrayList<>(treeCache.size());
+        List<N> list = new ArrayList<>(treeIdCache.size());
+
+        cache.values().forEach(node -> {
path: hsweb-commons/hsweb-commons-api/src/main/java/org/hswebframework/web/api/crud/entity/TreeUtils.java
user: login: kyouji
body: 已修改。

只有单元测试里的特殊情况才会出现问题：外部使用了线程不安全的集合（Collections2.transform）。

因为集合遍历了两次，每次都会分别执行了Collections2.transform中的function。导致操作节点不是同一个。
created_at: 2025-03-13T11:01:42Z
author_association: CONTRIBUTOR

PR comments:
user: login: CLAassistant
created_at: 2025-03-12T11:53:48Z
body: [![CLA assistant check](https://cla-assistant.io/pull/badge/signed)](https://cla-assistant.io/hs-web/hsweb-framework?pullRequest=310) <br/>All committers have signed the CLA.
author_association: NONE
user: login: zhou-hao
created_at: 2025-03-14T01:38:33Z
body: 本质上是由于每次遍历List 都会创建新的对象导致的吧？ 那把cache改为LinkedHashMap，直接遍历cache的values 是不是也可以？
author_association: MEMBER
user: login: kyouji
created_at: 2025-03-14T02:12:16Z
body: > 本质上是由于每次遍历List 都会创建新的对象导致的吧？ 那把cache改为LinkedHashMap，直接遍历cache的values 是不是也可以？

是的，dataList只遍历一次就没问题了
author_association: CONTRIBUTOR