Ubuntu20.04 Gazebo中仿真宇树机器狗和外置激光雷达

Ubuntu20.04 Gazebo中仿真宇树机器狗和外置激光雷达

0 引言

     机器狗因其良好的通过性和环境适应性,在工业巡检、安防巡逻及科研教学等领域得到广泛应用。宇树科技(Unitree)四足机器狗作为当前主流的商用四足机器人平台之一,具有成熟的运动控制接口和良好的仿真支持。

     在算法开发和系统集成阶段,直接进行实机调试成本高、风险大,因此基于 Gazebo 的仿真环境搭建显得尤为重要。本文基于 Ubuntu20.04,详细介绍了宇树机器狗Go2在 Gazebo 中的仿真部署流程,并进一步扩展外置激光雷达传感器,同时说明了加载场景的步骤,实现了完整的感知与运动仿真验证,为后续定位、建图与导航算法开发提供可靠基础。

1 系统环境依赖

1.1 系统环境

相关教程很多,不多赘述.

  • Ubuntu 20.04
  • ROS noetic desktop-full
  • Gazebo classic

1.2 软件依赖

  • unitree ros
  • unitree guide
  • unitree ros to real

这里说明一下编译安装宇树官方驱动的步骤:

# 创建catkin工作空间 mkdir -p ~/unitree_sim/src cd ~/unitree/src && catkin_init_workspace # unitree ros 国内代理下载快点 git clone --recurse-submodules https://gh-proxy.com/https://github.com/unitreerobotics/unitree_ros.git # unitree guide git clone --recurse-submodules https://gh-proxy.com/https://github.com/unitreerobotics/unitree_guide.git # 如果下载不完整,请手动补全 # 编译,暂时不需要编译 unitree_move_base cd .. && catkin_make -DCATKIN_BLACKLIST_PACKAGES="unitree_move_base" 

等待编译完成后,需要修改官方模型加载的一处错误:
~unitree_sim/src/unitree_ros/robots/go2_description/xacro/robot.xacro 中:

将48行中 "trunk.dae" 改为 "base.dae" ,保存修改。


2 验证基础功能

(本文以Go2为例)

2.1 Gazebo仿真

cd ~/unitree_sim && source devel/setup.bash # unitree guide roslaunch unitree_guide gazeboSim.launch rname:=go2 # 或 unitree gazebo roslaunch unitree_gazebo normal.launch rname:=go2 wname:=stairs

仿真器中加载出宇树go2模型和简易世界模型:

2.2 运动控制

cd ~/unitree_sim && source devel/setup.bash # 启动运动控制节点 rosrun unitree_guide junior_ctrl # 在该终端中,按键2-站立、按键4-运动模式、按键5-导航模式 # 进入运动模式或者导航模式,均需要先站立起来。 # 运动模式中,按键W-前进、按键S-后退、按键A-左平移、按键D右平移、按键JL分别控制左右转。 # 导航模式此处暂时不表。

按下按键2后,仿真器中机器狗站立起来:

按下按键4进入运动模式后,在按下前进W,机器狗开始一直向前行走:

再次按下按键2,机器狗停止运动。

2.3 进阶控制

    在2.2小节中所讲的导航模式,实际上是宇树官方开放给ros navigation导航栈的接口,当自主导航时由局部规划器输出 geometry_msgs/Twist  类型的速度命令话题 /cmd_vel,底层运动驱动会实时订阅该话题,进而控制机器人行走运动。

宇树官方驱动中,导航模式默认是关闭编译的,需要开启后重新编译 unitree guide。具体在~/unitree_sim/src/unitree_guide/unitree_guide/CMakeLists.txt中:

将第11行中 set(MOVE_BASE OFF) 改为 set(MOVE_BASE ON),然后重新编译该包:

cd ~/unitree_sim # 单独编译 unitree guide 包 catkin_make -DCATKIN_WHITELIST_PACKAGES="unitree_guide"

虽然当依次按下按键2和4进入运动模式后,可行走控制,但十分不便,自行尝试可知。因此,利用导航模式中的速度命令话题 /cmd_vel,这里编写了一个简易有效的脚本发布控制命令:

