Spring系列(六) Spring Web MVC 应用构建分析

DispatcherServlet

DispatcherServlet 是Spring MVC的前端控制器名称, 用户的请求到达这里进行集中处理, 在Spring MVC中, 它的作用是为不同请求匹配对应的处理器, 将结果传递给视图解析器最终呈现给客户端.

前端控制器模式(Front Controller Pattern)是用来提供一个集中的请求处理机制,所有的请求都将由一个单一的处理程序处理。该处理程序可以做认证/授权/记录日志,或者跟踪请求,然后把请求传给相应的处理程序。

Servlet WebApplicationContext 和 Root WebApplicationContext

Spring MVC 存在两个应用上下文, 分别为Servlet WebApplicationContext和Root WebApplicationContext. 他们分别初始化不同类型的bean.

下图来自Spring官方文档

794700-20180930155304709-353183632.png

在DispatcherServlet启动的时候, 它会创建Spring上下文Servlet WebApplicationContext, 其中包含Web相关的Controller,ViewResolver,HandlerMapping等.

另外一个上下文Root WebApplicationContext是由ContextLoaderListener创建的, 包含除了Web组件外的其他bean, 比如包含业务逻辑的Service, 还有数据库相关的组件等.

代码(JavaConfig方式的配置代码)

下面是用JavaConfig方式实现的配置代码, 我们先搭建好一个Spring MVC 项目,然后结合源码分析Spring如何注册DispatcherServlet实例的.

// 继承AbstractAnnotationConfigDispatcherServletInitializer并重写其中的三个方法
public class MvcWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {// 指定Root上下文的配置类@Overrideprotected Class<?>[] getRootConfigClasses() {return new Class[]{ RootConfig.class };}// 指定Web上下文的配置类@Overrideprotected Class<?>[] getServletConfigClasses() {return new Class[]{ WebConfig.class };}// url映射@Overrideprotected String[] getServletMappings() {return new String[]{"/"};}
}

通过重写AbstractAnnotationConfigDispatcherServletInitializer的三个方法完成配置, WebConfig用来配置Web组件, RootConfig用来配置非Web组件.

@EnableWebMvc // 启用MVC
@ComponentScan(basePackages = {"com.xlx.mvc.web"}) // 启用组件扫描,只扫描web相关的组件
@Configuration
public class WebConfig implements WebMvcConfigurer {// 视图解析器,jsp@Beanpublic ViewResolver viewResolver(){InternalResourceViewResolver resolver = new InternalResourceViewResolver();resolver.setPrefix("/WEB-INF/views/");resolver.setSuffix(".jsp");resolver.setExposeContextBeansAsAttributes(true);return  resolver;}// 重写以启用默认的处理器, 用来处理静态资源@Overridepublic void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer){configurer.enable();}}@Configuration
@ComponentScan(basePackages = {"com.xlx.mvc"},  excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = EnableWebMvc.class)
}) // 扫描包, 但排除EnableWebMvc注解的类
public class RootConfig {}

源码分析

Servlet 3.0 旨在支持基于代码的方式配置Servlet容器, 当3.0兼容的servlet容器启动的时候会在ClassPath查找并调用实现了接口ServletContainerInitializer的类的onStartup()方法, Spring中提供了这个接口的一个实现类SpringServletContainerInitializer. 其启动方法的代码如下:

@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)throws ServletException {List<WebApplicationInitializer> initializers = new LinkedList<>();// 应用中WebApplicationInitializer的bean生成到一个列表中.if (webAppInitializerClasses != null) {for (Class<?> waiClass : webAppInitializerClasses) {if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&WebApplicationInitializer.class.isAssignableFrom(waiClass)) {try {initializers.add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass).newInstance());}catch (Throwable ex) {throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);}}}}if (initializers.isEmpty()) {servletContext.log("No Spring WebApplicationInitializer types detected on classpath");return;}servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");AnnotationAwareOrderComparator.sort(initializers);// 遍历所有WebApplicationInitializer, 并调用其onStartup方法for (WebApplicationInitializer initializer : initializers) {initializer.onStartup(servletContext);}
}

在上面方法的最后, 可以看到其将控制权交给WebApplicationInitializer的实例并遍历调用了onStartup()方法, 而我们定义的类MvcWebAppInitializer 就是它的子类. 完整的继承关系为

WebApplicationInitializer <--
AbstractContextLoaderInitializer <--
AbstractDispatcherServletInitializer <--
AbstractAnnotationConfigDispatcherServletInitializer <--
MvcWebAppInitializer

