Spring MVC系列之异步请求

概述

Spring MVC的本质其实就是一个Servlet。在理解Spring MVC如何支持异步请求之前,需要先知道Servlet3异步如何支持异步请求。参考Servlet系列之Servlet3异步。

Spring MVC对异步请求的支持主要从三个类来看:

  • AsyncWebRequest:request
  • WebAsyncManager:处理异步请求的管理器
  • WebAsyncUtils:工具类

Spring MVC将异步请求细分为Callable、WebAsyncTask、DeferredResult三种类型,前两种是一类,核心是Callable。

组件

Spring MVC中异步请求涉及到的相关组件如下。

DeferredResult

Spring提供的一种用于保存延迟处理结果的泛型类,当一个处理器返回DeferredResult类型的返回值时将启动异步处理。

org.springframework.web.context.request.async.DeferredResult源码,版本为spring-web-6.1.5

public class DeferredResult<T> {
	private static final Object RESULT_NONE = new Object();
	@Nullable
	private final Long timeoutValue;
	private final Supplier<?> timeoutResult;
	@Nullable
	private Runnable timeoutCallback;
	@Nullable
	private Consumer<Throwable> errorCallback;
	@Nullable
	private Runnable completionCallback;
	@Nullable
	private DeferredResultHandler resultHandler;
	@Nullable
	private volatile Object result = RESULT_NONE;
	private volatile boolean expired;
}

AsyncRestTemplate

模板类方法,位于org.springframework.web.client包路径下,引入spring-web模块即可使用,但自Spring 5.0版本就被标记为@Deprecated。替换类为org.springframework.web.reactive.function.client.WebClient

WebClient

Spring 5新增的非阻塞、响应式HTTP客户端,更适合于异步请求和响应处理。

AsyncWebRequest

org.springframework.web.context.request.async.AsyncWebRequest源码:

public interface AsyncWebRequest extends NativeWebRequest {
	void setTimeout(@Nullable Long timeout);
	// 添加请求超时处理器
	void addTimeoutHandler(Runnable runnable);
	// 添加错误处理器
	void addErrorHandler(Consumer<Throwable> exceptionHandler);
	// 添加请求处理完成处理器
	void addCompletionHandler(Runnable runnable);
	void startAsync();
	// 判断是否启动异步处理
	boolean isAsyncStarted();
	void dispatch();
	// 判断异步处理是否已经处理完成
	boolean isAsyncComplete();
}

实现类有两个:

  • NoSupportAsyncWebRequest:不支持异步请求
  • StandardServletAsyncWebRequest:实际用作异步请求。除实现AsyncWebRequest接口外,还实现AsyncListener接口并继承ServletWebRequest

StandardServletAsyncWebRequest源码略,封装AsyncContext类型的属性asyncContext,在startAsync方法中会将Request#startAsync返回的AsyncContext设置给它,然后在别的地方主要使用它来完成各种功能。由于StandardServletAsyncWebRequest实现AsyncListener接口,所以它自己就是一个监听器,而且在startAsync方法中在创建出AsyncContext后会将自己作为监听器添加进去。监听器实现方法中onStartAsync方法和onError方法是空实现,onTimeout方法和onComplete方法分别调用封装的两个List<Runnable>类型的属性timeoutHandlers和completionHandlers所保存的Runnable方法,这样在使用时只需要简单地将需要监听超时和处理完成的监听方法添加到这两个属性中即可。

CallableProcessingInterceptor

拦截器接口,6个方法:

default <T> void beforeConcurrentHandling(NativeWebRequest request, Callable<T> task) throws Exception {
}
default <T> void preProcess(NativeWebRequest request, Callable<T> task) throws Exception {
}
default <T> void postProcess(NativeWebRequest request, Callable<T> task,
		@Nullable Object concurrentResult) throws Exception {
}
default <T> Object handleTimeout(NativeWebRequest request, Callable<T> task) throws Exception {
	return RESULT_NONE;
}
default <T> Object handleError(NativeWebRequest request, Callable<T> task, Throwable t) throws Exception {
	return RESULT_NONE;
}
default <T> void afterCompletion(NativeWebRequest request, Callable<T> task) throws Exception {
}

