IT教程 ·

突破CRUD | 万能树工具类封装

205 天考研总结

0、学完本文你也许可以收成

  • 以为一个树东西从初始逐渐优化完美的历程

  • 树东西封装的设想思索与完成思绪

  • 末了收成一款拿来即用的树东西源代码

关于前端树组件有肯定相识和运用过的同砚可直接腾跃到第3章节入手下手。

1、树长什么样 ?

前端的树组件大多数状况下涌现在后端的治理体系中,比方我们罕见的菜单树、机构树、某某分类树、树表格等。大抵像下方图片所展示的如许。

菜单树

突破CRUD | 万能树工具类封装 IT教程 第1张

机构树

突破CRUD | 万能树工具类封装 IT教程 第2张org_tree.png

树表格

突破CRUD | 万能树工具类封装 IT教程 第3张

大抵上来讲,前端树的展示情势就是上面3张图所列的几种情势。而这类前端树组件的展示组成须要依靠于后端返回的数据花样。

2、数据花样

连系我自身运用过的前端树组件来讲,大抵可以分为以下两种。

列表情势

[
    { id:1, pId:0, name:"父节点1"}

    { id:11, pId:1, name:"父节点11"},
    { id:111, pId:11, name:"叶子节点111"},
    { id:112, pId:11, name:"叶子节点112"},
    { id:113, pId:11, name:"叶子节点113"},
    { id:114, pId:11, name:"叶子节点114"},

    { id:12, pId:1, name:"父节点12"},
    { id:121, pId:12, name:"叶子节点121"},
    { id:122, pId:12, name:"叶子节点122"},
    { id:123, pId:12, name:"叶子节点123"},
    { id:124, pId:12, name:"叶子节点124"}
]

树形组织

[{    name:"父节点1",
    children: [
        { 
            name:"父节点11",
            children: [
                { name:"叶子节点111"},
                { name:"叶子节点112"},
                { name:"叶子节点113"},
                { name:"叶子节点114"}
            ]
        },
        { 
            name:"父节点12",
            children: [
                { name:"叶子节点121"},
                { name:"叶子节点122"},
                { name:"叶子节点123"},
                { name:"叶子节点124"}
            ]
        }
    ]
}]

本文所讲的树东西封装主假如针对第二种数据花样树形组织来讲,由于第一种自身不须要特别处置惩罚,也就不存在什么封装,就是简朴的列表查询展示,与平常数据列表数据花样的区别是多了数据ID与父ID属性供应给前端举行树组件的组织。

而第二种是要在列表情势的数据花样上举行转换,构成如上所示的树形组织。然则,我们发明内里没有数据ID与父ID属性,why ? 由于后端完成了数据层面树组织的组织事变,前端树组件再无需依据这两个属性举行树组织的推断构建,直接展示就OK,固然也不相对,终究还得看前端的树组件是不是须要。

但平常都邑保存这两个属性,由于除过树组件自身的组织需求,营业处置惩罚上每每须要这两个属性,而后端树东西要组织树组织,那肯定是须要数据ID与父ID的。

假如以为上面说的贫苦你就记着一点,不管是列表组织照样树形组织,一直保存数据ID与父ID两个属性就对了。

到这里又有一个新问题了,上面说了列表情势无需封装什么可以直接运用,既然云云那用列表情势的组织就完了呗,为何写个东西类搞个树组织出来呢 ?

原因是,前端树组件的完成体式格局异常多,差别树插件或组件须要的数据花样大概不一样,有的列表、树形花样都支撑,有的仅支撑列表或树形的一种,所以为了满足差别前端树的展示需求,供应树形组织的组织东西是必要的。

3、话不多说,先完成个第一版

从上面的内容我们相识了前端树组件的衬着展示须要后端供应满足需求的数据花样,那末实际上也就决议了树东西类的中心职责就是将平常的数据列表组织转换为树形组织,从而供应给前端运用。

解读上面所述的中心职责,起首平常列表是什么列表,此处我们假定为菜单列表,这就有了第一个类MenuEntity,紧接着是转换,谁转换成谁 ?数据列表转换树组织,树组织自身那应当就是个类,我们临时叫它 TreeNode,连系我们第一步假定的菜单列表,那实际上就是 List< MenuEntity > 转换为 List < TreeNode > ,云云就得到了第二个类TreeNode,末了还剩转换这个行动谁去做 ? 那就是我们本日的主角 TreeUtil 了。