#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys import select import termios import tty import rospy from geometry_msgs.msg import Twist" ========================================================= 键盘控制说明 Keyboard Control Help ========================================================= 移动控制(平移): q w e 左前 前进 右前 a s d 左移 停止 右移 z x c 左后 后退 右后 对角线移动(45°平移): f g h b 左前45° 右前45° 左后45° 右后45° 旋转控制(原地旋转): r t 左转 右转 速度调节: u / i : 增加 / 减少 线速度 j / k : 增加 / 减少 角速度 m / , : 同时调整 线速度和角速度 其他: 其他按键 : 停止运动 CTRL + C : 退出程序 ========================================================= """ class KeyboardTeleop: def __init__(self): rospy.init_node("keyboard_control", anonymous=True) self.pub = rospy.Publisher("/cmd_vel", Twist, queue_size=1) # 参数 self.max_lin = rospy.get_param("~speed", 0.8) self.max_ang = rospy.get_param("~turn", 0.8) # 速度状态 self.target = [0.0, 0.0, 0.0, 0.0] self.current = [0.0, 0.0, 0.0, 0.0] self.acc_step = 0.1 self.dec_step = 0.1 # 键位绑定 self.motion_map = { 'w': (1, 0, 0, 0), 'e': (1, 0, 0, -1), 'a': (0, 1, 0, 0), 'd': (0, -1, 0, 0), 'q': (1, 0, 0, 1), 'x': (-1, 0, 0, 0), 'c': (-1, 0, 0, 1), 'z': (-1, 0, 0, -1), 'r': (0, 0, 0, 1), 't': (0, 0, 0, -1), 'f': (1, 1, 0, 0), 'h': (-1, 1, 0, 0), 'g': (1, -1, 0, 0), 'b': (-1, -1, 0, 0), } self.speed_map = { 'u': (1.1, 1.0), 'i': (0.9, 1.0), 'j': (1.0, 1.1), 'k': (1.0, 0.9), 'm': (1.1, 1.1), ',': (0.9, 0.9), } self.settings = termios.tcgetattr(sys.stdin) self.last_key = None print("\033[2J\033[H") print(HELP_TEXT) # ---------------- 键盘读取 ---------------- def read_key(self): tty.setraw(sys.stdin.fileno()) readable, _, _ = select.select([sys.stdin], [], [], 0.1) key = sys.stdin.read(1) if readable else "" termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.settings) return key # ---------------- 插值平滑 ---------------- @staticmethod def smooth(curr, tgt, acc, dec): step = acc if curr < tgt else dec if abs(curr - tgt) <= step: return tgt return curr + step if curr < tgt else curr - step # ---------------- 状态显示 ---------------- def display(self, key): print("\033[25;0H",) print("\033[K[Speed ] linear: %5.2f angular: %5.2f" % (self.max_lin, self.max_ang)) print("\033[K[ Key ] %s" % (key if key else "None")) print("\033[K--------------------------------------------------") print("\033[K[Target ] X:%6.2f Y:%6.2f Z:%6.2f Th:%6.2f" % tuple(self.target)) print("\033[K[Current] X:%6.2f Y:%6.2f Z:%6.2f Th:%6.2f" % tuple(self.current)) print("\033[K--------------------------------------------------",) # ---------------- Twist 发布 ---------------- def publish_twist(self): msg = Twist() msg.linear.x = self.current[0] * self.max_lin msg.linear.y = self.current[1] * self.max_lin msg.linear.z = self.current[2] * self.max_lin msg.angular.z = self.current[3] * self.max_ang self.pub.publish(msg) # ---------------- 主循环 ---------------- def spin(self): rate = rospy.Rate(20) while not rospy.is_shutdown(): key = self.read_key() if key in self.motion_map: self.target = list(self.motion_map[key]) self.dec_step = 0.1 elif key in self.speed_map: self.max_lin *= self.speed_map[key][0] self.max_ang *= self.speed_map[key][1] else: self.target = [0.0] * 4 self.dec_step = 0.15 if key == "\x03": break for i in range(4): self.current[i] = self.smooth( self.current[i], self.target[i], self.acc_step, self.dec_step ) self.display(key) self.publish_twist() rate.sleep() self.shutdown() # ---------------- 退出清理 ---------------- def shutdown(self): stop = Twist() self.pub.publish(stop) termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.settings) rospy.loginfo("[keyboard_control] Exit.") if __name__ == "__main__": try: KeyboardTeleop().spin() except Exception as e: rospy.logerr(e) 

