如何用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年决定转码的时候&…...
Prompt Tuning、P-Tuning、Prefix Tuning的区别
一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...
FastAPI 教程:从入门到实践
FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API,支持 Python 3.6。它基于标准 Python 类型提示,易于学习且功能强大。以下是一个完整的 FastAPI 入门教程,涵盖从环境搭建到创建并运行一个简单的…...
五年级数学知识边界总结思考-下册
目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解:由来、作用与意义**一、知识点核心内容****二、知识点的由来:从生活实践到数学抽象****三、知识的作用:解决实际问题的工具****四、学习的意义:培养核心素养…...
NLP学习路线图(二十三):长短期记忆网络(LSTM)
在自然语言处理(NLP)领域,我们时刻面临着处理序列数据的核心挑战。无论是理解句子的结构、分析文本的情感,还是实现语言的翻译,都需要模型能够捕捉词语之间依时序产生的复杂依赖关系。传统的神经网络结构在处理这种序列依赖时显得力不从心,而循环神经网络(RNN) 曾被视为…...
(转)什么是DockerCompose?它有什么作用?
一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器。 Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...
零基础在实践中学习网络安全-皮卡丘靶场(第九期-Unsafe Fileupload模块)(yakit方式)
本期内容并不是很难,相信大家会学的很愉快,当然对于有后端基础的朋友来说,本期内容更加容易了解,当然没有基础的也别担心,本期内容会详细解释有关内容 本期用到的软件:yakit(因为经过之前好多期…...
Unsafe Fileupload篇补充-木马的详细教程与木马分享(中国蚁剑方式)
在之前的皮卡丘靶场第九期Unsafe Fileupload篇中我们学习了木马的原理并且学了一个简单的木马文件 本期内容是为了更好的为大家解释木马(服务器方面的)的原理,连接,以及各种木马及连接工具的分享 文件木马:https://w…...
让回归模型不再被异常值“带跑偏“,MSE和Cauchy损失函数在噪声数据环境下的实战对比
在机器学习的回归分析中,损失函数的选择对模型性能具有决定性影响。均方误差(MSE)作为经典的损失函数,在处理干净数据时表现优异,但在面对包含异常值的噪声数据时,其对大误差的二次惩罚机制往往导致模型参数…...
论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