在类 AbstractDispatcherServletInitializer 中实现了onStartup()方法, 最终调用registerDispatcherServlet()方法完成注册, 两个方法的代码如下:

@Override
public void onStartup(ServletContext servletContext) throws ServletException {super.onStartup(servletContext);registerDispatcherServlet(servletContext);
}protected void registerDispatcherServlet(ServletContext servletContext) {// 获取Sevlet名称, 这个方法返回了默认值"dispatcher"String servletName = getServletName();Assert.hasLength(servletName, "getServletName() must not return null or empty");// 此处调用的方法是抽象方法, 由子类AbstractAnnotationConfigDispatcherServletInitializer实现, 其最终调用了自定义类的getServletConfigClasses()方法获取配置信息(源码附在本段后面). 用来生成Servlet上下文.WebApplicationContext servletAppContext = createServletApplicationContext();Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");// 生成dispatcherServlet实例FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());// 注册DispatcherServletServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);if (registration == null) {throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +"Check if there is another servlet registered under the same name.");}registration.setLoadOnStartup(1);registration.addMapping(getServletMappings());registration.setAsyncSupported(isAsyncSupported());Filter[] filters = getServletFilters();if (!ObjectUtils.isEmpty(filters)) {for (Filter filter : filters) {registerServletFilter(servletContext, filter);}}customizeRegistration(registration);
}

下面附读取Servlet配置类的代码: 类AbstractAnnotationConfigDispatcherServletInitializer实现了createServletApplicationContext(), 可以看到代码中调用了方法getServletConfigClasses(), 这是个抽象方法, 声明为protected abstract Class<?>[] getServletConfigClasses();. 最终的实现正是在我们自定义的子类MvcWebAppInitializer中.

@Override
protected WebApplicationContext createServletApplicationContext() {AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();// 读取配置类Class<?>[] configClasses = getServletConfigClasses();if (!ObjectUtils.isEmpty(configClasses)) {context.register(configClasses);}return context;
}

上面完成了DispatcherServlet的注册和启动, 接下来可以定义Controller了.

请求映射

在此之前需要了解下关于URL映射的Servlet规范, 注意这是Servlet的规范, 当然也适用于DispatcherServlet, 代码中我们为DispatcherServlet映射为"/", 规范中"/"为使用"default"Servlet, 也就意味着所有的请求默认通过DispatcherServlet处理.

为了处理静态资源, 在WebConfig中覆盖了方法configureDefaultServletHandling()已启用静态资源处理器DefaultServletHttpRequestHandler, 它的优先级是最低, 这意味着在匹配不到其他handler的时候,servlet会将请求交给这个handler处理.

规则按顺序执行,匹配到就直接返回.

  1. 精确匹配, url完全与模式匹配
  2. 最长路径匹配, 查找模式中路径最长的匹配项, 例如/user/list/1匹配模式/user/list/, 而不是/user/
  3. 扩展名匹配
  4. 默认Servlet

代码

@Controller
@RequestMapping(value = "/home")
public class HomeController {@RequestMapping(value = "/default",method = RequestMethod.GET)public String home(){return "home";}
}

源码分析

我们的Controller以注解(@RequestMapping,@GetMapping等)方式定义, RequestMappingHandlerMapping用来生成请求url与处理方法的映射关系(mapping),这个mapping最终是由DispatcherServlet调用找到匹配到url对应的controller方法并调用.

通过查看Spring的bean依赖关系图(找到类WebConfig, Ctrl+Alt+U并选spring beans dependency)可以找到RequestMappingHandlerMapping生成的线索.

简化的关系图如下:

794700-20180930155411844-186474857.png

可以看到WebmvcConfigurationSupport中有个@Bean注解的方法生成RequestMappingHandlerMapping的实例, 而WebmvcConfigurationSupport继承了DelegatingWebMvcConfiguration, 后者是由@EnableWebMvc注解导入.