拦截器的作用:在不同的时间点通过执行相应的方法来做一些额外的事情,理解拦截器的核心是理解它里边的各个方法执行的时间点。beforeConcurrentHandling方法是在并发处理前执行的,会在主线程中执行,其他方法都在具体处理请求的子线程中执行。

DeferredResultProcessingInterceptor

拦截器接口,6个方法,命名和上面一模一样。

CallableInterceptorChain

CallableInterceptorChain用于封装CallableProcessingInterceptor,将多个相应的拦截器封装到一个List类型的属性,然后在相应的方法中调用所封装的Interceptor相应方法进行处理。责任链模式。方法名与Interceptor中稍有区别,对应关系如下:

  • applyBeforeConcurrentHandling:对应Interceptor中的beforeConcurrentHandling方法
  • applyPreProcess:对应Interceptor中的preProcess方法
  • applyPostProcess:对应Interceptor中的postProcess方法
  • triggerAfterTimeout:对应Interceptor中的afterTimeout方法
  • triggerAfterCompletion:对应Interceptor中的afterCompletion方法
  • triggerAfterError:对应Interceptor中的handleError方法

DeferredResultInterceptorChain

同上,用于封装DeferredResultProcessingInterceptor。

WebAsyncTask

WebAsyncTask是一个泛型类,封装Callable方法,并提供一些异步调用相关的属性:

public class WebAsyncTask<V> implements BeanFactoryAware {
	// 用来实际处理请求
	private final Callable<V> callable;
	// 用于设置超时时间
	@Nullable
	private final Long timeout;
	// 用来调用callable
	@Nullable
	private final AsyncTaskExecutor executor;
	// 用容器中注册的名字配置executor
	@Nullable
	private final String executorName;
	// 用于根据名字获取executor
	@Nullable
	private BeanFactory beanFactory;
	// 用于执行超时的回调
	@Nullable
	private Callable<V> timeoutCallback;
	// 用于发生错误的回调
	@Nullable
	private Callable<V> errorCallback;
	// 用于请求处理完成的回调
	@Nullable
	private Runnable completionCallback;
}

executor可以直接设置到WebAsyncTask中,也可使用注册在容器中的名字来设置executorName属性:

@Nullable
public AsyncTaskExecutor getExecutor() {
	if (this.executor != null) {
		return this.executor;
	} else if (this.executorName != null) {
		Assert.state(this.beanFactory != null, "BeanFactory is required to look up an executor bean by name");
		return this.beanFactory.getBean(this.executorName, AsyncTaskExecutor.class);
	} else {
		return null;
	}
}

WebAsyncManager

Spring MVC处理异步请求过程中最核心的类,管理着整个异步处理的过程。

几个重要属性:

  • timeoutCallableInterceptor:CallableProcessingInterceptor类型,专门用于Callable和WebAsyncTask类型超时的拦截器
  • timeoutDeferredResultInterceptor:DeferredResultProcessingInterceptor类型,专门用于DeferredResult和ListenableFuture类型超时的拦截器
  • callableInterceptors:Map类型,用于所有Callable和WebAsyncTask类型的拦截器
  • deferredResultInterceptors:Map类型,用于所有DeferredResult和ListenableFuture类型的拦截器
  • asyncWebRequest:为了支持异步处理而封装的request
  • taskExecutor:用于执行Callable和WebAsyncTask类型处理,如果WebAsyncTask中没有定义executor则使用WebAsyncManager中的taskExecutor。

最重要的两个方法是startCallableProcessing和startDeferredResultProcessing,是启动异步处理的入口方法。它们一共做三件事:

  • 启动异步处理;
  • 给Request设置相应属性(主要包括timeout、timeoutHandler和completionHandler);
  • 在相应位置调用相应的Spring MVC自定义的拦截器。

