• 已删除用户
Administrator
发布于 2018-07-06 / 0 阅读
0

利用自定义注解实现List转树结构

在做组织机构功能或者菜单功能等具备树形结构功能时,我们需要对查询出来的 List 数据进行转换,使其成为具备层级的树结构。我希望能实现以下功能:

  1. 使用时越简单越好,能用一行代码搞定就坚决不用两行。

  2. 转换时支持指定节点字段名、指定父节点字段名、指定子节点字段名、指定排序字段名,不限制列表中字段的名称。

  3. 支持自动解析父节点名称到节点数据中。

  4. 支持自动拼接全节点名称,例如:XX 公司-XX 部门-XX 小组。

典型树格式说明

{
    "id":1,
    "name":"XX公司",
    "children":[
        {
            "id":11,
            "parentId":1,
            "name":"行政部"
        },
        {
            "id":12,
            "parentId":1,
            "name":"技术部",
            "children":[
                {
                    "id":121,
                    "parentId":12,
                    "name":"大前端"
                },
                {
                    "id":122,
                    "parentId":12,
                    "name":"后端组"
                },
                {
                    "id":123,
                    "parentId":12,
                    "name":"质量组"
                }
            ]
        }
    ]
}

树节点注解类

TreeNode.java

/**
 * 树节点注解
 *
 * @author zhangqin
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TreeNode {
    /**
     * 指定节点字段名
     *
     * @return
     */
    String nodeKey();

    /**
     * 指定父节点字段名
     *
     * @return
     */
    String parentKey();

    /**
     * 指定子节点字段名
     *
     * @return
     */
    String childrenKey();

    /**
     * 指定排序字段名
     *
     * @return
     */
    String sortKey() default "";

    /**
     * 指定节点名称字段名
     *
     * @return
     */
    String nodeNameKey() default "";

    /**
     * 指定父节点名称字段名
     *
     * @return
     */
    String parentNameKey() default "";

    /**
     * 指定全节点名称字段名
     *
     * @return
     */
    String fullNameKey() default "";
}

说明:

  • nodeKey
    指定节点字段名。例如:idmenuIdorganizationId,解析时以该字段作为节点的唯一 ID。

  • parentKey
    指定父节点字段名。例如:pidparentId,解析时以parentKey指定的字段作为父子级关联的 ID。

  • childrenKey
    指定子节点字段名。例如childrenlist,将解析的子集放入childrenKey指定的属性中,其指定的类型必须为List类型。

  • sortKey
    指定排序字段名。例如sortlevel,指定解析的节点列表按哪个字段进行内存排序

  • nodeNameKey
    指定节点名称字段名。配置了 parentNameKeyfullNameKey 才有意义。

  • parentNameKey
    指定父节点名称字段名,需要和nodeNameKey同时使用。例如:parentNameparentMenuNameparentOrganizationName,如需自动将父节点的名称赋值到子节点,则可通过 nodeNameKey 指定要解析的字段名、parentNameKey 指定要赋值的字段名。

  • fullNameKey
    指定全节点名称字段名,需要和nodeNameKeyparentNameKey同时使用,例如:fullNamemenuFullNameOrganizationFullName。如需显示节点全名称,如:XX公司-XX部门-XX小组,可结合nodeNameKeyparentNameKey进行配置。

树工具类

利用注解+反射+递归实现 List 解析成为具有层级的树结构。

TreeUtils.java

/**
 * 树工具
 *
 * @author zhangqin
 */
@Log4j2
public class TreeUtils {

    /**
     * 将列表构建为树结构,对象必须有@TreeNode注解
     *
     * @param list
     * @return
     */
    public static <T> List<T> buildTree(List<T> list) {
        // 列表是否为空
        if (CollectionUtils.isEmpty(list)) {
            return Lists.newArrayList();
        }

        // 是否包含@TreeNode注解
        TreeNode treeNode = list.get(0).getClass().getAnnotation(TreeNode.class);
        if (null == treeNode) {
            throw new RuntimeException(list.get(0).getClass().getSimpleName() + "未标记@TreeNode注解。");
        }

        // 递归处理树的层级结构
        List<T> routeList = recursive(null, list);
        return routeList;
    }

