SpringBoot源码解读与原理分析(三十六)SpringBoot整合WebMvc(一)@Controller控制器装配原理

文章目录

  • 前言
  • 第12章 SpringBoot整合WebMvc
    • 12.1 SpringBoot整合WebMvc案例
    • 12.2 整合WebMvc的组件自动装配
    • 12.3 WebMvc的核心组件
      • 12.3.1 DispatcherServlet
      • 12.3.2 Handler
      • 12.3.3 HandlerMapping
      • 12.3.4 HandlerAdapter
      • 12.3.5 ViewResolver
    • 12.4 @Controller控制器装配原理
      • 12.4.1 初始化@RequestMapping的入口
      • 12.4.2 processCandidateBean
      • 12.4.3 detectHandlerMethods
        • 12.4.3.1 筛选Handler方法,创建RequestMappingInfo
        • 12.4.3.2 遍历方法,注册方法映射

前言

SpringBoot经常整合的其中一个核心场景是Web开发。

SpringFramework 5.x中对于Web场景的开发提供了两套实现方案:WebMvc与WebFlux。

第12章 SpringBoot整合WebMvc

12.1 SpringBoot整合WebMvc案例

导入依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>

编写Controller类:

@Controller
@RequestMapping("/user")
public class UserController {@RequestMapping(method = RequestMethod.GET, value = "/test")@ResponseBodypublic String test(String name) {System.out.println("请求参数 name = " + name);return name;}
}

编写路径配置类:

@Configuration
public class PathConfig implements WebMvcConfigurer {@Overridepublic void configurePathMatch(PathMatchConfigurer configurer) {configurer.addPathPrefix("/api", c -> c.isAnnotationPresent(Controller.class));}
}

SpringBoot给Controller类的方法的访问路径添加前缀的方式有多种,编写配置类是其中一种。这里编写这个配置类是为了方便下文分析源码,因此是可选的。

编写主启动类:

@SpringBootApplication
public class WebMvcApp {public static void main(String[] args) {SpringApplication.run(WebMvcApp.class, args);}}

执行主启动类的main方法,启动服务。

在HTTP客户端使用GET方式访问 http://127.0.0.1:8080/user/test?name=齐天大圣,得到结果如下:

请求执行结果
说明SpringBoot整合WebMvc成功。

12.2 整合WebMvc的组件自动装配

在 SpringBoot源码解读与原理分析(六)WebMvc场景的自动装配 中,已经对SpringBoot整合WebMvc时注册的组件进行了梳理,总结如下:

  • WebMvcAutoConfiguration
    • WebMvcAutoConfigurationAdapter
      • 消息转换器HttpMessageConverter
      • 视图解析器ContentNegotiatingViewResolver
      • 国际化组件LocaleResolver
      • 异步支持AsyncSupportConfigurer
    • EnableWebMvcConfiguration
      • RequestMappingHandlerMapping
      • RequestMappingHandlerAdapter
      • 静态资源加载配置
  • DispatcherServletAutoConfiguration
    • DispatcherServlet
  • ServletWebServerFactoryAutoConfiguration
    • 嵌入式Web容器EmbeddedTomcat、EmbeddedJetty、EmbeddedUndertow
    • 后置处理器的注册器BeanPostProcessorsRegistrar

12.3 WebMvc的核心组件

12.3.1 DispatcherServlet

DispatcherServlet是WebMvc的核心前端控制器,统一接收客户端(浏览器)的所有请求,并根据请求URI转发给项目中编写好的Controller方法。Controller方法处理完毕后,将处理结果返回给DispatcherServlet,由DispatcherServlet响应到客户端(浏览器)。

但要注意,匹配寻找Controller方法、请求转发以及响应数据等工作,都不是DispatcherServlet亲自完成的,而是委托给其他组件,这些组件与DispatcherServlet共同协作完成整个MVC的工作。

DispatcherServlet的工作流程如下图所示:

DispatcherServlet的工作流程1

12.3.2 Handler