好,至此,经由过程剖析树东西类的中心职责,我们剖析得到了三个类。

  • MenuEntity

  • TreeNode

  • TreeUtil

OK,有了上面的内容那就来个简朴的完成。

树节点类

public class TreeNode {
    // 树节点ID
    private String id;
    // 树节点称号
    private String name;
    // 树节点编码
    private String code;
    // 树节点链接
    private String linkUrl;
    // 树节点图标
    private String icon;
    // 父节点ID
    private String parentId;
}

菜单类

public class MenuEntity {
    // 菜单ID
    private String id;
    // 上级菜单ID
    private String pid;
    // 菜单称号
    private String name;
    // 菜单编码
    private String code;
    // 菜单图标
    private String icon;
    // 菜单链接
    private String url;
}

树东西类

public class TreeUtil {

    /**
     * 树构建
     */
    public static List<TreeNode> build(List<TreeNode> treeNodes,Object parentId){
        List<TreeNode> finalTreeNodes = CollectionUtil.newArrayList();
        for(TreeNode treeNode : treeNodes){
            if(parentId.equals(treeNode.getParentId())){
                finalTreeNodes.add(treeNode);
                innerBuild(treeNodes,treeNode);
            }
        }
        return finalTreeNodes;
    }

    private static void innerBuild(List<TreeNode> treeNodes,TreeNode parentNode){
        for(TreeNode childNode : treeNodes){
            if(parentNode.getId().equals(childNode.getParentId())){
                List<TreeNode> children = parentNode.getChildren();
                if(children == null){
                    children = CollectionUtil.newArrayList();
                    parentNode.setChildren(children);
                }
                children.add(childNode);
                childNode.setParentId(parentNode.getId());
                innerBuild(treeNodes,childNode);
            }
        }
    }
}

树东西类完成的两个症结点,第一,树构建的入手下手位置也就是从那里入手下手构建,所以须要一个父ID参数来指定构建的肇端位置,第二,构建到什么时刻完毕,不做限定的的话,我们的树是可以无穷延长的,所以此处innerBuild要领举行递归操纵。

测试代码

public static void main(String[] args{
    // 1、模仿菜单数据
    List<MenuEntity> menuEntityList = CollectionUtil.newArrayList();
    menuEntityList.add(new MenuEntity("1","0","体系治理","sys","/sys"));
    menuEntityList.add(new MenuEntity("11","1","用户治理","user","/sys/user"));
    menuEntityList.add(new MenuEntity("111","11","用户增加","userAdd","/sys/user/add"));
    menuEntityList.add(new MenuEntity("2","0","商号治理","store","/store"));
    menuEntityList.add(new MenuEntity("21","2","商品治理","shop","/shop"));

    // 2、MenuEntity -> TreeNode
    List<TreeNode> treeNodes = CollectionUtil.newArrayList();
    for(MenuEntity menuEntity : menuEntityList){
        TreeNode treeNode = new TreeNode();
        treeNode.setId(menuEntity.getId());
        treeNode.setParentId(menuEntity.getPid());
        treeNode.setCode(menuEntity.getCode());
        treeNode.setName(menuEntity.getName());
        treeNode.setLinkUrl(menuEntity.getUrl());
        treeNodes.add(treeNode);
    }

    // 3、树组织构建
    List<TreeNode> treeStructureNodes = TreeUtil.build(treeNodes,"0");
    Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes)));
}

收工,第一版简朴的树东西就完成了。

4、迭代优化

1.0 这不是我的事

然则,经由过程测试代码我们发明这个用起来不是太爽,要将菜单数据转换为树组织居然须要我先把菜单列表转换成树组织的列表才挪用树东西类的build要领,这里的转换操纵仅仅是属性的拷贝,并未完成树状组织的生成构建,但这是挪用者须要体贴的吗 ?很显然TreeNode鸠合建立生成这个历程应当是树东西类应当做的事变。所以做了以下调解。

1 调解了build要领参数,将原有treeNodes 调解为 menuEntityList,意味着将上面说的treeNodes构建组成交给TreeUtil去做。

2 新增了Convert类,并包括convert要领,该要领的职责是完成菜单实体到树节点属性的拷贝。

3 再次调解build要领参数,新增Convert转换。

调解完成的效果,看下代码。

树东西

public class TreeUtil_1_0 {

    // 新增的属性转换要领
    public interface Convert<MenuEntity,TreeNode>{
        public void convert(MenuEntity menuEntity, TreeNode treeNode);
    }