假设该脚本命名为 keycontrol.py,以下给出整个运动控制流程:

chmod +x keycontrol.py # 开启gazebo仿真器 roslaunch unitree_guide gazeboSim.launch rname:=go2 # 开启底层运动控制节点 # 在该终端按下按键2站立,然后按下按键5进入导航模式 rosrun unitree_guide junior_ctrl # 启动控制命令脚本 python3 keycontrol.py

执行情况如下:

​​​​​​


3 外置激光雷达

    在完成宇树机器狗本体仿真与基础运动控制验证后,为了进一步支持环境感知、定位与导航算法的开发,有必要在仿真环境中为机器狗添加外置激光雷达。本章将详细介绍外置激光雷达在 Gazebo 中的建模方法、与机器人本体的集成方式以及仿真驱动与数据验证过程。

3.1 激光雷达仿真模型

由于个人需要,本文选择了速腾聚创的多线机械激光雷达进行仿真:

编译安装 robosense simulator:

cd ~/unitree_sim/src # 拉取 robosense simulator 仿真驱动 git clone https://gh-proxy.com/https://github.com/tomlogan501/robosense_simulator.git cd .. catkin_make -DCATKIN_WHITELIST_PACKAGES="robosense_gazebo_plugins" catkin_make -DCATKIN_WHITELIST_PACKAGES="robosense_description" 

安装完毕后,进行下一步。

3.2 机器狗安装雷达配置

由于本章采用的是外置激光雷达,因此需要将激光雷达模型正确地安装到机器狗的仿真模型上。该过程在机器狗的 xacro 文件中完成。

在模型配置过程中,需要重点关注以下几个方面:

安装位置选择
激光雷达一般安装在机器人机身顶部或前部位置,以减少机器狗自身结构对激光扫描的遮挡。仿真中需根据实际硬件安装位置设置合理的平移和旋转参数。

坐标系定义
为激光雷达单独定义一个 link,并通过 joint 与机器狗本体 link 相连。该 link 将作为激光雷达数据的参考坐标系。

TF 关系正确性
确保激光雷达坐标系能够通过 TF 正确转换到机器狗基坐标系及世界坐标系。这对于后续的定位与建图算法至关重要。


3.2.1 机器狗上加载雷达

将 ~/unitree_sim/src/robosense_simulator/robosense_description/meshes/robosense_16.dae中的雷达模型复制到~/unitree_sim/src/unitree_ros/robots/go2_description/meshes/中:

cp ~/unitree_sim/src/robosense_simulator/robosense_description/meshes/robosense_16.dae ~/unitree_sim/src/unitree_ros/robots/go2_description/meshes/

修改~/go2_description/xacro/robot.xacro文件,添加以下内容:

<link name="lidar"> <inertial> <origin xyz="0 0 0" rpy="0 0 0"/> <mass value="0.05"/> <inertia ixx="1e-5" ixy="0" ixz="0" iyy="1e-5" iyz="0" izz="1e-5"/> </inertial> <visual> <origin xyz="0 0 0" rpy="-1.57079632679 0 0"/> <geometry> <mesh filename="package://go2_description/meshes/robosense_16.dae"/> </geometry> <material> <color rgba="0.792156862745098 0.819607843137255 0.933333333333333 1"/> </material> </visual> <collision> <origin xyz="0 0 0" rpy="0 0 0"/> <geometry> <mesh filename="package://go2_description/meshes/robosense_16.dae"/> </geometry> </collision> </link> <joint name="lidar_joint" type="fixed"> <origin xyz="0.23 0 0.085" rpy="0 0 0"/> <parent link="base"/> <child link="lidar"/> <axis xyz="0 0 0"/> </joint>

修改后保存,打开gazebo查看:

roslaunch unitree_guide gazeboSim.launch rname:=go2 

结果如下,雷达安装在机器狗头部上方:

3.2.2 雷达仿真驱动配置

将雷达添加到机器狗本体后,还需要配置雷达的物理仿真驱动:

修改 ~/unitree_sim/src/unitree_ros/robots/go2_description/xacro/gazebo.xacro ,添加以下:

