解析一套CRUD REST API的大量公共依赖方法

105 阅读6分钟

背景

最近项目要进行老服务到新服务的REST API迁移,涉及一套CRUD API,但是项目时间紧,需要4个人同时参与开发,初步阅读代码发现CRUD4个API都有大量重复的公共方法依赖,如果4个人独立开发,必然有开发工作上的重复和浪费,同时在后期合并代码时也有很多冲突要解决,带来极大的沟通成本。

因此,在前期就将项目中公共的方法识别出来,可以将他们方法定义写好,作为所有人的开发基础,至于实现,可以看情况分配给项目组里的人,使得公共的方法只实现一次,相关的接口可以直接复用。

思考

可是这些代码依赖的方法各种各样,有rpc,有dao,有config,有metric等等等等,并且调用链也非常深,可能达到10+层,因此人工分析低效且不精确,好在IDEA有工具,可以解析某方法向下调用的所有链路

尝试

发现idea有快捷键可以发现方法的向下调用链路,并且可以复制:

image.png

image.png

image.png

以截图中代码为例,复制得到的文本如下

App.summaryChannel(String, Map<Node, Set<String>>)  (org.garry)
    App.constructNode(String)  (org.garry)
        Node.java(2 usages)  (org.garry)
        App.countLeadingSpaces(String)  (org.garry)
        Node.java  (org.garry)
    App.removeDuplicate(Node)  (org.garry)
        Node.java(2 usages)  (org.garry)
        Node.java(2 usages)  (org.garry)
    App.summary(Map<Node, Set<String>>, Node, String)  (org.garry)
        Node.java  (org.garry)
        App.summary(Map<Node, Set<String>>, Node, String)  (org.garry)
            Node.java  (org.garry)
App.constructNode(String)  (org.garry)
    Node.java(2 usages)  (org.garry)
    App.countLeadingSpaces(String)  (org.garry)
    Node.java  (org.garry)
App.prettyPrintNode(Map<Node, Set<String>>, Node, String, String, boolean)  (org.garry)
    Node.java  (org.garry)
    Node.java  (org.garry)
    App.prettyPrintNode(Map<Node, Set<String>>, Node, String, String, boolean)  (org.garry)
        Node.java  (org.garry)
        Node.java  (org.garry)

解决方案

经过反复思考,决定将调用链文本每行末尾增加注释,表示该方法在当前API中与哪些其他API共用方法,主要实现步骤如下

  1. 复制出依赖文本
  2. 构建Tree(类名使用Node)
  3. 清除重复节点
  4. 统计公共方法
  5. 输出每个API中公共的方法

1. 复制出依赖文本

上面已实现

2. 构建Tree

Node类新建如下

package org.garry;

import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