/*** * 返回排序为0的RequestMappingHandlerMapping实例bean, 用来处理注解方式的Controller请求.*/
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();// 顺序为0, 顺便提一句, 静态资源的处理器Handler的顺序为Integer.Maxmapping.setOrder(0);mapping.setInterceptors(getInterceptors());mapping.setContentNegotiationManager(mvcContentNegotiationManager());mapping.setCorsConfigurations(getCorsConfigurations());PathMatchConfigurer configurer = getPathMatchConfigurer();Boolean useSuffixPatternMatch = configurer.isUseSuffixPatternMatch();if (useSuffixPatternMatch != null) {mapping.setUseSuffixPatternMatch(useSuffixPatternMatch);}Boolean useRegisteredSuffixPatternMatch = configurer.isUseRegisteredSuffixPatternMatch();if (useRegisteredSuffixPatternMatch != null) {mapping.setUseRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch);}Boolean useTrailingSlashMatch = configurer.isUseTrailingSlashMatch();if (useTrailingSlashMatch != null) {mapping.setUseTrailingSlashMatch(useTrailingSlashMatch);}UrlPathHelper pathHelper = configurer.getUrlPathHelper();if (pathHelper != null) {mapping.setUrlPathHelper(pathHelper);}PathMatcher pathMatcher = configurer.getPathMatcher();if (pathMatcher != null) {mapping.setPathMatcher(pathMatcher);}Map<String, Predicate<Class<?>>> pathPrefixes = configurer.getPathPrefixes();if (pathPrefixes != null) {mapping.setPathPrefixes(pathPrefixes);}return mapping;
}

好了, 现在有了DispatcherServlet, 并且有了可以处理映射关系的RequestMappingHandlerMapping, 接下来再看下当请求到达时, DispatcherServlet 如何为Url找到对应的Handler方法.

DispatcherServlet中定义了处理请求的doService()方法, 最终这个方法委托doDispatch()处理请求, 特别注意中文注释的几个语句, 除此之外, 这个方法还提供了生命周期的一些处理工作.

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;try {processedRequest = checkMultipart(request);multipartRequestParsed = (processedRequest != request);// 获取当前请求对应的handlermappedHandler = getHandler(processedRequest);if (mappedHandler == null) {noHandlerFound(processedRequest, response);return;}// 获取当前请求对应handler的适配器HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());// Process last-modified header, if supported by the handler.String method = request.getMethod();boolean isGet = "GET".equals(method);if (isGet || "HEAD".equals(method)) {long lastModified = ha.getLastModified(request, mappedHandler.getHandler());if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {return;}}if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}// 最终调用Handler的方法mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}applyDefaultViewName(processedRequest, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);}catch (Exception ex) {dispatchException = ex;}catch (Throwable err) {// As of 4.3, we're processing Errors thrown from handler methods as well,// making them available for @ExceptionHandler methods and other scenarios.dispatchException = new NestedServletException("Handler dispatch failed", err);}processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);}catch (Exception ex) {triggerAfterCompletion(processedRequest, response, mappedHandler, ex);}catch (Throwable err) {triggerAfterCompletion(processedRequest, response, mappedHandler,new NestedServletException("Handler processing failed", err));}finally {if (asyncManager.isConcurrentHandlingStarted()) {// Instead of postHandle and afterCompletionif (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}}else {// Clean up any resources used by a multipart request.if (multipartRequestParsed) {cleanupMultipart(processedRequest);}}}
}

上面代码中, 重点关注getHandler方法.

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {if (this.handlerMappings != null) {for (HandlerMapping mapping : this.handlerMappings) {HandlerExecutionChain handler = mapping.getHandler(request);if (handler != null) {return handler;}}}return null;
}

可以看到请求所需的handler是取自实例变量this.handlerMappings,接下来顺藤摸瓜, 看这个变量是何时初始化的.通过引用, 我们查找到了下面方法.