<gazebo reference="lidar"> <sensor type="gpu_ray" name="robosense-RS16"> <pose>0 0 0 0 0 0</pose> <visualize>false</visualize> <update_rate>10</update_rate> <ray> <scan> <horizontal> <samples>900</samples> <resolution>1</resolution> <min_angle>-3.1415926535897931</min_angle> <max_angle>3.1415926535897931</max_angle> </horizontal> <vertical> <samples>16</samples> <resolution>1</resolution> <min_angle>-0.262</min_angle> <max_angle> 0.262</max_angle> </vertical> </scan> <range> <min>0.1</min> <max>100.0</max> <resolution>0.01</resolution> </range> <noise> <type>gaussian</type> <mean>0.0</mean> <stddev>0.01</stddev> </noise> </ray> <plugin name="gazebo_ros_laser_controller" filename="libgazebo_ros_robosense_gpu_laser.so"> <topicName>/rslidar_points</topicName> <frameName>lidar</frameName> </plugin> </sensor> </gazebo>
若没有GPU,将 gpu_ray改为ray,将 libgazebo_ros_robosense_gpu_laser.so 改为 libgazebo_ros_robosense_laser.so

修改后保存,进行下一步。

3.3 雷达数据验证可视化

启动 gazebo 和 rostopic 查看雷达点云数据:

# 打开gazebo roslaunch unitree_guide gazeboSim.launch rname:=go2

rostopic list 查看话题如下:

$ rostopic list /apply_force/trunk /clock /gazebo/link_states /gazebo/model_states /gazebo/parameter_descriptions /gazebo/parameter_updates /gazebo/performance_metrics /gazebo/set_link_state /gazebo/set_model_state /go2_gazebo/FL_calf_controller/command /go2_gazebo/FL_calf_controller/joint_wrench /go2_gazebo/FL_calf_controller/state /go2_gazebo/FL_hip_controller/command /go2_gazebo/FL_hip_controller/joint_wrench /go2_gazebo/FL_hip_controller/state /go2_gazebo/FL_thigh_controller/command /go2_gazebo/FL_thigh_controller/joint_wrench /go2_gazebo/FL_thigh_controller/state /go2_gazebo/FR_calf_controller/command /go2_gazebo/FR_calf_controller/joint_wrench /go2_gazebo/FR_calf_controller/state /go2_gazebo/FR_hip_controller/command /go2_gazebo/FR_hip_controller/joint_wrench /go2_gazebo/FR_hip_controller/state /go2_gazebo/FR_thigh_controller/command /go2_gazebo/FR_thigh_controller/joint_wrench /go2_gazebo/FR_thigh_controller/state /go2_gazebo/RL_calf_controller/command /go2_gazebo/RL_calf_controller/joint_wrench /go2_gazebo/RL_calf_controller/state /go2_gazebo/RL_hip_controller/command /go2_gazebo/RL_hip_controller/joint_wrench /go2_gazebo/RL_hip_controller/state /go2_gazebo/RL_thigh_controller/command /go2_gazebo/RL_thigh_controller/joint_wrench /go2_gazebo/RL_thigh_controller/state /go2_gazebo/RR_calf_controller/command /go2_gazebo/RR_calf_controller/joint_wrench /go2_gazebo/RR_calf_controller/state /go2_gazebo/RR_hip_controller/command /go2_gazebo/RR_hip_controller/joint_wrench /go2_gazebo/RR_hip_controller/state /go2_gazebo/RR_thigh_controller/command /go2_gazebo/RR_thigh_controller/joint_wrench /go2_gazebo/RR_thigh_controller/state /go2_gazebo/joint_states /rosout /rosout_agg /rslidar_points /tf /tf_static /trunk_imu /visual/FL_foot_contact/the_force /visual/FR_foot_contact/the_force /visual/RL_foot_contact/the_force /visual/RR_foot_contact/the_force 

其中 /rslidar_points 是仿真雷达的点云话题,/trunk_imu 是机器狗本体的 imu数据话题。


打开rviz查看实时点云结果如下:

至此,雷达模型和仿真驱动添加完毕,后续可以将机器狗导入任意世界场景中进行仿真使用。

本体与传感器外参如下:

T_base_lidar: translation: [0.23, 0.0, 0.085] rotation (rpy): [0, 0, 0] T_base_imu: translation: [0.0, 0.0, 0.0] rotation (rpy): [0, 0, 0] 