@Data
@RequiredArgsConstructor
@ToString(exclude = "parent")
public class Node {
    private final String id;
    private final Node parent;
    private List<Node> children = new ArrayList<>();

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Node node = (Node) o;
        return Objects.equals(id, node.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

构建Tree则借用一个stack,顺序遍历文本行就可做到

    static Node constructNode(String path) throws IOException {
        Node root = new Node("root", null);
        int indent = -1;
        Stack<Node> stack = new Stack<>();
        stack.push(root);
        ClassLoader classLoader = App.class.getClassLoader();
        try (InputStream inputStream = classLoader.getResourceAsStream(path)) {
            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
            for (String rawLine : br.lines().toList()) {
                // 计算indent
                int curIndents = countLeadingSpaces(rawLine) / 4;

                // 判断出入栈
                // 拿到父Node
                // 执行添加到children操作
                Node parent;
                if (curIndents == indent) {
                    stack.pop();
                    parent = stack.peek();
                } else if (curIndents > indent) {
                    parent = stack.peek();
                } else {
                    for (int i = 0; i < indent - curIndents; i++) {
                        stack.pop();
                    }
                    stack.pop();
                    parent = stack.peek();
                }
                Node node = new Node(rawLine.replaceAll("\\(\\d usages\\)", "").trim(), parent); // 这里有一个小细节,IDEA会把方法调用次数显示成xx usages,会导致同样的方法出现多次,这里将这个备注去掉,方便后续去重
                parent.getChildren().add(node);
                stack.push(node);
                indent = curIndents;
            }
        }
        return root;
    }

3. 清除重复节点

这里主要使用BFS,将遇到的Node都记录到iteratedIds中,因此如果重复的方法只会保留最接近root节点的

    private static void removeDuplicate(Node root) {
        Set<String> iteratedIds = new HashSet<>();
        Deque<Pair<Node, Node>> deque = new ArrayDeque<>();
        deque.add(new Pair<>(root, null));
        while (!deque.isEmpty()) {
            Pair<Node, Node> pair = deque.removeFirst();
            Node node = pair.getKey();
            Node parent = pair.getValue();
            if (iteratedIds.contains(node.getId())) {
                parent.getChildren().remove(node);
            } else {
                iteratedIds.add(node.getId());
                node.getChildren().forEach(child -> deque.add(new Pair<>(child, node)));
            }
        }
    }

4. 统计公共方法

每个API的Tree构建并去重后,便进行方法使用统计

    Map<Node, Set<String>> map = new HashMap<>(); // key: 方法行文本 value:API集合
    for (String channel : channels) {
        summaryChannel(channel, map);
    }
...
    private static void summaryChannel(String channel, Map<Node, Set<String>> map) throws IOException {
        Node root = constructNode(channel);
        removeDuplicate(root);
        summary(map, root, channel);
    }
...

    private static void summary(Map<Node, Set<String>> result, Node root, String channel) {
        result.computeIfAbsent(root, n -> new HashSet<>());
        result.get(root).add(channel);
        root.getChildren().forEach(child -> {
            summary(result, child, channel);
        });
    }

5. 输出每个API中公共的方法

主要说一下prettyPrintNode这个方法,他会遍历tree中每一个Node,即方法,并使用Node的key去map中查找复用情况,然后和方法一起打印出来

    public static void main(String[] args) throws IOException {
        String[] channels = new String[]{"create", "get", "bulk_create", "bulk_update", "bulk_delete", "delete"}; // 这里表示6个API,CRUD分别2、1、1、2个

        Map<Node, Set<String>> map = new HashMap<>();
        for (String channel : channels) {
            summaryChannel(channel, map); // 这里是上面几个步骤的功能,贴出来方便这一步的理解
        }

        for (String channel : channels) {
            Node root = constructNode(channel);
            System.out.println("\n\n\n\n\n\n以下为channel: " + channel);
            prettyPrintNode(map, root, channel, "", true);
        }
    }
        private static void prettyPrintNode(Map<Node, Set<String>> map, Node root, String channel, String prefix, boolean isRoot) {
        if (!isRoot) {
            Set<String> strings = map.get(root);
            String suffix = "";
            if (strings.contains(channel) && strings.size() > 1) {
                HashSet<String> suffixParam = new HashSet<>(strings);
                suffixParam.remove(channel);
                suffix += " also in " + suffixParam;
            }
            System.out.println(prefix + root.getId() + suffix);
        }
        root.getChildren().forEach(child -> {
            prettyPrintNode(map, child, channel, isRoot ? "" : prefix + "    ", false);
        });
    }

最后贴一个执行结果(已脱敏)

以下为channel: get
AdRequestValidator.validateGetAdsByIRRequest(String, IRTypeEnum, String)  (someorg.spring.whyservice.cm.validator)
    AdRequestValidator.validateIRId(String, String)  (someorg.spring.whyservice.cm.validator) also in [bulk_create, bulk_delete, create, delete, bulk_update]
    AdRequestValidator.validateIRType(IRTypeEnum, String)  (someorg.spring.whyservice.cm.validator) also in [bulk_create, bulk_delete, create, delete, bulk_update]
    RequestValidator.validateCampaignId(String)  (someorg.spring.whyservice.cm.validator) also in [bulk_create, bulk_delete, create, delete, bulk_update]
        AdGroupRequestValidator.validateCampaignId(String)  (someorg.spring.whyservice.adgroup.validator) also in [bulk_create, bulk_delete, create, delete, bulk_update]
        RequestValidator.validateCampaignId(ErrorService, String)  (someorg.spring.whyservice.cm.validator) also in [bulk_create, bulk_delete, create, delete, bulk_update]
CampaignGetManager.getAdFromAdsBidActiveRecord(AdsBidActive, IRTypeEnum, String, Boolean, MAEnum, int)  (someorg.spring.whyservice.cm.manager)
    CampaignGetManager.buildAdAlerts(Ad, int, MAEnum)  (someorg.spring.whyservice.cm.manager)
        CampaignConfig.minAdRate(int)  (someorg.spring.whyservice.config) also in [bulk_create, create, bulk_update]
        CampaignGetManagerHelper.buildAlert(AlertTypeEnum, AlertDimension, Aspect)  (someorg.spring.whyservice.util)
CampaignGetManager.handleException(Exception, int, String, String)  (someorg.spring.whyservice.cm.manager)
    ZddPrometheusMetrics.incrementCounter(Metric, String...)  (someorg.app.spring.ppx.common.metrics) also in [bulk_create, bulk_delete, create, delete, bulk_update]
        ZddPrometheusMetrics.incrementCounterByValue(Metric, long, String...)  (someorg.app.spring.ppx.common.metrics) also in [bulk_create, bulk_delete, create, delete, bulk_update]
            Metric.getHelpText()  (someorg.app.spring.ppx.common.metrics) also in [bulk_create, bulk_delete, create, delete, bulk_update]
            ZddPrometheusMetrics.getValidTags(Metric, String...)  (someorg.app.spring.ppx.common.metrics) also in [bulk_create, bulk_delete, create, delete, bulk_update]
                Metric.getLabels()  (someorg.app.spring.ppx.common.metrics) also in [bulk_create, bulk_delete, create, delete, bulk_update]
            whyserviceCampaignConfig.isPrometheusMetricsEnabled()  (someorg.spring.whyservice.serviceconfig) also in [bulk_create, bulk_delete, create, delete, bulk_update]
CampaignGetManager.handleSuccess(int, String)  (someorg.spring.whyservice.cm.manager)
    ZddPrometheusMetrics.incrementCounter(Metric, String...)  (someorg.app.spring.ppx.common.metrics) also in [bulk_create, bulk_delete, create, delete, bulk_update]
        ZddPrometheusMetrics.incrementCounterByValue(Metric, long, String...)  (someorg.app.spring.ppx.common.metrics) also in [bulk_create, bulk_delete, create, delete, bulk_update]
            Metric.getHelpText()  (someorg.app.spring.ppx.common.metrics) also in [bulk_create, bulk_delete, create, delete, bulk_update]
            ZddPrometheusMetrics.getValidTags(Metric, String...)  (someorg.app.spring.ppx.common.metrics) also in [bulk_create, bulk_delete, create, delete, bulk_update]
                Metric.getLabels()  (someorg.app.spring.ppx.common.metrics) also in [bulk_create, bulk_delete, create, delete, bulk_update]
            whyserviceCampaignConfig.isPrometheusMetricsEnabled()  (someorg.spring.whyservice.serviceconfig) also in [bulk_create, bulk_delete, create, delete, bulk_update]
CampaignManager.getValidKeyBasedCampaignForGets(Long, Long)  (someorg.spring.whyservice.cm.manager)
    CampaignManager.getNotEndedCampaign(Long, Long)  (someorg.spring.whyservice.cm.manager) also in [bulk_create, bulk_delete, create, delete, bulk_update]
        CampaignManager.getCampaign(Long, Long)  (someorg.spring.whyservice.cm.manager) also in [bulk_create, bulk_delete, create, delete, bulk_update]
CampaignRequestValidator.validateCampaignNotCPC(AdsCampaign)  (someorg.spring.whyservice.cm.validator) also in [bulk_create, bulk_delete, create, delete, bulk_update]
getFailedReferenceNames() in GetListingIdsBySKUNamesResponse in InventoryServiceManager  (someorg.spring.whyservice.cm.manager) also in [bulk_create, bulk_delete, create, delete, bulk_update]
getSkuId() in InventoryListing in GetByInventoryNamesResponse  (someorg.app.spring.ppx.data) also in [bulk_create, bulk_delete, create, delete, bulk_update]
getSuccessReferenceNames() in GetListingIdsBySKUNamesResponse in InventoryServiceManager  (someorg.spring.whyservice.cm.manager) also in [bulk_create, bulk_delete, create, delete, bulk_update]
InventoryServiceManager.getListingIdsBySkuNames(Long, MAEnum, List<IRName>)  (someorg.spring.whyservice.cm.manager) also in [bulk_create, bulk_delete, create, delete, bulk_update]
    addFailedReferenceName(IRName, ErrorDetailV3) in GetListingIdsBySKUNamesResponse in InventoryServiceManager  (someorg.spring.whyservice.cm.manager) also in [bulk_create, bulk_delete, create, delete, bulk_update]
    GetByInventoryNamesResponse.getListings()  (someorg.app.spring.ppx.data) also in [bulk_create, bulk_delete, create, delete, bulk_update]
    getGroupId() in InventoryListing in GetByInventoryNamesResponse  (someorg.app.spring.ppx.data) also in [bulk_create, bulk_delete, create, delete, bulk_update]
    getPartOfGroup() in InventoryListing in GetByInventoryNamesResponse  (someorg.app.spring.ppx.data) also in [bulk_create, bulk_delete, create, delete, bulk_update]
    getSkuName() in InventoryListing in GetByInventoryNamesResponse  (someorg.app.spring.ppx.data) also in [bulk_create, bulk_delete, create, delete, bulk_update]
    InventoryServiceManager.getInventoryResponse(String, MAEnum, List<IRName>)  (someorg.spring.whyservice.cm.manager) also in [bulk_create, bulk_delete, create, delete, bulk_update]
        InventoryServiceClient.getListingsByInventory(String, GetByInventoryNamesRquest)  (someorg.app.spring.ppx.rest) also in [bulk_create, bulk_delete, create, delete, bulk_update]
            CommonCMGingerClient.target(Client, String, Entity<?>, String, CalEventType, String, boolean, ...)  (someorg.spring.whyservice.rest.reco) also in [bulk_create, bulk_delete, create, delete, bulk_update]
                CommonCMGingerClient.getHeaders(boolean, boolean, Integer, String)  (someorg.spring.whyservice.rest.reco) also in [bulk_create, bulk_delete, create, delete, bulk_update]
                    CosHeadersValueEnum.get(int)  (someorg.spring.whyservice.rest.reco) also in [bulk_create, bulk_delete, create, delete, bulk_update]
                    CosHeadersValueEnum.getMarketplaceId()  (someorg.spring.whyservice.rest.reco) also in [bulk_create, bulk_delete, create, delete, bulk_update]
                CommonCMGingerClient.setQueryParam(WebTarget, String)  (someorg.spring.whyservice.rest.reco) also in [bulk_create, bulk_delete, create, delete, bulk_update]
            InventoryException.InventoryException(String, ErrorMessageV3)  (someorg.app.spring.ppx.exceptions) also in [bulk_create, bulk_delete, create, delete, bulk_update]
        InventoryServiceManager.getListingIdsBySKUNamesRequest(MAEnum, List<IRName>)  (someorg.spring.whyservice.cm.manager) also in [bulk_create, bulk_delete, create, delete, bulk_update]
            GetByInventoryNamesRquest.GetByInventoryNamesRquest(MAEnum, List<String>, List<String>)  (someorg.app.spring.ppx.data) also in [bulk_create, bulk_delete, create, delete, bulk_update]
    InventoryServiceManager.getPublicUserId(Long)  (someorg.spring.whyservice.cm.manager) also in [bulk_create, bulk_delete, create, delete, bulk_update]
    setSuccessReferenceNames(Multimap<IRName, InventoryListing>) in GetListingIdsBySKUNamesResponse in InventoryServiceManager  (someorg.spring.whyservice.cm.manager) also in [bulk_create, bulk_delete, create, delete, bulk_update]
SiteRampConfig.isAdRateFloorEnabled(int)  (someorg.spring.whyservice.config) also in [bulk_create, create, bulk_update]
    SiteRampConfig.getBooleanConfig(SiteRampspringConfigEnum, int)  (someorg.spring.whyservice.config) also in [bulk_create, create, bulk_update]
        getConfigName() in SiteRampspringConfigEnum in SiteRampConfig  (someorg.spring.whyservice.config) also in [bulk_create, create, bulk_update]
        getDefaultValue() in SiteRampspringConfigEnum in SiteRampConfig  (someorg.spring.whyservice.config) also in [bulk_create, create, bulk_update]

里面每一行后面都有可能有also in ...的字样,如果有的话,说明和其他API复用此方法,那么就可以协调一下谁来实现了。

总结

工作中遇到的问题千千万,如果能通过技术解决,并且花时间不长,那么实际对业务和自身都有益处,本例中IDEA提供分析方法调用链的功能是本次工作的决定因素,否则自己去实现这个功能可能会花很长时间,就不会这么做了,也许会花很多时间人工比对方法的复用情况,甚至在后续代码合并,code review过程中都会有很多繁琐的工作,想想,IDEA真是给力呀!