private void initHandlerMappings(ApplicationContext context) {this.handlerMappings = null;if (this.detectAllHandlerMappings) {// 找到上下文中的所有HandlerMapping, 包括祖先上下文Map<String, HandlerMapping> matchingBeans =BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);if (!matchingBeans.isEmpty()) {this.handlerMappings = new ArrayList<>(matchingBeans.values());// HandlerMapping排序AnnotationAwareOrderComparator.sort(this.handlerMappings);}}else {try {HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);this.handlerMappings = Collections.singletonList(hm);}catch (NoSuchBeanDefinitionException ex) {// Ignore, we'll add a default HandlerMapping later.  // 这个注释...}}// 保证至少要有一个HandlerMapping.if (this.handlerMappings == null) {this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);if (logger.isTraceEnabled()) {logger.trace("No HandlerMappings declared for servlet '" + getServletName() +"': using default strategies from DispatcherServlet.properties");}}
}

整理下调用关系: DispatcherServlet initHandlerMappings <-- initStrategies <-- onRefresh <--
FrameworkServlet initWebApplicationContext <-- initServletBean <--
HttpServletBean init <--
GenericServlet init(ServletConfig config)
最后的GenericServlet是servlet Api的.

Spring Boot 中的DispatcherServlet

Spring Boot微服务中的DispatcherServlet装配, 因为其一般使用内置的Servlet容器, 是通过DispatcherServletAutoConfiguration来完成的. 下面是生成DispatcherServlet bean的代码, 这个bean在内部静态类DispatcherServletConfiguration中.

@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet() {DispatcherServlet dispatcherServlet = new DispatcherServlet();dispatcherServlet.setDispatchOptionsRequest(this.webMvcProperties.isDispatchOptionsRequest());dispatcherServlet.setDispatchTraceRequest(this.webMvcProperties.isDispatchTraceRequest());dispatcherServlet.setThrowExceptionIfNoHandlerFound(this.webMvcProperties.isThrowExceptionIfNoHandlerFound());return dispatcherServlet;
}

上面我们通过注解方式构建了一个MVC应用程序, 并且通过源码分析其构建原理, 其中Spring使用的前端控制器实现类是DispatcherServlet, 其在Servlet容器启动的时候实例化, 并初始化容器中的Handler处理器. 当请求到达DispatcherServlet时会调用其doDispatcher()方法选择最合适的处理器. 最后我们扫了一眼Spring Boot的自动装配DispatcherServlet方式.

转载于:https://www.cnblogs.com/walkinhalo/p/9732125.html

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

做个好人,加个晚班

我和建平在腾讯加班的日子前几天&#xff0c;他让我给他发照片&#xff0c;因为公司里年末要发照片墙&#xff0c;他说要把我的和他的照片放上去&#xff0c;然后我硬是找了几张觉得不错的给他&#xff0c;然后他也真的放上去了。再看那些照片&#xff0c;拍出来的效果都不咋样…

中ridge_10种线性代数在数据科学中的强大应用(内附多种资源)

原文选自 | Analytics Vidhya作者 | Khyati Mahendru本文转载自 TalkingData数据学堂 &#xff0c;未经允许禁止转载本文摘要线性代数为各种各样的数据科学算法和应用提供支持在这里&#xff0c;我会向您介绍通过线性代数帮助您成为更好的数据科学家的10种实际应用我们已将这些…

语言与golang语言运行速度_Golang语言情怀第13期 Go 语言设计模式 介绍

设计模式是什么俗话说&#xff1a;站在别人的肩膀上&#xff0c;我们会看得更远。设计模式的出现可以让我们站在前人的肩膀上&#xff0c;通过一些成熟的设计方案来指导新项目的开发和设计&#xff0c;以便于我们开发出具有更好的灵活性和可扩展性&#xff0c;也更易于复用的软…

苦练IoT应用开发,还能加速变现,这个机会别错过

都说人间大事&#xff0c;不过吃喝二字。厨房经济近年来显示出了巨大发展潜力&#xff0c;智能厨电已成为潮流趋势。智慧厨电究竟是如何——让厨房小白做出一顿可口大餐&#xff1f;让懒人摆脱厨房油烟和洗碗的困扰&#xff1f;让怕冷的人喝到永远55℃的热水&#xff1f;……在…

android人脸识别demo_零门槛解决Windows人脸识别应用开发难题

自人脸识别免费SDK——ArcFace3.0上线以来&#xff0c;凭借对人脸识别、活体检测、年龄检测、性别检测等核心算法模型进行全面升级&#xff0c;大幅提升算法鲁棒性&#xff0c;显著降低接入门槛&#xff0c;同时支持Windows、iOS、Android&#xff08;包含Android10&#xff09…

Visual Studio会让嵌入式开发变得更香

在几个月之前&#xff0c;我一直非常喜欢用Source Insight看代码&#xff0c;主要是习惯了原来的风格。从Source Insight 转到vscode 的原因是&#xff0c;在腾讯使用samba连接Source Insight看代码非常非常卡&#xff0c;让我觉得很难受。然后是在同事的建议下更换了vscode,里…

现实世界的Windows Azure:采访InishTech的销售及市场部主管Andrew O’Connor

MSDN: 告诉我们关于你们公司的信息以及您为Windows Azure创建的解决方案。O’Connor: InishTech 有点不寻常。我们的软件许可和保护服务&#xff08;SLPS&#xff09;平台是一个传统的多租户Windows Azure应用程序&#xff0c;利用Windows Azure SDK、 Windows Azure Dev Fabri…

珠海半导体公司招聘

受一个朋友所托&#xff0c;帮忙发一个招聘信息公司名字&#xff1a;珠海极海半导体有限公司上班地点&#xff1a;广州岗位名称&#xff1a;FAE工程师岗位要求&#xff1a;薪资&#xff1a;15K左右&#xff0c;会根据实际面试情况做相应调整。一些聊天内容的消息供大家参考&…

Linux同步原语系列-spinlock及其演进优化

1. 引言通常我们的说的同步其实有两个层面的意思&#xff1a;一个是线程间的同步&#xff0c;主要是为了按照编程者指定的特定顺序执行&#xff1b;另外一个是数据的同步&#xff0c;主要是为了保存数据。为了高效解决同步问题&#xff0c;前人抽象出同步原语供开发者使用。不仅…

linux环境部署python3+django

1. 确定Linux安装C/C编译器,在线安装: yum install gcc gcc-c autoconf automake 2. 安装依赖环境: yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel 3. …

hp-socket 文本跟图片同时发送_文本、截图和应用,这样「包装」一秒变美观

社交网络被发明出来的一个重要目的就是分享&#xff0c;无论是所见所闻还是喜怒哀乐&#xff0c;亦或是脑海中突然迸发出的一段妙言&#xff0c;某个转瞬即逝的精彩瞬间&#xff0c;一篇深度好文&#xff0c;一个在少数派看到的绝赞应用……分享内容丰富多彩&#xff0c;相比之…

poj2041

简单题 View Code #include <iostream>#include <cstdio>#include <cstdlib>#include <cstring>#include <algorithm>using namespace std;#define maxn 30char order[maxn];char st[maxn];int l;void work(char ch){char x;int d;switch (ch) …

[转]Angular2 Material2 封装组件 —— confirmDialog确定框

本文转自&#xff1a;https://www.jianshu.com/p/0c566fc1730d 环境&#xff1a; Angular 4.0.0 Angular2 Material2 2.0.0-beta.3 node v7.4.0 npm 4.0.5 使用Dialog封装confirmDialog确定框 源代码 来&#xff0c;首先来看效果图~ 删除例子确定删除框点击确定后返回值1.定义通…

项目实战|100个蓝牙接收器发货了

还记得之前写的这篇文章不&#xff1a;《一个蓝牙实战项目的掏肺总结》&#xff0c;这个项目最近做完了&#xff0c;交了100套出去&#xff0c;这是发货前的大合照&#xff1a;做完此项目&#xff0c;有些许收获&#xff0c;在此分享给大家。东西虽简单&#xff0c;但它依然是一…

深圳的冬天真的来了

天气预报说&#xff0c;明天深圳的天气会很低。热了一整年&#xff0c;这次一定是真的了&#xff0c;大家出门记得穿厚一些。

date js 半年_moment.js 搜索栏获取最近一周,一个月,三个月,半年,一年时间

统计时间label: 统计时间,name: countTime,type: select,data: [{value: 0,text: 最近一周},{value: 1,text: 一个月},{value: 2,text: 三个月},{value: 3,text: 半年},{value: 4,text: 一年}]实现方式searchValue为搜索栏所选所填内容&#xff0c; 以及作为搜索和接口调用条件…

华为eudemon 200E的hrp双心跳热备配置

本文为大家介绍使用两台华为Eudemon200E防火墙实现双机双心跳的HRP热备的配置实例&#xff0c;主要的知识点包括&#xff1a;华为防火墙HRP、VRRP的配置&#xff0c;定义防火墙区域。 一、网络拓扑&#xff1a;二、配置要求&#xff1a; 1、两台防火墙为E200E-A和E200E-B&#…

2021 年 Linux 界的 12 件大事

2021年即将结束了&#xff0c;今天就和大家分享一些来自Linux世界最重要的大事&#xff0c;这些事件大大影响了Linux用户&#xff1a;1、理查德斯托曼回归2019年&#xff0c;自由软件基金会(Free Software Foundation)创始人理查德斯托曼&#xff08;Richard Stallman&#xff…

关于Treap的学习感受

好了我就很愉快的回来补坑了~ Treap也是一种平衡树&#xff0c;它较普通二叉查找树而言&#xff0c;每个节点被赋予了一个新的属性&#xff1a;优先级&#xff08;没错就是类似优先队列的优先&#xff09;&#xff0c;对于Treap中的每个结点&#xff0c;除了它的权值满足二叉查…

2022年考研结束了

为期两天的研究生考试结束了。我没参加研究生考试&#xff0c;所以对研究生考试的压力不从得知&#xff0c;我从一个外人的角度来看&#xff0c;这无非就是一个简单的考试&#xff0c;考上了欢喜雀跃&#xff0c;考不上嘛&#xff0c;我就会说&#xff0c;大不了来年再考一次&a…