背景
最近项目要进行老服务到新服务的REST API迁移,涉及一套CRUD API,但是项目时间紧,需要4个人同时参与开发,初步阅读代码发现CRUD4个API都有大量重复的公共方法依赖,如果4个人独立开发,必然有开发工作上的重复和浪费,同时在后期合并代码时也有很多冲突要解决,带来极大的沟通成本。
因此,在前期就将项目中公共的方法识别出来,可以将他们方法定义写好,作为所有人的开发基础,至于实现,可以看情况分配给项目组里的人,使得公共的方法只实现一次,相关的接口可以直接复用。
思考
可是这些代码依赖的方法各种各样,有rpc,有dao,有config,有metric等等等等,并且调用链也非常深,可能达到10+层,因此人工分析低效且不精确,好在IDEA有工具,可以解析某方法向下调用的所有链路
尝试
发现idea有快捷键可以发现方法的向下调用链路,并且可以复制:
以截图中代码为例,复制得到的文本如下
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共用方法,主要实现步骤如下
- 复制出依赖文本
- 构建Tree(类名使用Node)
- 清除重复节点
- 统计公共方法
- 输出每个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真是给力呀!