【Unity】通用GM QA工具 运行时数值修改 命令行 测试工具
GM工具使用:



GM工具通常用于游戏运行时修改数值(加钱/血量)、解锁关卡等,用于快速无死角测试游戏。一个通用型GM工具对于游戏项目是非常实用且必要的,但通用不能向易用妥协,纯命令行GM门槛太高,对QA不友好。
这类运行时命令行工具实现原理很简单,主要是通过给变量或方法添加Attribute标识,然后通过反射获取被标记的变量或方法,命令触发通过反射为变量赋值或Invoke方法。
此类工具免费或付费的已经泛滥了,不推荐浪费时间重复造轮子。
1. 免费开源的Log显示工具,也嵌入了命令行功能。由于GF有更好用的Debuger窗口了,所以没选择它:https://github.com/yasirkula/UnityIngameDebugConsole
2. Quantum Console, 收费,AssetStore上好评最多,但强行绑定了一个UGUI界面,无解耦。这里我是想直接扩展进GF Debuger窗口,方便使用,因此需要修改插件源码:Quantum Console | Utilities Tools | Unity Asset Store
感兴趣的话直接AssetStore搜“command console”,免费的也有很多。
我就不浪费时间筛选,直接选择购买好评较多的Quantum Console进行整改。
Quantum Console用法:
Quantum C默认只对继承MonoBehavior的脚本有效,应该是因为需要反射获取所有类型速度太慢,初始化时会卡顿。
对于继承自MonoBehavior的脚本直接通过以下Attribute标记命令即可:
1. 命令行前缀,CommandPrefix("GM."):
相当于给命令行分组,比如把所有命令行标记个前缀叫“GM.”, 那么输入"GM"时所有GM开头的命令都会在列表中显示出来。
[CommandPrefix("GM.玩家.")]
public class PlayerEntity
{
}
2. 把变量或方法作为命令行,Command("命令名字", "命令用法说明"):
[Command("移动速度", "float类型,默认值10")]
private float moveSpeed = 10f;
[Command("添加敌人", "参数int,创建敌人个数")]
internal void AddEnemies(int v)
对于非MonoBehavior脚本需要手动调用注册命令接口,将该类型添加到需要反射扫描的名单里:
1. QuantumRegistry.RegisterObject()和QuantumRegistry.DeregisterObject()注册或取消注册,然后通过Command("命令名字", "命令描述", MonoTargetType.Registry)添加命令:
public class PlayerDataModel : DataModelBase
{protected override void OnCreate(RefParams userdata){QuantumRegistry.RegisterObject(this);}protected override void OnRelease(){QuantumRegistry.DeregisterObject(this);}[Command("金币", "玩家金币数量", MonoTargetType.Registry)]public int Coins;
}
将Quantum C扩展进GF:
由于GF解耦做得非常好了,我们只需要自定义类实现GameFramework.Debugger.IDebuggerWindow接口就可以写自己的GUI界面和功能了。
1. 扩展Debuger菜单栏,编写GM工具交互界面:
using System.Collections.Generic;
using UnityEngine;
using GameFramework;
using GameFramework.Debugger;
using System;
using Cysharp.Threading.Tasks;
using GM.Utilities;
using System.Threading.Tasks;
using System.Reflection;
using System.Linq;
namespace GM
{public class GMConsoleWindow : IDebuggerWindow{const string LogCommand = "{0}";const string LogSuccess = "<color=#2BD988>{0}</color>";const string LogFailed = "<color=#F22E2E>{0}</color>";const string InputFieldCtrlID = "Input";private int m_MaxLine = 100;private int m_MaxRecordInputHistory = 30;private Queue<GMLogNode> m_LogNodes;private LinkedList<string> m_InputHistoryList;private LinkedListNode<string> m_CurrentHistory = null;string m_InputText;string m_PreInputText;bool m_InputFocused;bool m_InputChanged;Vector2 m_ScrollPosition = Vector2.zero;Vector2 m_FilterScrollPosition = Vector2.zero;SuggestionStack m_CommandsFilter;SuggestorOptions m_FilterOptions;Rect inputRect = default;bool m_LogAppend;bool m_MoveCursorToEnd;GUIStyle m_CommandsFilterBtStyle;private readonly Type m_VoidTaskType = typeof(Task<>).MakeGenericType(Type.GetType("System.Threading.Tasks.VoidTaskResult"));private List<System.Threading.Tasks.Task> m_CurrentTasks;private List<IEnumerator<ICommandAction>> m_CurrentActions;public void Initialize(params object[] args){if (!QuantumConsoleProcessor.TableGenerated){QuantumConsoleProcessor.GenerateCommandTable(true);}m_InputHistoryList = new LinkedList<string>();m_LogNodes = new Queue<GMLogNode>();m_CurrentTasks = new List<System.Threading.Tasks.Task>();m_CurrentActions = new List<IEnumerator<ICommandAction>>();m_CommandsFilter = new SuggestionStack();m_FilterOptions = new SuggestorOptions(){CaseSensitive = false,CollapseOverloads = true,Fuzzy = true,};}public void OnDraw(){if (m_CommandsFilterBtStyle == null){m_CommandsFilterBtStyle = new GUIStyle(GUI.skin.button){alignment = TextAnchor.MiddleLeft};}GUILayout.BeginVertical();{m_ScrollPosition = GUILayout.BeginScrollView(m_ScrollPosition, "box");{foreach (var logNode in m_LogNodes){GUILayout.Label(logNode.LogMessage);}GUILayout.EndScrollView();}if (m_LogAppend){m_LogAppend = false;m_ScrollPosition = new Vector2(0, float.MaxValue);}GUILayout.BeginHorizontal();{GUI.enabled = QuantumConsoleProcessor.TableGenerated;GUI.SetNextControlName(InputFieldCtrlID);m_InputText = GUILayout.TextField(m_InputText);if (Event.current.type == EventType.Repaint){inputRect = GUILayoutUtility.GetLastRect();if (m_MoveCursorToEnd){m_MoveCursorToEnd = false;MoveInputCursorToEnd();}}m_InputFocused = (GUI.GetNameOfFocusedControl() == InputFieldCtrlID);m_InputChanged = m_InputText != m_PreInputText;if (m_InputChanged){m_PreInputText = m_InputText;m_CommandsFilter.UpdateStack(m_InputText, m_FilterOptions);}if (GUILayout.Button("Execute", GUILayout.Width(60))){ExecuteCommand(m_InputText);}if (GUILayout.Button("Clear", GUILayout.Width(60))){ClearLogs();}GUILayout.EndHorizontal();}GUILayout.EndVertical();if (m_InputFocused && m_CommandsFilter.TopmostSuggestionSet != null){if (Event.current.type == EventType.Repaint){float maxHeight = GUILayoutUtility.GetLastRect().height - inputRect.height - 5f;inputRect.height = Mathf.Clamp(m_CommandsFilter.TopmostSuggestionSet.Suggestions.Count * 30, maxHeight * 0.5f, maxHeight);inputRect.position -= Vector2.up * (inputRect.height + 5f);}if (m_InputChanged){m_FilterScrollPosition = Vector2.zero;}GUILayout.BeginArea(inputRect);m_FilterScrollPosition = GUILayout.BeginScrollView(m_FilterScrollPosition, "box", GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));{GUILayout.BeginVertical(GUILayout.ExpandHeight(true));{foreach (var item in m_CommandsFilter.TopmostSuggestionSet.Suggestions){if (GUILayout.Button(item.FullSignature, m_CommandsFilterBtStyle)){m_MoveCursorToEnd = true;var fragments = m_InputText.Split(' ');if (fragments.Length >= 2){m_InputText = string.Empty;for (int i = 0; i < fragments.Length - 1; i++){m_InputText = Utility.Text.Format("{0}{1}{2}", m_InputText, i == 0 ? string.Empty : " ", fragments[i]);}m_InputText = Utility.Text.Format("{0} {1}", m_InputText, item.PrimarySignature);}else{m_InputText = item.PrimarySignature;}}}GUILayout.EndVertical();}GUILayout.EndScrollView();}GUILayout.EndArea();}}}/// <summary>/// 输入框游标移动到尾部/// </summary>private void MoveInputCursorToEnd(){GUI.FocusControl(InputFieldCtrlID);// 获取当前TextEditorTextEditor editor = (TextEditor)GUIUtility.GetStateObject(typeof(TextEditor), GUIUtility.keyboardControl);if (editor != null){editor.cursorIndex = m_InputText.Length;editor.selectIndex = m_InputText.Length;}}public void OnEnter(){QuantumRegistry.RegisterObject<GMConsoleWindow>(this);}public void OnLeave(){QuantumRegistry.DeregisterObject<GMConsoleWindow>(this);}public void OnUpdate(float elapseSeconds, float realElapseSeconds){if (m_InputFocused){if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter))ExecuteCommand(m_InputText);if (Input.GetKeyDown(KeyCode.DownArrow)){SelectInputHistory(false);}else if (Input.GetKeyDown(KeyCode.UpArrow)){SelectInputHistory(true);}}TasksUpdate();ActionsUpdate();}public void Shutdown(){}private void SelectInputHistory(bool upOrdown){if (m_InputHistoryList.Count == 0) return;m_MoveCursorToEnd = true;if (upOrdown){if (m_CurrentHistory == null || m_CurrentHistory.Previous == null){m_InputText = m_InputHistoryList.Last.Value;m_CurrentHistory = m_InputHistoryList.Last;return;}m_InputText = m_CurrentHistory.Previous.Value;m_CurrentHistory = m_CurrentHistory.Previous;}else{if (m_CurrentHistory == null || m_CurrentHistory.Next == null){m_InputText = m_InputHistoryList.First.Value;m_CurrentHistory = m_InputHistoryList.First;return;}m_InputText = m_CurrentHistory.Next.Value;m_CurrentHistory = m_CurrentHistory.Next;}}private void AppendLog(GMLogType logType, string logMessage){m_LogNodes.Enqueue(GMLogNode.Create(logType, logMessage));while (m_LogNodes.Count > m_MaxLine){ReferencePool.Release(m_LogNodes.Dequeue());}m_LogAppend = true;}[Command("clear", "清空GM日志", MonoTargetType.Registry)]private void ClearLogs(){m_LogNodes.Clear();m_ScrollPosition = Vector2.zero;}private void ExecuteCommand(string cmd, bool recordHistory = true){if (string.IsNullOrWhiteSpace(cmd)) return;if (recordHistory) RecordInputHistory(cmd);AppendLog(GMLogType.Command, cmd);m_InputText = string.Empty;try{var commandResult = QuantumConsoleProcessor.InvokeCommand(cmd);if (commandResult != null){if (commandResult is IEnumerator<ICommandAction> enumeratorTp){m_CurrentActions.Add(enumeratorTp);ActionsUpdate();}else if (commandResult is IEnumerable<ICommandAction> enumerableTp){m_CurrentActions.Add(enumerableTp.GetEnumerator());ActionsUpdate();}else if (commandResult is UniTask task){m_CurrentTasks.Add(task.AsTask());}else if (commandResult.GetType().Name == "UniTask`1"){var asTaskGenericMethod = typeof(UniTaskExtensions).GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(item => item.Name == "AsTask" && item.IsGenericMethod);Type uniTaskType = commandResult.GetType();Type genericArgument = uniTaskType.GetGenericArguments()[0];MethodInfo genericMethod = asTaskGenericMethod.MakeGenericMethod(genericArgument);Task taskT = (Task)genericMethod.Invoke(null, new object[] { commandResult });m_CurrentTasks.Add(taskT);}else{var resultType = commandResult.GetType();if (resultType == typeof(string) || resultType.IsPrimitive){AppendLog(GMLogType.Success, commandResult.ToString());}else{AppendLog(GMLogType.Success, Utility.Json.ToJson(commandResult));}}}}catch (System.Reflection.TargetInvocationException e){AppendLog(GMLogType.Failed, e.Message);}catch (Exception e){AppendLog(GMLogType.Failed, e.Message);}}private void RecordInputHistory(string cmd){if (m_InputHistoryList.Count > 0 && m_InputHistoryList.First.Value == cmd) return;m_InputHistoryList.AddFirst(cmd);m_CurrentHistory = m_InputHistoryList.Last;while (m_InputHistoryList.Count > m_MaxRecordInputHistory){m_InputHistoryList.RemoveLast();}}private void TasksUpdate(){for (int i = m_CurrentTasks.Count - 1; i >= 0; i--){if (m_CurrentTasks[i].IsCompleted){if (m_CurrentTasks[i].IsFaulted){foreach (Exception e in m_CurrentTasks[i].Exception.InnerExceptions){AppendLog(GMLogType.Failed, e.Message);}}else{Type taskType = m_CurrentTasks[i].GetType();if (taskType.IsGenericTypeOf(typeof(Task<>)) && !m_VoidTaskType.IsAssignableFrom(taskType)){System.Reflection.PropertyInfo resultProperty = m_CurrentTasks[i].GetType().GetProperty("Result");object result = resultProperty.GetValue(m_CurrentTasks[i]);string log = Utility.Json.ToJson(result);AppendLog(GMLogType.Success, log);}}m_CurrentTasks.RemoveAt(i);}}}private void ActionsUpdate(){for (int i = m_CurrentActions.Count - 1; i >= 0; i--){IEnumerator<ICommandAction> action = m_CurrentActions[i];try{if (action.Execute() != ActionState.Running){m_CurrentActions.RemoveAt(i);}}catch (Exception e){m_CurrentActions.RemoveAt(i);AppendLog(GMLogType.Failed, e.Message);break;}}}private enum GMLogType{Command,Success,Failed}/// <summary>/// 日志记录结点。/// </summary>private sealed class GMLogNode : IReference{private GMLogType m_LogType;private string m_LogMessage;/// <summary>/// 初始化日志记录结点的新实例。/// </summary>public GMLogNode(){m_LogType = GMLogType.Failed;m_LogMessage = null;}/// <summary>/// 获取日志类型。/// </summary>public GMLogType LogType{get{return m_LogType;}}/// <summary>/// 获取日志内容。/// </summary>public string LogMessage{get{return m_LogMessage;}}/// <summary>/// 创建日志记录结点。/// </summary>/// <param name="logType">日志类型。</param>/// <param name="logMessage">日志内容。</param>/// <returns>创建的日志记录结点。</returns>public static GMLogNode Create(GMLogType logType, string logMessage){GMLogNode logNode = ReferencePool.Acquire<GMLogNode>();logNode.m_LogType = logType;switch (logType){case GMLogType.Success:logNode.m_LogMessage = Utility.Text.Format(LogSuccess, logMessage);break;case GMLogType.Failed:logNode.m_LogMessage = Utility.Text.Format(LogFailed, logMessage);break;default:logNode.m_LogMessage = Utility.Text.Format(LogCommand, logMessage);break;}return logNode;}/// <summary>/// 清理日志记录结点。/// </summary>public void Clear(){m_LogType = GMLogType.Failed;m_LogMessage = null;}}}
}
2. 将自定义的GM工具界面注册进GF Debuger窗口:
GF.Debugger.RegisterDebuggerWindow("GM", new GM.GMConsoleWindow());
效果:

相关文章:
【Unity】通用GM QA工具 运行时数值修改 命令行 测试工具
GM工具使用: GM工具通常用于游戏运行时修改数值(加钱/血量)、解锁关卡等,用于快速无死角测试游戏。一个通用型GM工具对于游戏项目是非常实用且必要的,但通用不能向易用妥协,纯命令行GM门槛太高,对QA不友好。 这类运行时命令行工具…...
[Spring] Spring原理(SpringBoot完结)
🌸个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 🏵️热门专栏: 🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 🍕 Collection与…...
python | rq,一个无敌的 关于Redis 的Python 库!
本文来源公众号“python”,仅用于学术分享,侵权删,干货满满。 原文链接:rq,一个无敌的 Python 库! 大家好,今天为大家分享一个无敌的 Python 库 - rq。 Github地址:https://githu…...
Redis的缓存淘汰策略
1. 查看Redis 最大的占用内存 打开redis配置文件, 设置maxmemory参数,maxmemory 是bytes字节类型, 注意转换 2. Redis默认内存多少可以用 注意: 在64bit系统下, maxmemory 设置为 0 表示不限制Redis内存使用 3. 一般生产上如何配置 一般推荐Redis 设置内…...
【C++】深度解析:用 C++ 模拟实现 priority_queue类,探索其底层实现细节(仿函数、容器适配器)
目录 ⭐前言 ✨堆 ✨容器适配器 ✨仿函数 ⭐priority_queue介绍 ⭐priority_queue参数介绍 ⭐priority_queue使用 ⭐priority_queue实现 ✨仿函数实现 ✨堆的向上调整和向下调整 ✨完整代码 ⭐前言 ✨堆 堆是一种特殊的树形数据结构,通常以二叉树的…...
1个人躲,5个人抓!《极限竞速:地平线5》全新游戏模式“捉迷藏”即将推出
风靡全球的赛车竞速游戏《极限竞速:地平线5》即将推出全新游戏模式——捉迷藏(Hide & Seek)。 《极限竞速:地平线5》日前发布了全新预告,展示了即将于 9 月 10 日推出的捉迷藏模式游戏玩法。该预告是日前举办的2024 年科隆国际游戏展 Xb…...
ARCGIS XY坐标excel转要素面
1、准备好excel 坐标 excel文件转为csv才能识别,CSV只能保留第一个工作表并且,不会保留格式。 2、在ArcGis中导入XY事件图层 创建XY事件图层 图层要素赋对象ID 将导入的图层导出为先新的图层,这样就给每个要素附加了唯一的值 选择点集转线…...
MyBatis源码系列3(解析配置文件,创建SqlSessionFactory对象)
创建SqlSessionFactory; 首先读取配置文件,使用构造者模式创建SqlSessionFactory对象。 InputStream inputStream Resources.getResourceAsStream("mybatis-config.xml");SqlSessionFactory sqlSessionFactory new SqlSessionFactoryBuilder…...
企业级web应用服务器tomcat
目录 一、Web技术 1.1 HTTP协议和B/S 结构 1.2 前端三大核心技术 1.2.1 HTML 1.2.2 CSS(Cascading Style Sheets)层叠样式表 1.2.3 JavaScript 二、tomcat的功能介绍 2.1 安装 tomcat 环境准备 2.1.1 安装java环境 2.1.2 安装并启动tomcat …...
深入浅出,探讨IM(即时通讯-聊天工具)技术架构及用户界面设计
在数字化时代的浪潮中,即时通讯(IM)工具已然成为人们日常沟通的重要方式。从微信、QQ到飞信钉、喧喧IM、企业微信、钉钉、Slack,这些IM工具不仅为我们提供了便捷的沟通方式,更在技术架构和用户界面设计上展现了独特的魅…...
小米、友邦带领恒指大反攻!
港股三大指数反弹止步2连跌,恒生科技指数一度冲高至2%,恒指收涨1.44%。盘面上,大型科技股多数表现活跃,业绩超预期,小米大涨超8%表现尤其抢眼,京东涨约4%,百度涨1.71%,网易涨2.14%&a…...
中国植物性状数据库
中国植物性状的研究主要集中在植物的生理结构和功能,以及它们对环境的适应性上。中国植物性状的多样性体现在多个方面,包括植物的生理结构、生长习性、以及对环境的适应性等。 中国植物性状数据库,包含了来自140个样点的1529种植物…...
[数据集][目标检测]街灯路灯检测数据集VOC+YOLO格式1893张1类别
数据集格式:Pascal VOC格式YOLO格式(不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数):1893 标注数量(xml文件个数):1893 标注数量(txt文件个数):1893 标注…...
C++位运算
C位运算 运算符 & 按位与 如果两个相应的二进制位都为1,则该位的结果值为1,否则为0 | 按位或 两个相应的二进制位中只要有一个为1,该位的结果值为1 ^ 按位异或 若参加运算的两个二进制位值相同则为0,否则为1 ~ 取反 ~是一元…...
Day97:云上攻防-云原生篇KubernetesK8s安全APIKubelet未授权访问容器执行
知识点: 1、云原生-K8s安全-名词架构&各攻击点 2、云原生-K8s安全-Kubelet未授权访问 3、云原生-K8s安全-API Server未授权访问 K8S集群 Kubernetes是一个开源的,用于编排云平台中多个主机上的容器化的应用,目标是让部署容器化的应用…...
招聘|头部云厂商招 PG 核心骨干 DBA【上海】
我们的招聘专区又回来了!🏃 Bytebase 作为先进的数据库 DevOps 团队协同工具 🔧,用户群里汇聚了 💗 业界优秀的 DBA,SRE,运维的同学们 🌟。 上周用户群里有小伙伴发招聘信息 &…...
继承(下)【C++】
文章目录 子类继承父类之后,子类的默认成员函数的变化构造函数编译器自动生成的构造函数程序员手动写的构造函数 拷贝构造编译器自动生成的拷贝构造函数程序员手动写的拷贝构造函数 赋值重载编译器自动生成的赋值重载程序员手动写的赋值重载 析构函数 继承与友元菱形…...
AI模拟器
一、介绍 基于鸿蒙Next模拟一个ai对话过程二、场景需求 客户服务、数据分析、个性化推荐、图像和视频处理、智能家居、交通管理、教育行业、制造等等。 三、业务步骤 第一步:输入框提出问题,发送问题, 第二部:下次发送࿰…...
【C++二分查找 前缀和】1658. 将 x 减到 0 的最小操作数
本文涉及的基础知识点 C二分查找 C算法:前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 LeetCode1658. 将 x 减到 0 的最小操作数 给你一个整数数组 nums 和一个整数 x 。每一次操作时,你应当移除数组 nums 最左边或最右边的元素&am…...
验证实战知识点--(2)
1.seq中的pre_start pre_start 是 uvm_sequence 类的一个虚拟方法,用于在序列开始执行之前进行初始化和设置。这个方法在调用 start 方法前立即执行,提供了一个执行自定义初始化代码的机会。 start 方法用于启动序列的执行,而 pre_start 可以…...
Android Wi-Fi 连接失败日志分析
1. Android wifi 关键日志总结 (1) Wi-Fi 断开 (CTRL-EVENT-DISCONNECTED reason3) 日志相关部分: 06-05 10:48:40.987 943 943 I wpa_supplicant: wlan0: CTRL-EVENT-DISCONNECTED bssid44:9b:c1:57:a8:90 reason3 locally_generated1解析: CTR…...
Spring Boot 实现流式响应(兼容 2.7.x)
在实际开发中,我们可能会遇到一些流式数据处理的场景,比如接收来自上游接口的 Server-Sent Events(SSE) 或 流式 JSON 内容,并将其原样中转给前端页面或客户端。这种情况下,传统的 RestTemplate 缓存机制会…...
关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...
srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
Matlab | matlab常用命令总结
常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...
ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...
Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...
Map相关知识
数据结构 二叉树 二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子 节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只 有左子节点,有的节点只有…...
Spring AI与Spring Modulith核心技术解析
Spring AI核心架构解析 Spring AI(https://spring.io/projects/spring-ai)作为Spring生态中的AI集成框架,其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似,但特别为多语…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