在项目开发中编写的Controller方法中,一个标注了@RequestMapping注解的方法就是一个Handler。因此,Handler要完成的工作就是处理客户端发送的请求,并响应视图/JSON数据。

DispatcherServlet接收到请求后,会根据URI匹配到标注了@RequestMapping注解的Controller方法(即Handler),并将这些请求转发给这个Handler。

DispatcherServlet的工作流程可以做如下优化:

DispatcherServlet的工作流程2

12.3.3 HandlerMapping

DispatcherServlet根据是否标注@RequestMapping注解去匹配Handler的工作,DispatcherServlet自己也是不做的,而是委托给HandlerMapping来完成。

HandlerMapping意为处理器映射器,它的作用是根据URI,去匹配查找能处理当前请求的Handler。

加入HandlerMapping,DispatcherServlet的工作流程如下:

DispatcherServlet的工作流程3

源码1HandlerMapping.javapublic interface HandlerMapping {HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

由 源码1 可知,HandlerMapping通过getHandler方法查找Handler,返回一个HandlerExecutionChain对象。

HandlerExecutionChain意为Handler执行链,这是因为一个请求除了被Handler处理,还有可能被拦截器拦截处理,在执行Handler之前要先执行拦截器。基于此,HandlerMapping将当前请求会涉及到的拦截器和Handler一起封装起来,组合成一个HandlerExecutionChain对象,交给DispatcherServlet。

HandlerMapping接口有几个主要的落地实现类:

  • RequestMappingHandlerMapping

支持@RequestMapping注解的处理器映射器,是实际项目开发中最重要、最常用的。

  • BeanNameUrlHandlerMapping

使用bean对象的名称作为Handler接收请求路径的处理器映射器。

这种方式需要编写Controller类实现WebMvc定义的Controller接口,并重写handleRequest方法,如:

public class TestController implements Controller {@Overridepublic ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {return null;}
}

使用该方式编写的Controller,一个类只能接收一个请求路径,因此局限性较大,目前已被淘汰

  • SimpleUrlHandlerMapping

这种处理器映射器会在配置文件中统一配置请求路径和Controller的对应关系,同时仍要编写Controller类实现WebMvc定义的Controller接口。该方式也已被淘汰。

12.3.4 HandlerAdapter

DispatcherServlet获取到HandlerExecutionChain后,就要开始执行这些拦截器和Handler,DispatcherServlet会选择交给HandlerAdapter去执行。

HandlerAdapter意为处理器适配器,它的作用是执行HandlerMapping封装好的HandlerExecutionChain。

加入HandlerAdapter后DispatcherServlet的工作流程如下:

DispatcherServlet的工作流程4
HandlerMapping接口有几个主要的落地实现类:

  • RequestMappingHandlerAdapter:基于@RequestMapping注解的处理器适配器,底层使用反射机制调用Handler的方法(最常用)。
  • SimpleControllerHandlerAdapter:基于Controller接口的处理器适配器,底层会将Handler强转为Controller,调用其handleRequest方法。
  • SimpleServletHandlerAdapter:基于Servlet的处理器适配器,底层会将Handler强转为Servlet,调用其service方法。

由此可以发现,WebMvc兼顾多种编写Handler的方式,但最常用的是基于@RequestMapping注解的方式

12.3.5 ViewResolver

DispatcherServlet获取到HandlerAdapter返回的ModelAndView之后,需要进行响应视图,这部分工作DispatcherServlet将会委托给ViewResolver来处理。

ViewResolver意为视图解析器,它的作用是根据ModelAndView中存放的视图名称到预先配置好的位置去查找对应的视图文件(.jsp、.html等),并进行实际的视图渲染。渲染完成后,将视图响应给DispatcherServlet。

加入ViewResolver后DispatcherServlet的工作流程如下:

DispatcherServlet的工作流程5
默认情况下,ViewResolver只会初始化一个实现类InternalResourceViewResolver,它继承自UrlBasedViewResolver类,可以方便地声明页面路径的前后缀,以便开发中在返回视图(JSP页面)时编写视图名称。

除了InternalResourceViewResolver,WebMvc还为一些模板引擎提供了支持类,例如支持FreeMarker的FreeMarkerViewResolver、支持Groovy XML/XHTML的GroovyMarkupViewResolver等。

至此,DispatcherServlet的核心工作流程梳理完毕,这样看来DispatcherServlet其实没有做具体的工作,而是扮演了一个“调度者”的角色,在不同的环节分发不同的工作给其他核心组件

12.4 @Controller控制器装配原理

当编写的Controller类标注了@Controller注解(或派生注解),方法标注了@RequestMapping注解(或派生注解)时,即可装载到WebMvc中完成视图跳转/数据响应的功能。

12.4.1 初始化@RequestMapping的入口

使用@RequestMapping注解的方式声明Handler,一定会与RequestMappingHandlerMapping有关联。由 12.1 节的分析可知,自动配置类WebMvcAutoConfiguration中就已经初始化了RequestMappingHandlerMapping。

借助IDE打开RequestMappingHandlerMapping类的源码,发现该类重写了其父类InitializingBean的afterPropertiesSet方法,这说明在RequestMappingHandlerMapping对象的初始化阶段有额外的扩展处理。

源码2RequestMappingHandlerMapping.java@Override
@SuppressWarnings("deprecation")
public void afterPropertiesSet() {this.config = new RequestMappingInfo.BuilderConfiguration();this.config.setUrlPathHelper(getUrlPathHelper());// this.config.set...super.afterPropertiesSet();
}
源码3AbstractHandlerMethodMapping.java@Override
public void afterPropertiesSet() {initHandlerMethods();
}private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";
protected void initHandlerMethods() {// 获取IOC容器中所有bean对象的名称for (String beanName : getCandidateBeanNames()) {if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {// 如果bean对象的名称不以“scopedTarget.”开头,则执行processCandidateBean(beanName);}}handlerMethodsInitialized(getHandlerMethods());
}

由 源码2-3 可知,afterPropertiesSet方法一直向下调用到initHandlerMethods方法。从方法名可以推断,该方法会初始化所有的Handler方法。在该方法的实现中,会获取IOC容器中所有bean对象的名称,找出其中不以“scopedTarget.”开头的bean对象名称,并执行processCandidateBean方法。

12.4.2 processCandidateBean

源码4AbstractHandlerMethodMapping.javaprotected void processCandidateBean(String beanName) {Class<?> beanType = null;try {beanType = obtainApplicationContext().getType(beanName);} // catch ...}// 判断bean对象的类型是否是Handlerif (beanType != null && isHandler(beanType)) {detectHandlerMethods(beanName);}
}

由 源码4 可知,processCandidateBean首先会根据bean对象的名称获取到bean对象的类型,再判断这个类型是否是Handler,如果是Handler,则继续向下执行detectHandlerMethods方法。

源码5RequestMappingHandlerMapping.java@Override
protected boolean isHandler(Class<?> beanType) {return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

由 源码5 可知,判断一个类是否是Handler的方法是:判断类上是否标注了@Controller注解(或派生注解)或者@RequestMapping注解(或派生注解)。只要二者存在一个,就可以判定该类是一个Handler。

如UserController类标注了@Controller注解,则会被判定为是一个Handler:

UserController类被判定Handler
在实际的项目开发中,通常是同时标注@Controller注解和@RequestMapping注解。但 源码5 提示我们,实际上,在类上只标注@RequestMapping注解就可以判定该类是一个Controller类。

12.4.3 detectHandlerMethods

源码6AbstractHandlerMethodMapping.javaprotected void detectHandlerMethods(Object handler) {Class<?> handlerType = (handler instanceof String ?obtainApplicationContext().getType((String) handler) : handler.getClass());if (handlerType != null) {Class<?> userType = ClassUtils.getUserClass(handlerType);// 筛选Handler方法,创建RequestMappingInfoMap<Method, T> methods = MethodIntrospector.selectMethods(userType,(MethodIntrospector.MetadataLookup<T>) method -> {try {return getMappingForMethod(method, userType);} // catch ...});// logger ...// 遍历方法,注册方法映射methods.forEach((method, mapping) -> {Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);registerHandlerMethod(handler, invocableMethod, mapping);});}
}

由 源码6 可知,detectHandlerMethods方法会先根据bean对象名称找到对应的bean对象类型,筛选出该类型下的全部Handler方法,并一一遍历和注册这些方法。

12.4.3.1 筛选Handler方法,创建RequestMappingInfo

利用MethodIntrospector的selectMethods方法进行方法的遍历。在selectMethods方法内部会利用反射机制逐个遍历类中的方法,并执行selectMethods方法参数中的lambda表达式。

lambda表达式中的getMappingForMethod方法源码如下:

源码7RequestMappingHandlerMapping.javaprotected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {// 创建方法级的RequestMappingInfoRequestMappingInfo info = createRequestMappingInfo(method);if (info != null) {// 创建类级的RequestMappingInfoRequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);if (typeInfo != null) {// 拼接类级和方法级的请求映射路径info = typeInfo.combine(info);}// 拼接路径前缀String prefix = getPathPrefix(handlerType);if (prefix != null) {info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);}}return info;
}private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {// 检查类或方法上是否标注了@RequestMapping注解RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);RequestCondition<?> condition = (element instanceof Class ?getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));// 如果标注了@RequestMapping注解,则创建RequestMappingInfo,否则返回空return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}

由 源码7 可知,getMappingForMethod方法的逻辑如下:

首先,会检查方法上是否标注了@RequestMapping注解,如果有则创建方法级的RequestMappingInfo对象,否则直接返回null;

然后再检查类上是否标注了@RequestMapping注解,如果有则创建类级的RequestMappingInfo对象;

借助Debug可知,RequestMappingInfo对象中保存了Controller类的映射URI,或者Handler方法的请求路径和请求方式,如下图:

RequestMappingInfo对象保存了请求路径和请求方式
其次,调用combine方法把方法级的RequestMappingInfo对象和类级的合并到一起,合并之后类级别的RequestMappingInfo对象中保存的映射URI和方法级别中保存的合并到了一起,如图:

合并RequestMappingInfo对象
最后一步是拼接路径前缀。这个路径前缀是自定义的,即 xxx 节整合项目中的PathConfig配置类。如果项目中没有设置,则不需要处理这一步。通过拼接路径前缀,访问Handler方法的URI完整了,如图:

拼接路径前缀

12.4.3.2 遍历方法,注册方法映射

经过getMappingForMethod方法的处理,获得一个Map集合,该集合的value值就是包含映射URI、请求方式等信息的RequestMappingInfo对象。

源码8AbstractHandlerMethodMapping.javaprotected void detectHandlerMethods(Object handler) {Class<?> handlerType = (handler instanceof String ?obtainApplicationContext().getType((String) handler) : handler.getClass());if (handlerType != null) {Class<?> userType = ClassUtils.getUserClass(handlerType);// 筛选Handler方法,创建RequestMappingInfoMap<Method, T> methods = ...// ......// 遍历方法,注册方法映射methods.forEach((method, mapping) -> {Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);registerHandlerMethod(handler, invocableMethod, mapping);});}
}protected void registerHandlerMethod(Object handler, Method method, T mapping) {this.mappingRegistry.register(mapping, handler, method);
}

由 源码8 可知,detectHandlerMethods方法的下半段会遍历Map集合,将Handler方法与RequestMappingInfo对象一一映射,并注册到MappingRegistry中。

因此,MappingRegistry中存放的是Handler方法与RequestMappingInfo对象的映射关系。

Handler方法与URI的映射关系
其中,key值是RequestMappingInfo对象,value值Handler方法。

在DispatcherServlet接收到客户端请求时,则可以根据URI去MappingRegistry中寻找,如果找到匹配的Handler方法,则定位到可以处理请求的Handler方法,并将请求转发。

······

本节完,更多内容请查阅分类专栏:SpringBoot源码解读与原理分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2813772.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

SQL注入漏洞解析--less-7

我们先看一下第七关 页面显示use outfile意思是利用文件上传来做 outfile是将检索到的数据&#xff0c;保存到服务器的文件内&#xff1a; 格式&#xff1a;select * into outfile "文件地址" 示例&#xff1a; mysql> select * into outfile f:/mysql/test/one f…

naive-ui-admin 表格去掉工具栏toolbar

使用naive-ui-admin的时候&#xff0c;有时候不需要显示工具栏&#xff0c;工具栏太占地方了。 1.在src/components/Table/src/props.ts 里面添加属性 showToolbar 默认显示&#xff0c;在不需要的地方传false。也可以默认不显示 &#xff0c;这个根据需求来。 2.在src/compo…

redis-Redis主从,哨兵和集群模式

一&#xff0c;Redis的主从复制 ​ 主机数据更新后根据配置和策略&#xff0c; 自动同步到备机的master/slaver机制&#xff0c;Master以写为主&#xff0c;Slave以读为主。这样做的好处是读写分离&#xff0c;性能扩展&#xff0c;容灾快速恢复。 1.1 环境搭建 如果你的redi…

[足式机器人]Part2 Dr. CAN学习笔记-Ch00-2 - 数学知识基础

本文仅供学习使用 本文参考: B站:DR_CAN 《控制之美(卷1)》 王天威 《控制之美(卷2)》 王天威 Dr. CAN学习笔记-Ch00 - 数学知识基础 Part2 4. Ch0-4 线性时不变系统中的冲激响应与卷积4.1 LIT System:Linear Time Invariant4.2 卷积 Convolution4.3 单位冲激 Unit Impulse—…

nn.Linear() 使用提醒

原本以为它是和nn.Conv2d()一样&#xff0c;就看第二个维度的数值&#xff0c;今天才知道&#xff0c;它是只看最后一个维度的数值&#xff01;&#xff01;&#xff01; 例子1 Descripttion: Result: Author: Philo Date: 2024-02-27 14:33:50 LastEditors: Philo LastEditT…

中小型项目实现简单版本备份的批处理脚本

对于项目备份与版本管理,可以使用的工具 Git工具 Git 是一个开源的分布式版本控制系统,用于追踪文件更改和帮助多人协作开发。它被设计成能够快速、高效地处理从很小到非常大的项目。是目前最受欢迎的版本控制系统之一。 rsync rsync是一个强大的文件同步工具,用于在本地或远…

【JSON2WEB】06 JSON2WEB前端框架搭建

【JSON2WEB】01 WEB管理信息系统架构设计 【JSON2WEB】02 JSON2WEB初步UI设计 【JSON2WEB】03 go的模板包html/template的使用 【JSON2WEB】04 amis低代码前端框架介绍 【JSON2WEB】05 前端开发三件套 HTML CSS JavaScript 速成 前端技术路线太多了&#xff0c;知识点更多&…

【Pytorch深度学习开发实践学习】Pytorch实现LeNet神经网络(1)

1.model.py import torch.nn as nn import torch.nn.functional as F引入pytorch的两个模块 关于这两个模块的作用&#xff0c;可以参考下面 Pytorch官方文档 torch.nn包含了构成计算图的基本模块 torch,nn.function包括了计算图中的各种主要函数&#xff0c;包括&#…

安装使用zookeeper

先去官网下载zookeeper&#xff1a;Apache ZooKeeper 直接进入bin目录&#xff0c;使用powerShell打开。 输入: ./zkServer.cmd 命令&#xff0c;启动zookeeper。 zookeeper一般需要配合Dubbo一起使用&#xff0c;作为注册中心使用&#xff0c;可以参考另一篇博客&#xf…

java springmvc/springboot 项目通过HttpServletRequest对象获取请求体body工具类

请求 测试接口 获取到的 获取到打印出的json字符串里有空格这些&#xff0c;在json解析的时候正常解析为json对象了。 工具类代码 import lombok.extern.slf4j.Slf4j; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.we…

如何做代币分析:以 TRX 币为例

作者&#xff1a;lesleyfootprint.network 编译&#xff1a;cicifootprint.network 数据源&#xff1a;TRX 代币仪表板 &#xff08;仅包括以太坊数据&#xff09; 在加密货币和数字资产领域&#xff0c;代币分析起着至关重要的作用。代币分析指的是深入研究与代币相关的数据…

pr2024 Premiere Pro 2024 mac v24.2.1中文激活版

Premiere Pro 2024 for Mac是Adobe公司推出的一款强大的视频编辑软件&#xff0c;专为Mac操作系统优化。它提供了丰富的剪辑工具、特效和音频处理选项&#xff0c;帮助用户轻松创建专业级的影视作品。 软件下载&#xff1a;pr2024 Premiere Pro 2024 mac v24.2.1中文激活版 无论…

苍穹外卖-day07 - 缓存菜品- 缓存套餐- 添加购物车- 查看购物车- 清空购物车

1. 缓存菜品 用redis来缓存菜品数据 &#xff0c; 减少数据库查询操作 ; 缓存逻辑 : 每个分类下的菜品保存一份缓存数据 数据库中菜品数据有变更时清理缓存数据 关键代码 : Autowiredprivate RedisTemplate redisTemplate;/*** 根据分类id查询菜品** param categoryId* re…

Python爬虫实战第二例【二】

零.前言&#xff1a; 本文章借鉴&#xff1a;Python爬虫实战&#xff08;五&#xff09;&#xff1a;根据关键字爬取某度图片批量下载到本地&#xff08;附上完整源码&#xff09;_python爬虫下载图片-CSDN博客 大佬的文章里面有API的获取&#xff0c;在这里我就不赘述了。 一…

DP读书:开源软件的影响力(小白向)解读Embedded_SIG介绍以及代码架构解析

从一个SIG的文档来看&#xff0c;一个社区的生态。 开源 openEuler Embedded软件发行版的影响力 openEuler Embedded是基于openEuler社区面向嵌入式场景的Linux版本。 该版本与其他openEuler版本在内核和软件版本方面保持一致&#xff0c;但内核配置、软件包组合和配置以及特…

LeetCode 刷题 [C++] 第240题.搜索二维矩阵 II

题目描述 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。 每列的元素从上到下升序排列。 题目分析 通过分析矩阵的特点发现&#xff0c;其左下角和右上角可以看作一个“二叉搜索树的根节…

WPF 【十月的寒流】学习笔记(3):DataGrid分页

文章目录 前言相关链接代码仓库项目配置&#xff08;省略&#xff09;项目初始配置xamlviewModel Filter过滤详细代码展示结果问题 Linq过滤CollectionDataxamlviewModel sql&#xff0c;这里用到数据库&#xff0c;就不展开了 总结 前言 我们这次详细了解一下列表通知的底层是…

GIS之深度学习03:Anaconda无法正常启动问题汇总(更新)

在安装完成anaconda后&#xff0c;总会出现一些问题&#xff0c;以下为遇到的问题及解决方案&#xff1a; &#xff08;有问题请私信&#xff0c;持续更新&#xff09; 01&#xff1a;anaconda navigator启动时一直卡在 loading applications 页面 解决&#xff1a; 找到anac…

在Ubuntu22.04 LTS上搭建Kubernetes集群

文章目录 准备工作系统准备软件包准备主机和IP地址准备 安装步骤安装前准备关闭防火墙设置服务器时区关闭 swap 分区关闭SELinux配置hosts配置文件转发 IPv4 并让 iptables 看到桥接流量 安装容器运行时安装Kubernetes配置并安装apt包初始化集群 安装calico网络插件部署应用 本…

2-3 树

原文链接&#xff1a;https://blog.csdn.net/Ceylan__/article/details/125578130 一、2-3树 的定义 Q&#xff1a;什么是二叉排序树 A&#xff1a;二叉排序树或者是一棵空树&#xff0c;或者是具有如下性质的二叉树 1&#xff09;若它的左子树不空&#xff0c;则 左子树 上…