4 导入世界场景

    在完成宇树机器狗本体及外置激光雷达的仿真配置后,为了对机器人在复杂环境中的运动与感知能力进行验证,需要构建合适的仿真场景并导入 Gazebo。世界模型用于描述机器人所处的仿真环境,是运动控制、传感器感知以及算法验证的重要基础。本章将简单介绍 Gazebo 世界模型的构建和将自定义世界模型导入机器狗仿真系统的过程。

4.1 Gazebo世界模型概述

    Gazebo 世界模型(World)用于描述仿真环境中的所有静态与动态元素,包括地面、墙体、障碍物以及环境光照等。世界模型通常以 .world 文件的形式存在,其本质是基于 SDF(Simulation Description Format)的场景描述文件。

   一个完整的 Gazebo 世界模型通常包含以下内容:

  • 仿真环境的物理属性,如重力、摩擦系数等;
  • 地面模型及其尺寸和材质;
  • 各类障碍物模型,用于构建复杂环境;
  • 光源设置,用于改善仿真可视化效果。

合理设计世界模型不仅可以提升仿真的真实感,还能为激光雷达、视觉传感器等提供更加符合实际的感知输入。

4.2 基础世界模型构建

    在仿真初期,为了验证系统整体功能,通常先构建一个相对简单的世界模型。该模型一般包含平坦地面和少量基础障碍物,便于观察机器人运动和传感器数据的变化。

   基础世界模型的构建主要包括:

  • 设置平面地面模型,作为机器人运动的基础支撑;
  • 添加简单几何体(如立方体、墙体)作为障碍物;
  • 调整模型尺寸与位置,确保机器人具有足够的活动空间。

通过基础世界模型,可以快速验证机器人在仿真中的稳定性以及激光雷达对障碍物的感知能力。

4.3 自定义复杂世界与导入

    为了方便,笔者直接选取了 aws_robomaker_racetrack_world 的世界模型,当然还有许多其它的优秀的 Gazebo 世界模型参考:https://github.com/PX4/PX4-SITL_gazebo-classic.git 。

说明一下编译安装步骤:

cd ~/unitree_sim/src git clone https://gh-proxy.com/https://github.com/aws-robotics/aws-robomaker-racetrack-world.git cd .. catkin_make install -DCATKIN_WHITELIST_PACKAGES="aws_robomaker_racetrack_world"

编译完成后,修改 ~/unitree_sim/src/unitree_guide/unitree_guide/launch/gazeboSim.launch :

第2行的 <arg name="wname" default="earth"/> 改为 <arg name="wname" default="racetrack_day"/> 第4行的 <arg name="rname" default="go1"/> 改为 <arg name="rname" default="go2"/> 第17行的 <arg name="world_name" value="$(find unitree_gazebo)/worlds/$(arg wname).world"/> 改为 <arg name="world_name" value="$(find aws_robomaker_racetrack_world)/worlds/$(arg wname).world"/> 

修改后保存,打开机器狗仿真器:

# 激活环境变量 cd ~/unitree_sim && source devel/setup.bash # gazebo 启动 roslaunch unitree_guide gazeboSim.launch # 若加载失败,可能是之前gazebo相关节点没有正常关闭。请先关闭: killall -9 gzserver gzclient # 运动控制启动,按下按键2-站立 rosrun unitree_guide junior_ctrl 

加载效果:


5 小结

    该仿真系统为后续开展定位、建图和导航等算法研究提供了稳定、可复现的实验平台,并对机器狗仿真系统的搭建具有一定参考价值。后续有机会再更新适配主流建图定位和导航算法的教程。长文撰写不易,点个赞吧!

Read more

深入解剖STL RB-tree(红黑树):用图解带入相关复杂操作实现

深入解剖STL RB-tree(红黑树):用图解带入相关复杂操作实现

👇点击进入作者专栏: 《算法画解》 ✅ 《linux系统编程》✅ 《C++》 ✅ 文章目录 * 一、红黑树介绍 * 1. 什么是红黑树? * 2. 红黑树的规则 * 3. 为什么最长路径不超过最短路径的两倍? * 4. 红黑树的效率 * 二、红黑树的实现 * 2.1 红黑树的节点结构 * 2.2 红黑树整体结构 * 三、红黑树的插入操作 * 3.1 插入的大致流程 * 3.2 插入后的三种情况 * 情况1:叔叔节点存在且为红色(变色处理) * 情况2:叔叔节点不存在或为黑色 + cur和p在同一侧(单旋+变色) * 情况3:叔叔节点不存在或为黑色 + cur和p在不同侧(双旋+变色) * 3.3 插入完整代码 * 3.4 旋转操作的实现