startCallableProcessing方法用于处理Callable和WebAsyncTask类型的异步请求,使用CallableProcessingInterceptor,拦截器封装在CallableInterceptorChain类型的拦截器链中统一调用。

startDeferredResultProcessing方法用于处理DeferredResult类型的异步请求,使用DeferredResultProcessingInterceptor拦截器,拦截器封装在DeferredResultInterceptorChain类型的拦截器链中统一调用。和startCallableProcessing方法执行过程类似,只是并没有使用taskExecutor来提交执行,这是因为DeferredResult并不需要执行处理。

startCallableProcessing方法主要做5件事:

  • 将webAsyncTask中相关属性取出并设置到对应的地方;
  • 初始化拦截器链;
  • 给asyncWebRequest设置timeoutHandler和completionHandler;
  • 执行处理器链中相应方法;
  • 启动异步处理并使用taskExecutor提交任务。

启动处理是调用startAsyncProcessing方法,源码如下:

private void startAsyncProcessing(Object[] processingContext) {
	synchronized (WebAsyncManager.this) {
		this.concurrentResult = RESULT_NONE;
		this.concurrentResultContext = processingContext;
	}
	Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
	this.asyncWebRequest.startAsync();
}

做3件事:

  • 清空之前并发处理的结果
  • 将processingContext设置给concurrentResultContext属性
  • 调用asyncWebRequest的startAsync方法启动异步处理

processingContext参数传进来的是处理器中使用的ModelAndViewContainer,concurrent-ResultContext用来在WebAsyncManager中保存ModelAndViewContainer,在请求处理完成后会设置到RequestMappingHandlerAdapter中。

执行处理,执行处理使用的是taskExecutor,这里并没直接使用taskExecutor.submit(callable)来提交,而是提交新建的Runnable,并将Callable的call方法直接放在run方法里调用:

try {
	Future<?> future = this.taskExecutor.submit(() -> {
		Object result = null;
		try {
			interceptorChain.applyPreProcess(this.asyncWebRequest, callable);
			result = callable.call();
		} catch (Throwable ex) {
			result = ex;
		} finally {
			result = interceptorChain.applyPostProcess(this.asyncWebRequest, callable, result);
		}
		setConcurrentResultAndDispatch(result);
	});
	interceptorChain.setTaskFuture(future);
} catch (Throwable ex) {
	Object result = interceptorChain.applyPostProcess(this.asyncWebRequest, callable, ex);
	setConcurrentResultAndDispatch(result);
}

这么做主要有两个作用:

  • 可以在处理过程中的相应位置调用拦截器链中相应的方法;
  • 在call方法执行完之前不会像Future#get()那样阻塞线程。

Runnable是没有返回值的,所以Callable处理的结果需要自己从run方法内部传递出来,WebAsyncManager.setConcurrentResultAndDispatch方法来处理返回的结果,这里边会将处理的结果传递出来:

// 省略日志打印
private void setConcurrentResultAndDispatch(@Nullable Object result) {
	// 检查asyncWebRequest和状态
	Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
	synchronized (WebAsyncManager.this) {
		if (!this.state.compareAndSet(State.ASYNC_PROCESSING, State.RESULT_SET)) {
			return;
		}
		// 设置异步处理结果
		this.concurrentResult = result;
		// 检查Request是否已设置为异步处理完成状态(网络中断会造成Request设置为异步处理完成状态)
		if (this.asyncWebRequest.isAsyncComplete()) {
			return;
		}
		// 发送请求
		this.asyncWebRequest.dispatch();
	}
}

concurrentResult用来保存异步处理结果的属性。Spring MVC中异步请求处理完成后会再次发起一个相同的请求,然后在HandlerAdapter中使用一个特殊的HandlerMethod来处理它,具体过程后面再讲解,不过通过Request的dispatch方法发起的请求使用的还是原来的Request,也就是说原来保存在Request中的属性不会丢失。