    /**
     * 树构建
     */
    public static List<TreeNode> build(List<MenuEntity> menuEntityList,Object parentId,Convert<MenuEntity,TreeNode> convert){

        // 本来挪用方做的事变
        List<TreeNode> treeNodes = CollectionUtil.newArrayList();
        for(MenuEntity menuEntity: menuEntityList){
            TreeNode treeNode = new TreeNode();
            convert.convert(menuEntity,treeNode);
            treeNodes.add(treeNode);
        }

        List<TreeNode> finalTreeNodes = CollectionUtil.newArrayList();
        for(TreeNode treeNode : treeNodes){
            if(parentId.equals(treeNode.getParentId())){
                finalTreeNodes.add(treeNode);
                innerBuild(treeNodes,treeNode);
            }
        }
        return finalTreeNodes;
    }

    private static void innerBuild(List<TreeNode> treeNodes,TreeNode parentNode){
        for(TreeNode childNode : treeNodes){
            if(parentNode.getId().equals(childNode.getParentId())){
                List<TreeNode> children = parentNode.getChildren();
                if(children == null){
                    children = CollectionUtil.newArrayList();
                    parentNode.setChildren(children);
                }
                children.add(childNode);
                childNode.setParentId(parentNode.getId());
                innerBuild(treeNodes,childNode);
            }
        }
    }
}

测试代码

public static void main(String[] args{
    // 1、模仿菜单数据
    List<MenuEntity> menuEntityList = CollectionUtil.newArrayList();
    menuEntityList.add(new MenuEntity("1","0","体系治理","sys","/sys"));
    menuEntityList.add(new MenuEntity("11","1","用户治理","user","/sys/user"));
    menuEntityList.add(new MenuEntity("111","11","用户增加","userAdd","/sys/user/add"));
    menuEntityList.add(new MenuEntity("2","0","商号治理","store","/store"));
    menuEntityList.add(new MenuEntity("21","2","商品治理","shop","/shop"));

    // 2、树组织构建
    List<TreeNode> treeStructureNodes = TreeUtil_1_0.build(menuEntityList, "0"new Convert<MenuEntity, TreeNode>() {
        @Override
        public void convert(MenuEntity menuEntity, TreeNode treeNode{
            treeNode.setId(menuEntity.getId());
            treeNode.setParentId(menuEntity.getPid());
            treeNode.setCode(menuEntity.getCode());
            treeNode.setName(menuEntity.getName());
            treeNode.setLinkUrl(menuEntity.getUrl());
        }
    });
    Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes)));
}

比较1.0与第一版的测试代码,发明少了树节点列表构建的历程,属性拷贝的事变作为回调历程在转换历程当中举行处置惩罚。

2.0 仅支撑造菜单树哪够

1.0优化完后,我们来了新的需求,有个机构树也须要生成,此时的树东西仅支撑了菜单树,所以我们举行革新,让其支撑其他任何对象的树生成。

革新点主假如将的TreeUtil中的菜单实体转换为泛型,限于篇幅,就贴个中心要领的代码

public static <T> List<TreeNode> build(List<T> list,Object parentId,Convert<T,TreeNode> convert){
    List<TreeNode> treeNodes = CollectionUtil.newArrayList();
    for(T obj : list){
        TreeNode treeNode = new TreeNode();
        convert.convert(obj,treeNode);
        treeNodes.add(treeNode);
    }

    List<TreeNode> finalTreeNodes = CollectionUtil.newArrayList();
    for(TreeNode treeNode : treeNodes){
        if(parentId.equals(treeNode.getParentId())){
            finalTreeNodes.add(treeNode);
            innerBuild(treeNodes,treeNode);
        }
    }
    return finalTreeNodes;
}

云云一来,我们就可以支撑恣意范例的树组织。

3.0 哥们,你返回的属性不够用啊

前两点比较轻易想到,也比较轻易完成,但这时刻前端同砚抛来了新的问题,哥们,你返回的树节点属性不够用啊,你看我这界面。须要备注你没返回来啊。

突破CRUD | 万能树工具类封装 IT教程 第3张

好吧,这类状况确切没考虑到。

要满足上述需求,简朴做法就将remark属性直接增加到 TreeNode 类中,Convert中赋下值,这不就满足了,但想一想又不对,本日这个前端店员缺个remark,来日诰日大概别的店员又缺个其他属性,全加到TreeNode中,TreeNode究竟是树节点照样营业实体,所以不能这么搞。