By Ne0inhk
Redis核心通用命令深度解析:结合C++ redis-plus-plus 实战指南

Redis核心通用命令深度解析:结合C++ redis-plus-plus 实战指南

前言:为何选择 Redis 与 C++? 在当今这个数据驱动的时代,高性能的数据存储与访问是构建现代化应用的基石。Redis,作为一个开源的、基于内存的键值对存储数据库,以其无与伦比的读写速度、丰富的数据结构、以及灵活的应用场景(缓存、消息队列、会话存储、排行榜等),成为了后端开发者的瑞士军刀。 与此同时,C++ 作为一门追求极致性能的编程语言,长期以来在游戏开发、金融交易、高性能计算等领域占据着主导地位。当 C++ 的高性能与 Redis 的高速度相结合时,我们便能够构建出响应迅捷、吞吐量巨大的应用程序。 然而,要将二者优雅地结合起来,我们需要一个强大的“桥梁”——Redis 客户端库。redis-plus-plus 就是这样一个专为现代 C++ (C++11 及以上) 设计的优秀库。它不仅封装了 Redis 的原生协议,提供了类型安全、易于使用的 API,

By Ne0inhk

几小时完成生鲜配送系统!飞算JavaAI专业版:智能引导+两大工具承包开发全流程

作为一名Java开发者,我曾无数次被「需求拆解难、后期调试烦」的问题困住,最近面对一个生鲜配送系统的开发需求,光梳理业务逻辑、设计表结构就要耗上大半天,后续还要花时间处理代码规范、依赖冲突,往往一周才能拿出可运行的项目。直到试用了飞算JavaAI专业版,才发现AI辅助开发能如此高效:借助它的智能引导系统和两大核心AI工具,我从需求输入到项目初步完成仅需几小时,大大节省了我的时间。 智能引导五步法:让模糊需求快速落地 做生鲜配送系统前,我的需求很简单:「支持用户下单、订单跟踪、配送员调度、库存管理」,但具体怎么拆分模块、设计接口完全没头绪。放在以前,至少要花1天时间和产品经理对接需求文档,而飞算JavaAI的智能引导系统,直接帮我把模糊需求变成了标准化的开发方案。 第一步「理解需求」就超出预期。我在输入框写下核心诉求后,系统10秒内就拆解出几个关键点,还补充了我没考虑到很多功能——比如当生鲜商品临近保鲜期时,系统会自动触发库存预警,异常订单(如地址不明确、支付超时)会自动分流处理,简直像有个资深行业顾问在补位。 第二步「设计接口」根据我的需求创建了繁多的接口供我选择,并且可

By Ne0inhk
1、Java快速开发平台崛起:六大国产顶尖框架全景对比与精准选型实战指南

1、Java快速开发平台崛起:六大国产顶尖框架全景对比与精准选型实战指南

2026最新Java快速开发框架选型宝典 国内六大热门框架全景对比+实战决策树 关键词:Java快速开发平台、2026Java框架、RuoYi、JeecgBoot、Pig、BladeX、JeeSite、Guns、企业级开发选型、微服务框架、低代码开发、Java框架对比 适用人群:Java架构师、后端开发工程师、技术负责人、初创公司技术选型者、企业信息化开发团队 在企业级Java开发领域,**“降本提效”**早已成为突破开发瓶颈的核心诉求——从零搭建项目、重复编写CRUD代码、手动开发权限/日志/监控等基础模块,不仅会消耗80%的开发精力,还会因团队技术差异导致项目质量参差不齐。而Java快速开发平台的出现,彻底改变了这一现状:它整合了企业级开发的通用能力,提供标准化架构、开箱即用的功能模块、一键生成的代码工具,让开发者能将90%的精力聚焦业务创新。 随着国内开源生态的成熟,一批本土化Java快速开发框架脱颖而出,它们适配国内开发者编码习惯、贴合企业实际需求,远比国外框架更易落地。2026年,RuoYi、JeecgBoot、Pig、

By Ne0inhk