WebAsyncUtils

源码省略。两个重载的getAsyncManager方法通过Request获取WebAsyncManager,分别是ServletRequest、WebRequest类型的Request,获取过程都是先判断Request属性里是否有保存的WebAsyncManager对象,如果有则取出后直接返回,如果没有则新建一个设置到Request的相应属性中并返回,下次再获取时直接从Request属性中取出。

createAsyncWebRequest方法用于创建AsyncWebRequest,调用上面提到的getAsyncManager方法获取WebAsyncManager,然后获取AsyncWebRequest。进而创建StandardServletAsyncWebRequest类型的Request并返回。

原理

Spring MVC对异步请求的处理主要在四个地方进行支持:

  • FrameworkServlet中给当前请求的WebAsyncManager添加CallableProcessingInterceptor类型的拦截器RequestBindingInterceptor,这是定义在FrameworkServlet内部的私有拦截器,其作用还是跟FrameworkServlet处理正常请求一样,在请求处理前将当前请求的LocaleContext和ServletRequestAttributes设置到LocaleContextHolder和RequestContextHolder中,并在请求处理完成后恢复,添加过程在processRequest方法中
  • RequestMappingHandlerAdapter的invokeHandleMethod方法提供对异步请求的核心支持,其中做四件跟异步处理相关的事情,下文详述
  • 返回值处理器:一共有四个处理异步请求的返回值处理器,它们分别是AsyncTaskMethodReturnValueHandler、CallableMethodReturnValueHandler、DeferredResultMethodReturnValueHandler和ListenableFutureReturnValueHandler,每一个对应一种类型的返回值,作用主要是使用WebAsyncManager启动异步处理
  • 在DispatcherServlet的doDispatch方法中,当HandlerAdapter使用Handler处理完请求时,会检查是否已经启动异步处理,如果启动则不再往下处理,直接返回

RequestMappingHandlerAdapter

RequestMappingHandlerAdapter.invokeHandleMethod()方法源码略,四件事:

  • 创建AsyncWebRequest并设置超时时间,具体时间可以通过asyncRequestTimeout属性配置到RequestMappingHandlerAdapter中。
  • 对当前请求的WebAsyncManager设置了四个属性:taskExecutor、asyncWebRequest、callableInterceptors和deferredResultInterceptors,除了asyncWebRequest的另外三个都可以在RequestMappingHandlerAdapter中配置,taskExecutor如果没配置将默认使用MvcSimpleAsyncTaskExecutor(继承自SimpleAsyncTaskExecutor)。
  • 如果当前请求是异步请求而且已经处理出结果,则将异步处理结果与之前保存到WebAsyncManager里的ModelAndViewContainer取出来,并将WebAsyncManager里的结果清空,然后调用ServletInvocableHandlerMethod的wrapConcurrentResult方法创建ConcurrentResultHandlerMethod类型(ServletInvocableHandlerMethod内部类)的ServletInvocable-HandlerMethod来替换自己,创建出来的ConcurrentResultHandlerMethod并不执行请求,它的主要功能是判断异步处理的结果是不是异常类型,如果是则抛出,如果不是则使用ReturnValueHandler对其进行解析并返回。
  • 如果requestMappingMethod的invokeAndHandle方法执行完后检查到当前请求已经启动了异步处理,则会直接返回null。

调用ServletInvocableHandlerMethod的wrapConcurrentResult方法创建新的ServletInvocableHandlerMethod来处理异步处理的结果。ConcurrentResultHandlerMethod是在ServletInvocableHandlerMethod中定义的继承自ServletInvocableHandlerMethod的内部类:

private class ConcurrentResultHandlerMethod extends ServletInvocableHandlerMethod {
	private final MethodParameter returnType;