这里要处置惩罚成可扩大,同时满足开闭准绳,所以此处比较妥的处置惩罚体式格局是继续,TreeNode属性满足不了的状况下,经由过程继续扩大详细营业的树节点来完成。

详细革新点以下

1 新增菜单实体扩大树节点以下

public class MenuEntityTreeNode extends TreeNode {
    // 扩大备注属性
    private String remark;
    // 省略set get ...

}

2 革新TreeUtil.build要领参数,新增TreeNode Class范例参数,以下

/**
 * 树构建
 */
public static <T,E extends TreeNode> List<E> build(List<T> list,Object parentId,Class<EtreeNodeClass,Convert<T,Econvert){
    List<E> treeNodes = CollectionUtil.newArrayList();
    for(T obj : list){
        E treeNode = (E)ReflectUtil.newInstance(treeNodeClass);
        convert.convert(obj, treeNode);
        treeNodes.add(treeNode);
    }

    List<E> finalTreeNodes = CollectionUtil.newArrayList();
    for(E treeNode : treeNodes){
        if(parentId.equals(treeNode.getParentId())){
            finalTreeNodes.add((E)treeNode);
            innerBuild(treeNodes,treeNode);
        }
    }
    return finalTreeNodes;
}

测试代码

public static void main(String[] args{
    // ...此处省略模仿数据建立历程

    // 2、树组织构建
    List<MenuEntityTreeNode> treeStructureNodes = TreeUtil_3_0.build(menuEntityList, "0",MenuEntityTreeNode.class,new TreeUtil_3_0.Convert<MenuEntity,MenuEntityTreeNode>(){

        @Override
        public void convert(MenuEntity object, MenuEntityTreeNode treeNode{
            treeNode.setId(object.getId());
            treeNode.setParentId(object.getPid());
            treeNode.setCode(object.getCode());
            treeNode.setName(object.getName());
            treeNode.setLinkUrl(object.getUrl());
            // 新增的营业属性
            treeNode.setRemark("增加备注属性");
        }
    });
   Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes)));
}

云云一来,差别营业场景下须要增加差别的属性时,即可做到可扩大,且对现有代码不形成任何影响和修改。

4.0 哥们,我的属性名不叫code

完成了3.0版本,基本上大部分需求就都可以满足了,然则这时刻前端同砚又抛来了新的问题,哥们,你返回的树节点编号属性是code,但我这边的叫number,对应不上,我这边调解的话影响比较大,你看后端返回的时刻能不能处置惩罚下。

code属性名肯定是不能调解的,由于其他模块树的节点编号都叫code。

那怎样办 ?实在也简朴,跟3.0版本一样,在扩大的营业树节点去加个属性,如许问题是处理了,但万一涌现一切treeNode的属性名都跟前端须要的不对应这类极度状况,那意味着一切树属性都须要自行扩大定义,这类岂不是返回了没什么用的父TreeNode心中的一切属性。序列化时却是可以掌握,为空的不举行序列化,但不是依靠序列化框架了么。另有没有其他方法。

轻微整顿下需求,就是树节点属性在返回前端时要可以支撑自定义属性名

类属性定义好就改不了了,怎样自定义,除了新增类和改现有的属性,另有什么方法呢 ?这时刻我们应当想到map

详细怎样做

1 起首,定义新的类TreeNodeMap,看名字就晓得基于map完成

public class TreeNodeMap extends HashMap {

    private TreeNodeConfig treeNodeConfig;

    public TreeNodeMap(){
        this.treeNodeConfig = TreeNodeConfig.getDefaultConfig();
    }

    public TreeNodeMap(TreeNodeConfig treeNodeConfig){
        this.treeNodeConfig = treeNodeConfig;
    }

    public <T> getId() {
        return (T)super.get(treeNodeConfig.getIdKey());
    }

    public void setId(String id) {
        super.put(treeNodeConfig.getIdKey(), id);
    }

    public <T> getParentId() {
        return (T)super.get(treeNodeConfig.getParentIdKey());
    }

    public void setParentId(String parentId) {
        super.put(treeNodeConfig.getParentIdKey(), parentId);
    }

    public <T> getName() {
        return (T)super.get(treeNodeConfig.getNameKey());
    }

    public void setName(String name) {
        super.put(treeNodeConfig.getNameKey(), name);
    }

    public <T> T  getCode() {
        return (T)super.get(treeNodeConfig.getCodeKey());
    }