    /**
     * 递归处理树的层级结构
     *
     * @param appendNode
     * @param list
     * @return
     */
    private static <T> List<T> recursive(T appendNode, List<T> list) {
        // 存储有节点集合
        List<T> newList = Lists.newArrayList();

        // 查找所有子节点
        List<T> childList = list.stream().filter(item -> {
            // 注解字段名
            String nodeKey = item.getClass().getAnnotation(TreeNode.class).nodeKey();
            String parentKey = item.getClass().getAnnotation(TreeNode.class).parentKey();
            String nodeNameKey = item.getClass().getAnnotation(TreeNode.class).nodeNameKey();
            String parentNameKey = item.getClass().getAnnotation(TreeNode.class).parentNameKey();
            String fullNameKey = item.getClass().getAnnotation(TreeNode.class).fullNameKey();

            if (null == appendNode) {
                // 全名称
                if (StringUtils.isNotBlank(nodeNameKey) && StringUtils.isNotBlank(parentNameKey)) {
                    Object nodeName = getFieldValue(item, nodeNameKey);
                    String fullName = nodeName.toString();
                    setFieldValue(item, fullNameKey, fullName);
                }
                Object parentValue = getFieldValue(item, parentKey);
                return (null == parentValue || StringUtils.isBlank(parentValue.toString()) || "0".equals(parentValue.toString()));
            } else {
                // 节点与父节点值
                Object nodeKeyValue = getFieldValue(appendNode, nodeKey);
                Object parentKeyValue = getFieldValue(item, parentKey);

                // 父名称
                if (StringUtils.isNotBlank(nodeNameKey) && StringUtils.isNotBlank(parentNameKey)) {
                    Object parentName = getFieldValue(appendNode, nodeNameKey);
                    setFieldValue(item, parentNameKey, parentName);
                }

                // 全名称
                if (StringUtils.isNotBlank(nodeNameKey) && StringUtils.isNotBlank(parentNameKey)) {
                    Object parentName = getFieldValue(appendNode, fullNameKey);
                    if (null == parentName) {
                        parentName = getFieldValue(appendNode, nodeNameKey);
                    }
                    Object nodeName = getFieldValue(item, nodeNameKey);
                    String fullName = parentName + "-" + nodeName;
                    setFieldValue(item, fullNameKey, fullName);
                }
                return nodeKeyValue.equals(parentKeyValue);
            }
        }).collect(Collectors.toList());

        // 遍历所有子节点,看是否还存在子节点
        list.removeAll(childList);
        childList.forEach(item -> {
            // 子路由节点
            T node = item;

            // 是否还存在下级
            boolean hasChild = list.stream().filter(sub -> {
                String nodeKey = sub.getClass().getAnnotation(TreeNode.class).nodeKey();
                String parentKey = sub.getClass().getAnnotation(TreeNode.class).parentKey();

                Object nodeValue = getFieldValue(item, nodeKey);
                Object parentValue = getFieldValue(sub, parentKey);
                return nodeValue.equals(parentValue);
            }).count() > 0;

            // 存在子节点,则递归处理子节点
            if (hasChild) {
                List<T> childNodes = recursive(node, list);
                String childrenKey = item.getClass().getAnnotation(TreeNode.class).childrenKey();
                setFieldValue(node, childrenKey, childNodes);
            }
            newList.add(node);
        });

        // 排序
        return newList.stream().sorted(Comparator.comparing(comparing -> {
            String sortKeyName = comparing.getClass().getAnnotation(TreeNode.class).sortKey();
            if (StringUtils.isNotBlank(sortKeyName)) {
                Object sortValue = getFieldValue(comparing, sortKeyName);
                return null != sortValue ? sortValue.toString() : "";
            }
            return "";
        })).collect(Collectors.toList());
    }

    /**
     * 获取字段值
     *
     * @param object
     * @param fieldName
     * @return
     */
    private static Object getFieldValue(Object object, String fieldName) {
        try {
            Field field = object.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(object);
        } catch (Exception e) {
            log.error("getFieldValue错误失败:{}", e);
        }
        return null;
    }

    /**
     * 设置字段值
     *
     * @param object
     * @param fieldName
     * @param value
     */
    private static void setFieldValue(Object object, String fieldName, Object value) {
        try {
            Field field = object.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            field.set(object, value);
        } catch (Exception e) {
            log.error("setFieldValue错误失败:{}", e);
        }
    }

}

使用示例

对返回的节点对象进行注解标注。

@Getter
@Setter
@TreeNode(
        nodeKey = "id",
        parentKey = "parentId",
        childrenKey = "children",
        sortKey = "sort",
        nodeNameKey = "organizationName",
        parentNameKey = "parentName",
        fullNameKey = "fullName"
)
@ApiModel("组织机构节点DTO")
public class OrganizationTreeNodeDTO {
    @ApiModelProperty("ID")
    private Long id;

    @ApiModelProperty("父级ID")
    private Long parentId;

    @ApiModelProperty("机构代码")
    private String organizationCode;

    @ApiModelProperty("机构名称")
    private String organizationName;

    @ApiModelProperty("排序")
    private Integer sort;

    @ApiModelProperty("子机构列表")
    private List<MenuListDTO> children;

    @ApiModelProperty("父级名称")
    private String parentName;

    @ApiModelProperty("全称名称")
    private String fullName;
}

使用TreeUtils.buildTree对 List 快捷解析

@ApiOperation("组织机构树")
@PostMapping(value = "/tree")
@SaCheckLogin
public Response<List<OrganizationTreeNodeDTO>> tree(@Valid @RequestBody OrganizationQueryDTO param, BindingResult result) {
    // 查询菜单
    List<OrganizationTreeNodeDTO> treeList = new ArrayList<>();
    List<Organization> sourceList = organizationMapper.selectOrgnizationTree(param.getOrganizationName());
    if (CollectionUtils.isNotEmpty(sourceList)) {
        // 类型转换
        List<OrganizationTreeNodeDTO> targetList = BeanUtils.mapList(sourceList, OrganizationTreeNodeDTO.class);
        // 构建菜单树
        treeList = TreeUtils.buildTree(targetList);
    }

    return new Response.Builder<List<OrganizationTreeNodeDTO>>().success(treeList);
}