	public ConcurrentResultHandlerMethod(@Nullable Object result, ConcurrentResultMethodParameter returnType) {
		super((Callable<Object>) () -> {
			if (result instanceof Exception exception) {
				throw exception;
			} else if (result instanceof Throwable throwable) {
				throw new ServletException("Async processing failed: " + result, throwable);
			}
			return result;
		}, CALLABLE_METHOD);
		if (ServletInvocableHandlerMethod.this.returnValueHandlers != null) {
			setHandlerMethodReturnValueHandlers(ServletInvocableHandlerMethod.this.returnValueHandlers);
		}
		this.returnType = returnType;
	}
	// 省略4个方法
}

ConcurrentResultHandlerMethod调用父类的构造方法(super)将HandlerMethod中的Handler和Method都替换掉,Handler用新建的匿名Callable,Method使用ServletInvocableHandlerMethod的静态属性CALLABLE_METHOD,它代码Callable的call方法。新建的Callable的执行逻辑也非常简单,就是判断异步处理的返回值是不是异常类型,如果是则抛出异常,不是则直接返回,然后使用和原来请求一样的返回值处理器处理返回值(因为在构造方法中将原来ServletInvocableHandlerMethod的返回值处理器设置给自己)。

流程

主要处理流程是这样的:首先在处理器中返回需要启动异步处理的类型时(三种类型)相应返回值处理器会调用WebAsyncManager的相关方法启动异步处理,然后在DispatcherServlet中将原来请求直接返回,当异步处理完成后会重新发出一个相同的请求,这时在RequestMappingHandlerAdapter中会使用特殊的ServletInvocableHandlerMethod来处理请求,处理方法是:如果异步处理返回的结果是异常类型则抛出异常,否则直接返回异步处理结果,然后使用返回值处理器处理,接着返回DispatcherServlet中按正常流程往下处理。

异步处理完成后会重新发起一个请求,这时会重新查找HandlerMethod并初始化PathVariable、MatrixVariable等参数,重新初始化Model中的数据并再次执行HandlerInterceptor中相应的方法。这么做主要是可以复用原来的那套组件进行处理而不需要重新定义。不过新请求的HandlerMethod是用的专门的类型,而Model是使用的原来保存在WebAsyncManager的concurrentResultContext属性中的ModelAndViewContainer所保存的Model,所以这里的查找HandlerMethod和初始化Model的过程是没用的,可进行一些优化,如,将创建ConcurrentResultHandlerMethod的过程放在HandlerMapping中(这样也更符合组件的功能),然后在调用ModelFactory的initModel方法前判断是不是异步处理dispatcher过来的请求,如果是则不再初始化,或者干脆创建新的HandlerAdapter来处理。

返回

当处理器方法返回WebAsyncTask或Callable类型时将自动启用异步处理。

当处理器方法返回WebAsyncTask类型的返回值时,Spring MVC使用AsyncTaskMethodReturnValueHandler来加以处理:

@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
		ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
	if (returnValue == null) {
		mavContainer.setRequestHandled(true);
		return;
	}
	WebAsyncTask<?> webAsyncTask = (WebAsyncTask<?>) returnValue;
	if (this.beanFactory != null) {
		webAsyncTask.setBeanFactory(this.beanFactory);
	}
	WebAsyncUtils.getAsyncManager(webRequest).startCallableProcessing(webAsyncTask, mavContainer);
}

如果返回值为null,就会给mavContainer设置为请求已处理,然后返回。如果返回值不为null,调用WebAsyncManager的startCallableProcessing方法处理请求。WebAsyncManager是使用WebAsyncUtils获取的。

当返回Callable类型时,使用CallableMethodReturnValueHandler来处理,源码略。

参考

  • 看透Spring MVC:源码分析与实践

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ubuntu neo4j 下载与配置(一)

neo4j 官方下载页面 https://neo4j.com/deployment-center/#community 进入页面之后&#xff0c;往下滑 咱们在下载neo4j时&#xff0c;官方可能要咱们填写一下个人信息&#xff0c;比如&#xff1a;姓名组织结构邮箱等&#xff1a; 咱们可以观察一下&#xff0c;ne4j的下载链…

