如何用Java写一个整理Java方法调用关系网络的程序
大家好,我是猿码叔叔,一位 Java 语言工作者,也是一位算法学习刚入门的小学生。很久没有为大家带来干货了。
最近遇到了一个问题,大致是这样的:如果给你一个 java 方法,如何找到有哪些菜单在使用。我的第一想法是,这不很简单吗?!使用 IDEA 自带的右键 Find Usage 功能,一步一步往上溯源最终找到 Controller 中的方法,找到 requestMapping 的映射路径,然后去数据库一查便知。
目录
一、问题真的这么简单吗?我们先分析一下这种问题的出现场景
二、有没有更快的解决方案?
三、如何提取 Java 对象中的方法以及方法中的被调用方法
IO 流读取 .java 文件
JDK 编译后的 .class 字节码文件
四、解读 -javap 命令反编译后的内容
五、用代码解析出类的方法与被调方法
六、让方法与被调方法的关系可视化
一、问题真的这么简单吗?我们先分析一下这种问题的出现场景
我所在的这个项目是一个接近15年的老项目,使用的还是 SSM 框架。前后端没有做到分离开来,耦合度极高。所以刚才那个问题的目的大致就是要将部分方法拆分出来,降低耦合度,使得后期的维护更加方便,亦或是扩展起来更加容易。
这种项目模块或方法之间耦合度高的问题,大多出现在老项目中。而仍然使用老项目的企业中国企居多。成本与安全也是阻碍老项目得到升级的两大关键问题。随着AI的兴起,这种问题的彻底解决或许能够看到一些希望,但是否有大模型专注于解决这种问题仍然需要考虑到成本和价值问题了。
二、有没有更快的解决方案?
除了刚才使用 IDEA 的 Find Usage 右键功能。我们或许可以调用 IDEA 的 API 也就是 Find Usage 功能,然后将项目中的所有方法串联成一个 N 叉树。对于 Controller 中的方法可以放在 Root 节点的下一层节点中。
但,IDEA 工具真的会给你提供这个 API 吗?答案是否定的,至少我搜索了很多相关内容,也没有得到一个准确的结果。或许有相关的开源组件提供这种方法溯源菜单的功能,但也都不尽如人意。
那我们能否自己写一个这样的程序呢?
三、如何提取 Java 对象中的方法以及方法中的被调用方法
这个程序实现起来其实很简单。我们只需要使用 IO 流去读取 .java 文件或者反射取出 class 中的 declaredMethods 即可。前者更开放,也更有挑战性。后者除了能取到声明方法以外,方法中的被调用方法反射做不到这一点。
IO 流读取 .java 文件
IO 流我们使用 BufferedReader 一行一行地读取 .java 文件中的内容,然后根据方法的特征解析出方法与被调用方法即可。听起来是不是很简单,怎么写代码?
考虑到 Java 中代码的多变性,比如换行、注释、内部类、静态代码块、字段等等,这些都是需要我们用算法来处理的。但这么搞下去,真的可以自己写一个 JDK 了。如果你肯坚持和足够动脑,也不是不可能实现。
JDK 编译后的 .class 字节码文件
如果你动手能力强,你会发现刚才说的一部分要处理的内容,jdk 可以帮你解决。比如注释。在项目编译后的 target 目录下,原来的 .java 文件会被编译成 .class 文件,这些文件中原有的注释内容100%都不会被保留。此时,我们可以考虑去读取 .class 文件来进一步实现我们的计划。
当拿到 .class 文件的数据时,我傻眼了。读取到的流数据并非我们眼睛看到的数据那样,而是二进制的字节码内容,要想解析这些数据,我们得学会解读这些内容。当然现在有很多工具可以反编译字节码文件。为了不重复造轮子,我去网上找到了如下代码,可以在 java 代码中执行反编译命令,将指定目录下的 .class 文件反编译成我们能读懂的内容。
private void decodeClassFile(File clazzFile) {String absPath = clazzFile.getAbsolutePath();try {String command = "javap -c " + absPath;Process process = runtime.exec(command);// 读取命令执行结果BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));StringBuilder sb = new StringBuilder();String line;while ((line = reader.readLine()) != null) {sb.append(line).append("\n");}// 等待命令执行完成process.waitFor();// 获取退出值process.exitValue();reader.close();absPath = absPath.substring(absPath.indexOf("cn"));absPath = absPath.replace('\\', '.');absPath = absPath.replace(".class", ".txt");// 将反编译后的内容写入到指定的文件内writeDecodedClazzFileDown(sb.toString(), absPath);} catch (IOException | InterruptedException e) {e.printStackTrace();}}
四、解读 -javap 命令反编译后的内容
打开反编译后的文件,你会发现方法的内容区域会有数字序列号,这些序列号其实是执行顺序的一个标识。序列号右侧的 “//” 后面会跟随描述当前行的内容类别。如果是方法,// 会写着 “Method”,如果是接口方法则是“InterfaceMethod”,如果是字段定义则是“Field”。对于其他的描述,读者有兴趣也可以去研究一下。紧随这些描述后的内容就是 java 源代码中的真实内容了。
46: invokevirtual #27 // Method cn/com/xxx/xxx/pojo/dev/Admin.getUserList:()Ljava/util/List;97: invokeinterface #7, 3 // InterfaceMethod org/springframework/ui/Model.addAttribute:(Ljava/lang/String;Ljava/lang/Object;)Lorg/springframework/ui/Model;
编号46,是一个普通方法;编号97是一个接口方法。46 的 Method 后面我给他分为 3 个部分。
1、方法名与方法所在的包路径。
2、() 中的内容代表参数信息。这些参数信息只包含包路径与类型,没有参数名称。
3、括号后面的内容是返回值信息。
我们看到,参数内容与返回值区域路径首字符多了一个 “L” 字符。这个代表对象类型区别于 Java 自己的 8 大基本类型。 这 8 个类型的指令如下:
dataType.put('[', "[]"); // 代表数组 dataType.put('I', "int"); dataType.put('J', "long"); dataType.put('F', "float"); dataType.put('D', "double"); dataType.put('Z', "boolean"); dataType.put('B', "byte"); dataType.put('C', "char"); dataType.put('S', "short");
对于其他更多的指令,读者可以去咨询 AI。比如文心一言或者GPT。
五、用代码解析出类的方法与被调方法
解析方法与被调方法前我们需要注意几点:
- Service 方法与 ServiceImpl 实现类的方法转换。在 Service 方法中的方法体是没有内容的,其内容会在他的实现类对应的方法体里。这时候你应该知道我要强调的是什么了。
- 方法与被调方法的参数信息在反编译文件里的内容是不同的,比如 double 类型与 int 类型同时存在时,你看到的是 “DI”,如果前者是数组,你看到的是“[DI”。所以为了在拿到被调方法时能够准确找到下一个节点,我们必须对这些内容进行还原
- 如上一条所说,我们解析出方法与被调方法的目的是能够准确的通过方法找到有哪些被调方法,拿到被调方法,能够准确找到被调方法中的被调方法。这是一个 N-ary 树的遍历思想,直到找不到为止。
- 当前类的自有方法互相调用,需要拼接其包路径。这是为了统一管理。
- 解析后的内容,在存放时应当有一个便于处理的格式。比如一级方法名前面没有空格,而二级的被调方法则应当在前面加上四个“-”字符,这样可以明确他们之间的关系。
下面是解析代码:
public class ProjectMethodCallingTreeGraph {private final static char[] METHOD_PREFIX = {'M', 'e', 't', 'h', 'o', 'd'};private final static char[] INTERFACE_METHOD_PREFIX = {'I', 'n', 't', 'e', 'r', 'f'};private final static String TARGET_ROOT_PATH = "D:\\WORK\\xxx\\pro-info\\xxx\\xxx-graph";private static Map<Character, String> dataType = new HashMap<>();static {dataType.put('[', "[]");dataType.put('I', "int");dataType.put('J', "long");dataType.put('F', "float");dataType.put('D', "double");dataType.put('Z', "boolean");dataType.put('B', "byte");dataType.put('C', "char");dataType.put('S', "short");}public static void main(String[] args) {String path = "D:\\WORK\\xx\\pro-info\\xxx\\xxxx";ProjectMethodCallingTreeGraph p = new ProjectMethodCallingTreeGraph();p.readDecodedClazzFile(path);}private void readDecodedClazzFile(String path) {File file = new File(path);for (File f : file.listFiles()) {String method = null;String fName = f.getName().substring(0, f.getName().length() - 3);boolean mapperOrService = f.getName().endsWith("Service.txt") || f.getName().endsWith("Mapper.txt");LinkedHashSet<String> callMethods = new LinkedHashSet<>();try (BufferedReader br = new BufferedReader(new FileReader(f))) {String line;StringBuilder sb = new StringBuilder();while ((line = br.readLine()) != null) {char[] cs = line.toCharArray();String res = findMethodLine(cs, callMethods, mapperOrService);if (res != null) {if (method != null && method.length() > 2) {sb.append("----").append(fName).append(method).append("\n");append(sb, callMethods, fName);callMethods.clear();}method = res;}}writeDown(f.getName(), sb.toString());} catch (Exception e) {System.out.println(e.getMessage());}}}private void append(StringBuilder sb, LinkedHashSet<String> callMethods, String fName) {for (String m : callMethods) {sb.append("--------");if (localMethod(m)) {sb.append(fName);}sb.append(m).append("\n");}}private boolean localMethod(String str) {int n = str.length(), leftParenthesis = -1;for (int i = 0; i < n; ++i) {if (leftParenthesis == -1 && str.charAt(i) == '.') {return false;}if (leftParenthesis == -1 && str.charAt(i) == '(') {leftParenthesis = i;}}return true;}private void writeDown(String fname, String content) {try (BufferedWriter bw = new BufferedWriter(new FileWriter(TARGET_ROOT_PATH + "\\" + fname))) {bw.write(content);} catch (Exception e) {e.fillInStackTrace();}}private String findMethodLine(char[] cs, LinkedHashSet<String> calledMethods, boolean mapperOrService) {int x = 0, n = cs.length;while (x < n && cs[x] == ' ') {++x;}return x == 2 ? getSpecialCharIndex(cs, mapperOrService) : (x > 4 ? findCalledMethods(cs, x, calledMethods) : null);}private String findCalledMethods(char[] cs, int x, LinkedHashSet<String> calledMethods) {// interfaceMethodStringBuilder sb = new StringBuilder();int n = cs.length;boolean canAppend = false, inParenthesis = false, simpleDataTypePrior = false;String typeMask = "";for (; x < n; ++x) {if (cs[x] == '/' && cs[x - 1] == '/') {if (cs[x + 2] == 'M' && compare2Arrays(cs, x + 2, METHOD_PREFIX)) {x += 8;canAppend = true;} else if (cs[x + 2] == 'I' && compare2Arrays(cs, x + 2, INTERFACE_METHOD_PREFIX)) {canAppend = true;x += 17;} else {return null;}continue;}if (cs[x] == '[' || (x + 1 < n && cs[x + 1] == ')' && cs[x] == ';')) {continue;}if (canAppend && cs[x] != ':') {if (cs[x] == '/') {sb.append('.');} else if (cs[x] == ';') {sb.append(typeMask).append(", ");typeMask = "";simpleDataTypePrior = false;} else {if (inParenthesis && cs[x - 1] == '[') {typeMask = "[]";}if (cs[x] == 'L' && (cs[x - 1] == '(' || cs[x - 1] == '[' || cs[x - 1] == ';')) {continue;}if ((cs[x - 1] == '(' || cs[x - 1] == ';' || simpleDataTypePrior || cs[x - 1] == '[') && dataType.containsKey(cs[x])) {simpleDataTypePrior = true;sb.append(dataType.get(cs[x]));if (cs[x - 1] == '[') {sb.append("[]");}if ((x + 1 < n && cs[x + 1] != ')') || (x + 1 < n && cs[x + 1] == ';' && cs[x + 2] != ')')) {sb.append(", ");}} else {sb.append(cs[x]);}}if (cs[x] == '(') {inParenthesis = true;}}if (cs[x] == ')') {break;}}if (sb.length() > 0) {calledMethods.add(sb.toString());}return null;}private boolean compare2Arrays(char[] a, int x, char[] b) {return Arrays.equals(a, x, x + b.length, b, 0, b.length);}private String getSpecialCharIndex(char[] cs, boolean mapperOrService) {int pre = 0, cnt = 0, leftParenthesis = -1;StringBuilder sb = new StringBuilder();for (int i = 2; i < cs.length; ++i) {if (leftParenthesis != -1) {sb.append(cs[i]);}if (leftParenthesis == -1 && cs[i] == ' ') {pre = i;++cnt;}if (leftParenthesis == -1 && cs[i] == '(') {leftParenthesis = i;i = pre;}if (cs[i] == ')') {break;}}return cnt > 1 ? sb.toString() : null;}
}
配置好 .class 文件的路径以及写入的目标路径后,执行代码,等待几秒后,就可以去看看写入的方法与被调方法信息了。
六、让方法与被调方法的关系可视化
为了更直观的表达各方法之间的调用关系。我们可以为此创建一个 web 页面,来展现这些方法与方法之间的调用关系。由于时间有限,目前只能向读者提供方法与方法之间的调用关系,后续会丰富功能,并向大家展示。
- web 页面
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>方法调用关系图</title><style>html {background: orange;}.container {margin: 0;padding: 0;text-align: center;flex-direction: column;}.list-box {margin-top: 10px;height: 80px;overflow-y: hidden;border: solid 5px lightgray;overflow-x: scroll;}.list-item {list-style: none;padding: 2px 3px;}ul {display: flex;}li > button:hover {background: black;color: white;}button {background: white;color: rgba(0, 0, 0, 0.6);padding: 4px 6px;border: none;cursor: pointer;border-radius: 3px;}.clicked_current {color: white;background: black;}</style>
</head>
<body>
<div class="container">
</div>
</body>
<script type="text/javascript">const XMLRequest = new XMLHttpRequest();let level = 0;window.onload = function () {const url = "http://localhost/methodGraph";request(url, 'get', false);XMLRequest.onreadystatechange = function () {if (XMLRequest.readyState === XMLHttpRequest.DONE && (XMLRequest.status === 200 || XMLRequest.status === 304)) {renderElements(JSON.parse(XMLRequest.responseText));}}XMLRequest.send(null);}function request(url, method, async) {XMLRequest.open(method, url, async);XMLRequest.setRequestHeader('Content-Type', 'application/json');}let curLevel = -1;function renderElements(arr) {const parentDom = document.querySelector(".container");const frag = document.createDocumentFragment();for (const item of arr) {const li = document.createElement("li");li.classList.add("list-item");const btn = document.createElement("button");btn.onclick = function () {curLevel = parseInt(this.parentNode.parentNode.classList[0].substring(5));const docs = document.querySelectorAll(".clicked_current");for (const doc of docs) {doc.classList.remove("clicked_current");}btn.classList.add("clicked_current");search(item);}btn.title = item;btn.textContent = getNameFromLongString(item);li.appendChild(btn);frag.append(li);}if (curLevel === level - 1) {if (arr.length > 0) {const div = document.createElement("div");div.classList.add("list-box");const ul = document.createElement("ul");ul.classList.add(`level${level++}`)ul.appendChild(frag);div.appendChild(ul);parentDom.appendChild(div);}} else {let rem = curLevel + 2;if (arr.length > 0) {const ulExist = document.querySelector(`.level${curLevel + 1}`);ulExist.innerHTML = "";ulExist.appendChild(frag);rem++;}while (parentDom.childNodes.length > rem) {const last = parentDom.childNodes.length;parentDom.removeChild(parentDom.childNodes[last - 1]);level--;}}}function getNameFromLongString(longName) {if (level === 0) {return longName.substring(longName.lastIndexOf('.') + 1);}longName = longName.substring(0, longName.indexOf('('));return longName.substring(longName.lastIndexOf('.') + 1);}function search(name) {const url = `http://localhost/findByName?name=${name}`;request(url, 'get', false);XMLRequest.onreadystatechange = function () {if (XMLRequest.readyState === XMLHttpRequest.DONE && (XMLRequest.status === 200 || XMLRequest.status === 304)) {renderElements(JSON.parse(XMLRequest.responseText));}}XMLRequest.send(null);}
</script>
</html>
- controller
@RequestMapping("/findByName")@ResponseBodypublic List<String> findByName(@RequestParam(name = "name", defaultValue = "unknown") String name) {return projectInformationService.findByClazzName(name);}
- 实现类
package com.example.develper.demos.service;import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.*;@Service
public class ProjectInformationServiceImpl implements ProjectInformationService{Map<String, Map<String, List<String>>> name2Clazz;// 在 properties 中配置之前保存的那个目录里@Value("${methodConnection.analyze.target.path}")private String methodConnectionPath;private void initialized() {if (name2Clazz != null) { return; }if (methodConnectionPath == null) {throw new IllegalArgumentException("请配置已经解析好的方法关系网络文档路径!");}name2Clazz = new HashMap<>();File file = new File(methodConnectionPath);if (!file.exists()) {throw new IllegalArgumentException("请配置正确的文档路径!");}loadsClazzInfo(file);}private void loadsClazzInfo(File file) {File[] files = file.listFiles();for (File f : files) {String name = f.getName().substring(0, f.getName().length() - 4);Map<String, List<String>> method2CalledMethods = new HashMap<>();name2Clazz.put(name, method2CalledMethods);try (BufferedReader br = new BufferedReader(new FileReader(f))) {String line;String methodName = null;while ((line = br.readLine()) != null) {String[] ret = countPlaceholder(line);if ("4".equals(ret[0])) {methodName = ret[1];method2CalledMethods.put(methodName, new ArrayList<>());} else if (methodName != null) {method2CalledMethods.get(methodName).add(ret[1]);}}} catch (Exception e) {e.fillInStackTrace();}}}private String[] countPlaceholder(String line) {int x = 0, cnt = 0, n = line.length();StringBuilder sb = new StringBuilder();while (x < n) {if (line.charAt(x) == '-') {++cnt;} else {sb.append(line.charAt(x));}++x;}String[] ret = new String[2];ret[0] = Integer.toString(cnt);ret[1] = sb.toString();return ret;}@Overridepublic List<String> loadAllControllers() {initialized();List<String> ret = new ArrayList<>();for (String name : name2Clazz.keySet()) {if (name.endsWith("Controller")) {ret.add(name);}}return ret;}@Overridepublic List<String> findByClazzName(String name) {// cn.com.xx.xx.common.Base58.encode(if (name == null || name.trim().isEmpty()) {return new ArrayList<>();}if (name.endsWith("Controller")) {return new ArrayList<>(name2Clazz.getOrDefault(name, new HashMap<>()).keySet());}char[] cs = name.toCharArray();StringBuilder prefix = new StringBuilder();StringBuilder suffix = new StringBuilder();int i = 0;while (i < cs.length && cs[i] != '(') {if (cs[i] == '.') {prefix.append(prefix.length() > 0 ? '.' : "").append(suffix);suffix.setLength(0);} else {suffix.append(cs[i]);}++i;}while (i < cs.length) {suffix.append(cs[i++]);}char[] service = {'S', 'e', 'r', 'v', 'i', 'c', 'e'};int x = prefix.length() - 1, k = service.length - 1;while (k >= 0 && prefix.charAt(x) == service[k]) {--x; --k;}String clazzName = prefix.toString();if (k == -1) {x = prefix.length();while (prefix.charAt(x - 1) != '.') {--x;}prefix.insert(x, "impl.");clazzName = prefix.append("Impl").toString();name = prefix.append(".").append(suffix).toString();}Map<String, List<String>> clazz2Methods = name2Clazz.getOrDefault(clazzName, new HashMap<>());return clazz2Methods.getOrDefault(name, new ArrayList<>());}
}
七、结语
创作不易,期待读者的支持。web 页面效果不是很理想,后续会持续更新。毕竟这个功能给我平时的工作帮助挺大的。况且先前说的根据方法找菜单的功能并没有完全实现,但就目前的方向来看,一定是正确的。
相关文章:

如何用Java写一个整理Java方法调用关系网络的程序
大家好,我是猿码叔叔,一位 Java 语言工作者,也是一位算法学习刚入门的小学生。很久没有为大家带来干货了。 最近遇到了一个问题,大致是这样的:如果给你一个 java 方法,如何找到有哪些菜单在使用。我的第一想…...

基于STM32设计的管道有害气体检测装置(ESP8266局域网)176
基于STM32设计的管道有害气体检测装置(176) 文章目录 一、前言1.1 项目介绍【1】项目功能介绍【2】项目硬件模块组成【3】ESP8266模块配置【4】上位机开发思路【5】项目模块划分【6】LCD显示屏界面布局【7】上位机界面布局1.2 项目功能需求1.3 项目开发背景1.4 开发工具的选择1…...

iCloud照片库全指南:云端存储与智能管理
iCloud照片库全指南:云端存储与智能管理 在数字化时代,照片和视频成为了我们生活中不可或缺的一部分。随着手机摄像头质量的提升,我们记录生活点滴的方式也越来越丰富。然而,这也带来了一个问题:如何有效管理和存储日…...

IDEA中使用Maven打包及碰到的问题
1. 项目打包 IDEA中,maven打包的方式有两种,分别是 install 和 package ,他们的区别如下: install 方式 install 打包时做了两件事,① 将项目打包成 jar 或者 war,打包结果存放在项目的 target 目录下。…...

TreeMap、HashMap 和 LinkedHashMap 的区别
TreeMap、HashMap 和 LinkedHashMap 的区别 1、HashMap2、LinkedHashMap3、TreeMap4、总结 💖The Begin💖点点关注,收藏不迷路💖 在 Java 中,TreeMap、HashMap 和 LinkedHashMap 是三种常用的集合类,它们在…...

【跟我学K8S】45天入门到熟练详细学习计划
目录 一、什么是K8S 核心功能 架构组件 使用场景 二、入门到熟练的学习计划 第一周:K8s基础和概念 第二周:核心对象和网络 第三周:进阶使用和管理 第四周:CI/CD集成和监控 第五周:实战模拟和案例分析 第六周…...

ubuntu下载Nginx
一、Nginx下载安装(Ubuntu系统) 1.nginx下载 sudo apt-get install nginx2.nginx启动 启动命令 sudo nginx重新编译(每次更改完nginx配置文件后运行): sudo nginx -s reload3.测试nginx是否启动成功 打开浏览器访问本机80端口…...

【区分vue2和vue3下的element UI Dialog 对话框组件,分别详细介绍属性,事件,方法如何使用,并举例】
在 Vue 2 和 Vue 3 中,Element UI(针对 Vue 2)和 Element Plus(针对 Vue 3)提供了 Dialog 对话框组件,用于在页面中显示模态对话框。这两个库中的 Dialog 组件在属性、事件和方法的使用上有所相似ÿ…...

docker push 推送镜像到阿里云仓库
1.登陆阿里云 镜像服务,跟着指引操作就行 创建个人实例,创建命名空间、镜像仓库,绑定代码源头 2.将镜像推送到Registry $ docker login --username*** registry.cn-beijing.aliyuncs.com $ docker tag [ImageId] registry.cn-beijing.aliy…...

伯克利、斯坦福和CMU面向具身智能端到端操作联合发布开源通用机器人Policy,可支持多种机器人执行多种任务
不同于LLM或者MLLM那样用于上百亿甚至上千亿参数量的大模型,具身智能端到端大模型并不追求参数规模上的大,而是指其能吸收大量的数据,执行多种任务,并能具备一定的泛化能力,如笔者前博客里的RT1。目前该领域一个前沿工…...

昇思25天学习打卡营第17天(+1)|Diffusion扩散模型
1. 学习内容复盘 本文基于Hugging Face:The Annotated Diffusion Model一文翻译迁移而来,同时参考了由浅入深了解Diffusion Model一文。 本教程在Jupyter Notebook上成功运行。如您下载本文档为Python文件,执行Python文件时,请确…...

【Leetcode笔记】406.根据身高重建队列
文章目录 1. 题目要求2.解题思路 注意3.ACM模式代码 1. 题目要求 2.解题思路 首先,按照每个人的身高属性(即people[i][0])来排队,顺序是从大到小降序排列,如果遇到同身高的,按照另一个属性(即p…...

Linux 安装pdfjam (PDF文件尺寸调整)
跟Ghostscript搭配使用,这样就可以将不同尺寸的PDF调整到相同尺寸合并了。 在 CentOS 上安装 pdfjam 需要安装 TeX Live,因为 pdfjam 是基于 TeX Live 的。以下是详细的步骤来安装 pdfjam: ### 步骤 1: 安装 EPEL 仓库 首先,安…...

python+playwright 学习-90 and_ 和 or_ 定位
前言 playwright 从v1.34 版本以后支持and_ 和 or_ 定位 XPath 中的and和or xpath 语法中我们常用的有text()、contains() 、ends_with()、starts_with() //*[text()="文本"] //*[contains(@id, "xx")] //...

亲子时光里的打脸高手,贾乃亮与甜馨的父爱如山
贾乃亮这波操作,简直是“实力打脸”界的MVP啊! 7月5号,他一甩手,甩出张合照, 瞬间让多少猜测纷飞的小伙伴直呼:“脸疼不?”带着咱家小甜心甜馨, 回了哈尔滨老家,这趟亲…...

MySQL篇-SQL优化实战
SQL优化措施 通过我们日常开发的经验可以整理出以下高效SQL的守则 表主键使用自增长bigint加适当的表索引,需要强关联字段建表时就加好索引,常见的有更新时间,单号等字段减少子查询,能用表关联的方式就不用子查询,可…...

【MySQL备份】Percona XtraBackup总结篇
目录 1.前言 2.问题总结 2.1.为什么在恢复备份前需要准备备份 2.1.1. 保证数据一致性 2.1.2. 完成崩溃恢复过程 2.1.3. 解决非锁定备份的特殊需求 2.1.4. 支持增量和差异备份 2.1.5. 优化恢复性能 2.2.Percona XtraBackup的工作原理 3.注意事项 1.前言 在历经了详尽…...

【Git 】规范 Git 提交信息的工具 Commitizen
Commitizen是一个用于规范Git提交信息的工具,它旨在帮助开发者生成符合一定规范和风格的提交信息,从而提高代码维护的效率,便于追踪和定位问题。以下是对Commitizen的详细介绍。 1、Commitizen的作用与优势 规范提交信息:通过提供…...

ABB PPC902AE1013BHE010751R0101控制器 处理器 模块
ABB PPC902AE1013BHE010751R0101 该模块是用于自动化和控制系统的高性能可编程控制器。它旨在与其他自动化和控制设备一起使用,以提供完整的系统解决方案 是一种数字输入/输出模块,提供了高水平的性能和可靠性。它专为苛刻的工业应用而设计,…...

大模型AIGC转行记录(一)
自从22年11月chat gpt上线以来,这一轮的技术浪潮便变得不可收拾。我记得那年9月份先是在技术圈内讨论,然后迅速地,全社会在讨论,各个科技巨头、金融机构、政府部门快速跟进。 软件开发行业过去与现状 我19年决定转码的时候&…...

element-ui Tree之懒加载叶子节点强制设置父级半选效果
效果: 前言: 我们是先只展示一级的,二级的数据是通过点击之后通过服务器获取数据,并不是全量数据直接一起返回回来的。 问题: 当你设置了默认选中的子节点,但是由于刚进入页面此时tree中数据暂是没有这个…...

Java项目:基于SSM框架实现的高校共享单车管理系统分前后台【ssm+B/S架构+源码+数据库+开题报告+任务书+毕业论文】
一、项目简介 本项目是一套基于SSM框架实现的高校共享单车管理系统 包含:项目源码、数据库脚本等,该项目附带全部源码可作为毕设使用。 项目都经过严格调试,eclipse或者idea 确保可以运行! 该系统功能完善、界面美观、操作简单、…...

【Android】自定义换肤框架02之自定义AssetManager和Resource
ResourceId是如何变成对应Resource的 在上一章中,我们已经讲过,apk中有个资源索引文件 其中保存了每个资源对应的id,name,type,path 资源文件的解析,主要涉及两个类,AssetManager和Resource …...

熵权法、熵值法、熵权TOPSIS三种方法的实用场景及优劣比较
在统计分析与决策分析中,熵权法、熵值法和熵权TOPSIS是三种常用的基于信息熵理论的方法。这些方法在处理多属性或多指标决策问题时,能够客观地反映数据的内在特性和各指标的相对重要性,为科学决策提供了有力支持。 熵权法 实用场景 熵权法是一种基于信息熵的客观赋权方法…...

无人机人员搜救
人员搜救-水域救援 水域搜救:快速水面搜查 物资抛投:救生物资抛投 绳索牵引:牵引救援绳索 领航船艇:水面侦察领航 人员搜救 昼夜搜救,精准定位 水域搜救 经纬 M300 RTK 搭载禅思 H20T 能够满足全天候作业需求&a…...

目标检测算法
讯飞星火 目标检测算法是计算机视觉领域的一项核心技术,用于在数字图像或视频中识别和定位一个或多个特定对象实例。这些算法通常需要确定对象的位置(例如通过边界框)和类别(例如人、汽车、狗等)。以下是一些流行的目…...

SpringSecurity 三更草堂学习笔记
0.简介 Spring Security是Spring家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。 一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多,因为相比与Spring…...

鸿蒙生态应用开发白皮书V3.0
来源:华为: 近期历史回顾:...

CSS - 深入理解选择器的使用方式
CSS基本选择器 通配选择器元素选择器类选择器id 选择器 通配选择器 作用:可以选中所有HTML元素。语法: * {属性名;属性值; }举例: /* 选中所有元素 */ * {color: orange;font-size: 40px; }在清除样式方面有很大作用 元素选择器…...

动手学深度学习(Pytorch版)代码实践 -循环神经网络-54~55循环神经网络的从零开始实现和简洁实现
54循环神经网络的从零开始实现 import math import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2l import matplotlib.pyplot as plt import liliPytorch as lp# 读取H.G.Wells的时光机器数据集 batch_size, num_steps 32, …...