    public TreeNodeMap setCode(String code) {
        super.put(treeNodeConfig.getCodeKey(), code);
        return this;
    }

    public List<TreeNodeMap> getChildren() {
        return (List<TreeNodeMap>)super.get(treeNodeConfig.getChildrenKey());
    }

    public void setChildren(List<TreeNodeMap> children) {
        super.put(treeNodeConfig.getChildrenKey(),children);
    }

    public void extra(String key,Object value){
        super.put(key,value);
    }
}

2 既然支撑属性名自定义,新增设置类TreeNodeConfig来完成这个事变,同时供应默许属性名

public class TreeNodeConfig {

    // 默许属性的单例对象
    private static TreeNodeConfig defaultConfig = new TreeNodeConfig();

    // 树节点默许属性常量
    static final String TREE_ID = "id";
    static final String TREE_NAME = "name";
    static final String TREE_CODE = "code";
    static final String TREE_CHILDREN = "children";
    static final String TREE_PARENT_ID = "parentId";

    // 属性
    private String idKey;
    private String codeKey;
    private String nameKey;
    private String childrenKey;
    private String parentIdKey;

    public String getIdKey() {
        return getOrDefault(idKey,TREE_ID);
    }

    public void setIdKey(String idKey) {
        this.idKey = idKey;
    }

    public String getCodeKey() {
        return getOrDefault(codeKey,TREE_CODE);
    }

    public void setCodeKey(String codeKey) {
        this.codeKey = codeKey;
    }

    public String getNameKey() {
        return getOrDefault(nameKey,TREE_NAME);
    }

    public void setNameKey(String nameKey) {
        this.nameKey = nameKey;
    }

    public String getChildrenKey() {
        return getOrDefault(childrenKey,TREE_CHILDREN);
    }

    public void setChildrenKey(String childrenKey) {
        this.childrenKey = childrenKey;
    }

    public String getParentIdKey() {
        return getOrDefault(parentIdKey,TREE_PARENT_ID);
    }

    public void setParentIdKey(String parentIdKey) {
        this.parentIdKey = parentIdKey;
    }

    public String getOrDefault(String key,String defaultKey){
        if(key == null) {
            return defaultKey;
        }
        return key;
    }

    public static TreeNodeConfig getDefaultConfig(){
        return defaultConfig;
    }

}

3 末了,革新TreeUtil.build 要领,基于2.0版本,只需将TreeNode替换成TreeNodeMap即可。

/**
 * 树构建
 */
public static <T> List<TreeNodeMap> build(List<T> list,Object parentId,Convert<T,TreeNodeMap> convert){
    List<TreeNodeMap> treeNodes = CollectionUtil.newArrayList();
    for(T obj : list){
        TreeNodeMap treeNode = new TreeNodeMap();
        convert.convert(obj,treeNode);
        treeNodes.add(treeNode);
    }

    List<TreeNodeMap> finalTreeNodes = CollectionUtil.newArrayList();
    for(TreeNodeMap treeNode : treeNodes){
        if(parentId.equals(treeNode.getParentId())){
            finalTreeNodes.add(treeNode);
            innerBuild(treeNodes,treeNode);
        }
    }
    return finalTreeNodes;
}

测试代码

public static void main(String[] args{
     // ... 省略菜单模仿数据的建立历程

    TreeNodeConfig treeNodeConfig = new TreeNodeConfig();
    // 自定义属性名
    treeNodeConfig.setCodeKey("number");
    List<TreeNodeMap> treeNodes = TreeUtil_4_0.build(menuEntityList, "0",treeNodeConfig,new TreeUtil_4_0.Convert<MenuEntity, TreeNodeMap>() {
        @Override
        public void convert(MenuEntity object, TreeNodeMap treeNode{
            treeNode.setId(object.getId());
            treeNode.setParentId(object.getPid());
            treeNode.setCode(object.getCode());
            treeNode.setName(object.getName());
            // 属性扩大
            treeNode.extra("extra1","123");
        }
    });

    Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeNodes)));
}

经由上面的革新,我们完成了树节点属性的自定义,趁便还完成了属性可扩大,一石二鸟。

3、总结

现在这个水平大概仍有些场景没法满足,然则关于大部分的问题场景基于3.0或4.0版本略加革新应当都可以处理。剩下的就连系场景再酌情优化调解。

4、源代码&视频

5、更多出色

以为还行,动动手指留个赞。
以上就是本日的内容,我们下期见。

并发编程的基石——AQS类

参与评论