iOS 实现类似抖音翻页滚动效果

这里是效果图 参考抖音的滚动效果&#xff0c;需要我们在结束拖动的时候&#xff0c;动画设置偏移量 这里有一个注意点&#xff0c;由于我们是在拖动结束的时候&#xff0c;手动改变tableview的偏移量&#xff0c; 改变了tableView 自身原有的的滚动效果&#xff0c;所以我们…

C++奇迹之旅:类和对象const成员static关键字友元内部类

文章目录 &#x1f4dd;const成员&#x1f320; const 成员函数是什么&#xff1f;&#x1f320; 取地址及const取地址操作符重载 &#x1f309;static成员&#x1f320;概念&#x1f320;static特性&#x1f309;static小题 &#x1f320;友元&#x1f309; 友元函数&#x1f…

npm安装时一直idealTree:npm: sill idealTree buildDeps卡住不动

npm安装时一直idealTree:npm: sill idealTree buildDeps卡住不动 解决步骤&#xff1a; 1.去以下的目录中删掉.npmrc文件&#xff08;只在C:\User.npmrc&#xff09; 2.清除缓存&#xff0c;使用npm cache verify 不要用npm cache clean --force&#xff0c;容易出现npm WAR…

国产AI大模型加速“上车”

上海白领刘先生&#xff0c;坐上他的汽车主驾&#xff0c;向右扭头说&#xff1a;“打开那窗户。”话音刚落&#xff0c;副驾驶的车窗自动开了。 这辆车搭载了基于国产AI大模型的智能系统&#xff0c;就像有了人的大脑和神经网络&#xff0c;通过学习提升语音、视觉等多模态感…

VCSA6.7重置root密码

VCSA6.7重置root密码 1、登录VCSA所运行的ESXI主机 2、打开VCSA虚拟机Web控制台&#xff0c;先拍摄一个快照&#xff0c;然后重启虚拟机&#xff0c;在如下界面按"e" 3、找到linux开头的段落&#xff0c;在末尾追加rw init/bin/bash; 4、输入完成后&#xff0c;按&…

《异常检测——从经典算法到深度学习》27 可执行且可解释的在线服务系统中重复故障定位方法

《异常检测——从经典算法到深度学习》 0 概论1 基于隔离森林的异常检测算法 2 基于LOF的异常检测算法3 基于One-Class SVM的异常检测算法4 基于高斯概率密度异常检测算法5 Opprentice——异常检测经典算法最终篇6 基于重构概率的 VAE 异常检测7 基于条件VAE异常检测8 Donut: …

溪谷软件:游戏联运有多简单?

游戏联运&#xff0c;即游戏联合运营&#xff0c;是一种游戏运营模式&#xff0c;涉及到多个平台或公司共同推广和运营同一款游戏。对于开发者而言&#xff0c;游戏联运的简化程度可能因具体情况而异&#xff0c;但以下是一些因素&#xff0c;使得游戏联运在某种程度上变得更加…

J9inceptionv3

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊# 前言 上周学习了inceptionv1网络&#xff0c;这周学习其改进版本inceptionv3 简介 Inception v3是谷歌研究团队提出的深度卷积神经网络架构&#xff0c;通过…

Docker-compose 简单介绍

目录 一 Docker-compose与 Docker Swarm 1&#xff0c;docker-compose 出现的意义 2&#xff0c; Docker Compose 是什么 3&#xff0c;Docker Swarm 是什么 3&#xff0c;Docker Compose Docker Swarm 主要区别 二 Docker-compose 简介 1&#xff0…

鸿蒙开发接口Ability框架:【@ohos.ability.dataUriUtils (DataUriUtils模块)】

DataUriUtils模块 DataUriUtils模块提供用于处理使用DataAbilityHelper方案的对象的实用程序类的能力&#xff0c;包括获取&#xff0c;添加&#xff0c;更新给定uri的路径组件末尾的ID。 说明&#xff1a; 本模块首批接口从API version 7开始支持。后续版本的新增接口&#x…

windows ubuntu sed,awk,grep篇,8,Awk 语法和基础命令

目录 51.Awk 命令语法 52.Awk 程序结构(BEGIN,body,END)区域 53.打印命令 54.模式匹配 Awk 是一个维护和处理文本数据文件的强大语言。在文本数据有一定的格式&#xff0c;即每行数据包 含多个以分界符分隔的字段时&#xff0c;显得尤其有用。即便是输入文件没有一定的格式&a…

在使用ChatGPT之前,你真的知道这些吗?|TodayAI

当OpenAI在2022年11月发布ChatGPT时&#xff0c;它标志着技术领域的一次重大突破。ChatGPT是一个高级AI聊天机器人&#xff0c;它的功能几乎令人难以置信。过去的AI技术多年来一直在逐步发展&#xff0c;早期版本通常只能生成毫无意义的文本或质量较差的图片。这些早期的尝试虽…

安装 AngularJS

安装 AngularJS 文章目录 安装 AngularJS1. 使用在线 cdn2. 使用依赖管理工具 npm 1. 使用在线 cdn <!-- 1. 引入在线地址 --> <script src"http://code.angularjs.org/1.2.25/angular.min.js"></script><!-- 2. 下载到本地&#xff0c;引入文…

集合系列(二十二) -一文到你搞懂二叉树实现

一、介绍 在前面的文章中&#xff0c;我们对树这种数据结构做了一些基本介绍&#xff0c;今天我们继续来聊聊一种非常常用的动态查找树&#xff1a; 二叉查找树。 二叉查找树&#xff0c;英文全称&#xff1a;Binary Search Tree&#xff0c;简称&#xff1a;BST&#xff0c;…

js cookie和它的写入,读取,删除

什么是cookie Cookie 是直接存储在浏览器中的一小串数据&#xff0c;它们是 HTTP 协议的一部分 Cookie 通常是由 Web 服务器使用响应 Set-Cookie HTTP-header 设置的。然后浏览器使用 Cookie HTTP-header 将它们自动添加到&#xff08;几乎&#xff09;每个对相同域的请求中。…

升级价值主张 用友帮企业找到乘风破浪的“密码”

近期&#xff0c;用友发布了其战略级产品用友BIP的全新价值主张&#xff0c;将其从原来的“企业数智化 用友BIP”升级为“用友BIP 成就数智企业”。用友这次价值主张升级看似变动不大&#xff0c;实则大有深意。 顺势而为的主动升级 从当前数智化发展的形势来看&#xff0c;各…

牛客NC320 装箱问题【中等 动态规划,背包问题 C++/Java/Go/PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/d195a735f05b46cf8f210c4ad250681c 几乎完全相同的题目&#xff1a; https://www.lintcode.com/problem/92/description 思路 动态规划都是递归递推而来。php答案是动态规划版本&#xff0c;递归版本有 测试用…

ios CI/CD 持续集成 组件化专题五-(自动发布私有库-组件化搭建)

一&#xff1a;手动发布私有库总结 手动发布pod私有库&#xff0c;需要进行如下几步操作&#xff1a; 1、修改完代码之后&#xff0c;需要提交代码push到git仓库。 2、给代码打tag。 3、修改podspec文件的version值&#xff0c;使其和设置的tag一直。 4、命令行执行pod repo…

【蓝桥杯省赛真题41】python搬运物品方案 中小学青少年组蓝桥杯比赛 算法思维python编程省赛真题解析

目录 python搬运物品方案 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 七、 推荐资料 1、蓝桥杯比赛 2、考级资料 3、其它资料 python搬运物品方案 第十三届蓝桥杯青少年组python省赛比赛 一、题目…