springboot系列--web相关知识探索三

embedded/2024/10/9 11:22:34/

一、前言

web相关知识探索二中研究了请求是如何映射到具体接口(方法)中的,本次文章主要研究请求中所带的参数是如何映射到接口参数中的,也即请求参数如何与接口参数绑定。主要有四种、分别是注解方式、Servlet API方式、复杂参数、以及自定义对象参数。本次主要研究注解方式以及Servlet API方式。

二、 注解方式

接口参数绑定主要涉及的一下注解:

@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@CookieValue、@RequestBody、@MatrixVariable

其中@MatrixVariable矩阵变量注解实际开发中基本不用,跳过研究。主要研究一下springmvc是如何将请求中的参数绑定到下面一个个注解修饰的参数中的。

一、测试用例 

@RestController
public class MvcTestController {/**** @param id* @param name* @param pv 这个map可以接受url上的所有值,*           key是id;value:url路径上的值*           key是name;value:* @param userAgent* @param header* @param age* @param list* @param params* @param _ga* @param cookie* @return*/@GetMapping("/demo/{id}/test/{name}")public Map<String,Object> test(@PathVariable("id") Integer id,@PathVariable("name") String name,@PathVariable Map<String,String> pv,@RequestHeader("User-Agent") String userAgent,@RequestHeader Map<String,String> header,@RequestParam("age") Integer age,@RequestParam("list") List<String> list,@RequestParam Map<String,String> params,@CookieValue("_ga") String _ga,@CookieValue("_ga") Cookie cookie){Map<String,Object> map = new HashMap<>();map.put("id",id);map.put("name",name);map.put("pv",pv);map.put("userAgent",userAgent);map.put("headers",header);map.put("age",age);map.put("list",list);map.put("params",params);map.put("_ga",_ga);return map;}@GetMapping("/test/servlet")public Map testServlet(HttpServletRequest request){Map<String,Object> map = new HashMap<>();map.put("request",request);return map;}@PostMapping("/test/method")public Map testPostMethod(@RequestBody String content){Map<String,Object> map = new HashMap<>();map.put("content",content);return map;}
}

首先,请求进来统一是有前端控制器(中央处理器),也即是DispatcherServlet这个类进行处理。然后中央处理器就会去调用处理器映射器,也即是handlerMapping,handlerMapping通过请求方式以及请求路径找到匹配的handler(也就是我们常说的接口,也是具体的处理方法),之后DispatcherServlet调用处理器适配器,也就是handlerAdatper根据具体的handler规则去执行对应的handler。

二·、原理

get请求:http://localhost:8080/demo/1/test/zhangsan,进入到上一章研究到的,通过handlerMapping获取到具体的handler地方。

1、HandlerMapping中找到能处理请求的Handler(Controller.method())

2、然后为当前Handler 找一个适配器 HandlerAdapter,这个适配器一般就是RequestMappingHandlerAdapter

3、适配器执行目标方法并确定方法参数的每一个值

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {try {ModelAndView mv = null;Exception dispatchException = null;try {processedRequest = this.checkMultipart(request);multipartRequestParsed = processedRequest != request;// 这里就是上一张研究到获取合适的handler的地方mappedHandler = this.getHandler(processedRequest);if (mappedHandler == null) {this.noHandlerFound(processedRequest, response);return;}
// 这里就是获取到合适的处理器适配器的地方,源码在下方HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());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;}this.applyDefaultViewName(processedRequest, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);} catch (Exception var20) {dispatchException = var20;} catch (Throwable var21) {dispatchException = new NestedServletException("Handler dispatch failed", var21);}this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);} catch (Exception var22) {this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);} catch (Throwable var23) {this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));}} finally {if (asyncManager.isConcurrentHandlingStarted()) {if (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}} else if (multipartRequestParsed) {this.cleanupMultipart(processedRequest);}}
}

一、找到合适的处理器适配器 

// 找到合适的HandlerAdapter 
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {// 遍历所有的处理器适配器,直到找到合适的 ,总共会有4种   if (this.handlerAdapters != null) {Iterator var2 = this.handlerAdapters.iterator();while(var2.hasNext()) {HandlerAdapter adapter = (HandlerAdapter)var2.next();// 这里的handler刚好就是HandlerMethod 对象,具体可以查看上一张获取handler。if (adapter.supports(handler)) {return adapter;}}}throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");}// 判断是否支持当前handler,public final boolean supports(Object handler) {return handler instanceof HandlerMethod && this.supportsInternal((HandlerMethod)handler);}

 

0--支持方法上标注@RequestMapping、@GetMpping等注解的处理器适配器

1--支持函数式编程的

 二、利用处理器适配器执行具体的handler

从mv = ha.handle(processedRequest, response, mappedHandler.getHandler());方法进入

 

 一、参数解析器--HandlerMethodArgumentResolver

1、参数解析器主要是确定将要执行的目标方法的每一个参数的值是什么

2、SpringMVC目标方法能写多少种参数类型。取决于参数解析器

例如:目标方法(controller中的方法,也即是具体接口)使用了@RequestParam注解,springmvc就会使用RequestParamMethodArgumentResolvers这个参数解析器进行解析,,其他注解同理,会用其他解析器解析。

// 这是参数解析器接口,具体实现有27种,
public interface HandlerMethodArgumentResolver {// 当前解析器是否支持解析这种参数boolean supportsParameter(MethodParameter var1);// 支持就调用 resolveArgument@NullableObject resolveArgument(MethodParameter var1, @Nullable ModelAndViewContainer var2, NativeWebRequest var3, @Nullable WebDataBinderFactory var4) throws Exception;
}
二、返回值处理器--HandlerMethodReturnValueHandler

返回值处理器主要就是处理返回值类型的,有多少种返回值处理器就能处理多少种返回值类型。这期主要研究参数解析器,返回值处理器另外再研究。

public interface HandlerMethodReturnValueHandler {boolean supportsReturnType(MethodParameter var1);void handleReturnValue(@Nullable Object var1, MethodParameter var2, ModelAndViewContainer var3, NativeWebRequest var4) throws Exception;
}
 三、将参数解析器、返回值处理器包装到执行handler对象中
@Nullableprotected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ServletWebRequest webRequest = new ServletWebRequest(request, response);ModelAndView var15;try {WebDataBinderFactory binderFactory = this.getDataBinderFactory(handlerMethod);ModelFactory modelFactory = this.getModelFactory(handlerMethod, binderFactory);ServletInvocableHandlerMethod invocableMethod = this.createInvocableHandlerMethod(handlerMethod);if (this.argumentResolvers != null) {// invocableMethod这个对象就是执行具体的handler的,把参数解析器包装进去
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);}
// invocableMethod这个对象就是执行具体的handler的,把返回值处理器包装进去if (this.returnValueHandlers != null) {invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);}invocableMethod.setDataBinderFactory(binderFactory);invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);ModelAndViewContainer mavContainer = new ModelAndViewContainer();mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));modelFactory.initModel(webRequest, mavContainer, invocableMethod);mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);asyncWebRequest.setTimeout(this.asyncRequestTimeout);WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);asyncManager.setTaskExecutor(this.taskExecutor);asyncManager.setAsyncWebRequest(asyncWebRequest);asyncManager.registerCallableInterceptors(this.callableInterceptors);asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);Object result;if (asyncManager.hasConcurrentResult()) {result = asyncManager.getConcurrentResult();mavContainer = (ModelAndViewContainer)asyncManager.getConcurrentResultContext()[0];asyncManager.clearConcurrentResult();LogFormatUtils.traceDebug(this.logger, (traceOn) -> {String formatted = LogFormatUtils.formatValue(result, !traceOn);return "Resume with async result [" + formatted + "]";});// 执行具体的handler,也就是controller中的接口方法invocableMethod = invocableMethod.wrapConcurrentResult(result);}invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);if (asyncManager.isConcurrentHandlingStarted()) {result = null;return (ModelAndView)result;}var15 = this.getModelAndView(mavContainer, modelFactory, webRequest);} finally {webRequest.requestCompleted();}return var15;}

四、真正执行目标方法

 真正执行目标方法是在这个类ServletInvocableHandlerMethod里面的invokeAndHandle方法里的       

Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);这个方法

真正执行目标方法有三个重要的步骤:

1、将目标方法中的参数与请求进来的参数进行绑定以及判断是否能否绑定等等。也即是对请求参数进行解析,这里就需要用到参数解析器了

2、通过反射调用目标方法进行执行

3、将目标方法执行后的数据绑定,也就是对返回值进行处理,这里就用到返回值处理器了

    public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {//这里是真正执行目标方法,执行这个方法,就会到controller里面的接口方法,下图可见Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);this.setResponseStatus(webRequest);if (returnValue == null) {if (this.isRequestNotModified(webRequest) || this.getResponseStatus() != null || mavContainer.isRequestHandled()) {this.disableContentCachingIfNecessary(webRequest);mavContainer.setRequestHandled(true);return;}} else if (StringUtils.hasText(this.getResponseStatusReason())) {mavContainer.setRequestHandled(true);return;}mavContainer.setRequestHandled(false);Assert.state(this.returnValueHandlers != null, "No return value handlers");try {this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);} catch (Exception var6) {if (logger.isTraceEnabled()) {logger.trace(this.formatErrorForReturnValue(returnValue), var6);}throw var6;}}

// 执行目标方法源码       
@Nullablepublic Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {// 这里就是将请求参数与目标方法中的参数进行绑定Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);if (logger.isTraceEnabled()) {logger.trace("Arguments: " + Arrays.toString(args));}// 通过反射执行目标方法return this.doInvoke(args);}

一、将请求参数与目标方法上的参数进行绑定

1、判断是否支持解析当前参数,如果有一个不支持就会报错

// 这段代码主要就是确定  controllerr中具体接口方法上的参数的值。也就是把请求参数的值与目标方法参数进行绑定protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {// 这里就是获取controllerr中具体接口方法上的参数、以及对应的类型,参数的位置等等,也就是获取参数声明信息MethodParameter[] parameters = this.getMethodParameters();// 如果目标方法没有参数,也就不需要绑定,直接返回if (ObjectUtils.isEmpty(parameters)) {return EMPTY_ARGS;} else {// 创建一个数组,长度是目标方法参数的个数Object[] args = new Object[parameters.length];// 循环遍历目标方法参数for(int i = 0; i < parameters.length; ++i) {MethodParameter parameter = parameters[i];parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);args[i] = findProvidedArgument(parameter, providedArgs);if (args[i] == null) {// 判断当前解析器是否支持当前接口方法中的这个参数类型if (!this.resolvers.supportsParameter(parameter)) {throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));}try {// 这里将进行参数解析,解析绑定后,将进行下一个参数的解析与绑定args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);} catch (Exception var10) {if (logger.isDebugEnabled()) {String exMsg = var10.getMessage();if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {logger.debug(formatArgumentError(parameter, exMsg));}}throw var10;}}}return args;}}// 判断解析器是否支持当前参数类型的解析,解析器上面有讲过,有27中解析器public boolean supportsParameter(MethodParameter parameter) {return this.getArgumentResolver(parameter) != null;}@Nullableprivate HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {// this.argumentResolverCache刚进来的时候是空的HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);if (result == null) {// 第一次是空的,会进到这里,然后	挨个遍历27个解析器Iterator var3 = this.argumentResolvers.iterator();while(var3.hasNext()) {HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();// 这里判断是否支持,具体源码在下方if (resolver.supportsParameter(parameter)) {result = resolver;// 解析出来后放入本地缓存中,下次进来就不用再判断了this.argumentResolverCache.put(parameter, resolver);break;}}}return result;}// 判断解析器是否支持当前参数解析,这里只拿PathVariable注解解析器源码举例public boolean supportsParameter(MethodParameter parameter) {// 判断当前参数是否使用了PathVariable注解,没使用就不支持解析if (!parameter.hasParameterAnnotation(PathVariable.class)) {return false;// 到了这里就说明当前参数一定是PathVariable注解修饰的,然后判断是不是map,如果不是map那么就能够支持} else if (!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {return true;} else {// 到这里就说明参数是map类型的,然后还需要做其他判断,也就是PathVariable注解中的value一定要有值,如果没有值,就说明这个注解的map想要全部接受url// 上的参数,这种就需要用另外一个解析器进行处理了,如果有值就说明url上的参数是map类型的,名字是PathVariable上的value属性的值。然后会将这个mapp参数值映射到// 这个参数上PathVariable pathVariable = (PathVariable)parameter.getParameterAnnotation(PathVariable.class);return pathVariable != null && StringUtils.hasText(pathVariable.value());}}

2、通过了参数解析判断后,将进行真正的参数解析。 

   // 这里主要是进行参数解析这个方法是在HandlerMethodArgumentResolverComposite这个类下@Nullablepublic Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {// 拿到所有参数解析器        HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);if (resolver == null) {throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first.");} else {// 调用参数解析器,解析参数的方法return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);}}// 这里是拿到这个参数对应的解析器,例如是@PathVariable注解修饰的参数,那就会拿到@PathVariable的参数解析器@Nullableprivate HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {// 这里首先会从缓存中获取,由于之前在做判断是否支持这种类型的参数时,有做过缓存处理,所以这里可以直接获取到这个参数类型对应的参数解析器HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);if (result == null) {Iterator var3 = this.argumentResolvers.iterator();while(var3.hasNext()) {HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();if (resolver.supportsParameter(parameter)) {result = resolver;this.argumentResolverCache.put(parameter, resolver);break;}}}return result;}// 真正开始解析参数,这个方法是在AbstractNamedValueMethodArgumentResolver这个类里面,应该是一个公共方法,类似于模板方法的公共方法,处理公共逻辑@Nullablepublic final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {// 这里将注解信息封装成一个对象,例如注解里标识这个参数以什么样的名字绑定,是否是必须得,以及默认值。例如:@PathVariable(value = "id",required = false)这里面的属性NamedValueInfo namedValueInfo = this.getNamedValueInfo(parameter);MethodParameter nestedParameter = parameter.nestedIfOptional();// 这里就是从封装对象解析出接口方法要绑定的名字,例如这个注解@PathVariable("id"),解析出来就是id这个名字Object resolvedName = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.name);if (resolvedName == null) {throw new IllegalArgumentException("Specified name must not resolve to null: [" + namedValueInfo.name + "]");} else {// 这里开始将url上对应的值与id这个名字进行绑定,也即是对id进行赋值,这里将参数解析绑定后,下面逻辑可以不用看了Object arg = this.resolveName(resolvedName.toString(), nestedParameter, webRequest);if (arg == null) {if (namedValueInfo.defaultValue != null) {arg = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);} else if (namedValueInfo.required && !nestedParameter.isOptional()) {this.handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);}arg = this.handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());} else if ("".equals(arg) && namedValueInfo.defaultValue != null) {arg = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);}if (binderFactory != null) {WebDataBinder binder = binderFactory.createBinder(webRequest, (Object)null, namedValueInfo.name);try {arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);} catch (ConversionNotSupportedException var11) {throw new MethodArgumentConversionNotSupportedException(arg, var11.getRequiredType(), namedValueInfo.name, parameter, var11.getCause());} catch (TypeMismatchException var12) {throw new MethodArgumentTypeMismatchException(arg, var12.getRequiredType(), namedValueInfo.name, parameter, var12.getCause());}if (arg == null && namedValueInfo.defaultValue == null && namedValueInfo.required && !nestedParameter.isOptional()) {this.handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);}}this.handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);return arg;}}// 获取参数注解信息private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {// 先从缓存中获取NamedValueInfo namedValueInfo = (NamedValueInfo)this.namedValueInfoCache.get(parameter);if (namedValueInfo == null) {// 这个方法是从注解中获取注解属性信息namedValueInfo = this.createNamedValueInfo(parameter);// 这里其实就是重新更新一下namedValueInfo值中的defaultValue属性。namedValueInfo = this.updateNamedValueInfo(parameter, namedValueInfo);this.namedValueInfoCache.put(parameter, namedValueInfo);}return namedValueInfo;}// 从注解中获取注解属性信息,拿PathVariable注解举例protected AbstractNamedValueMethodArgumentResolver.NamedValueInfo createNamedValueInfo(MethodParameter parameter) {// 这里获取的是一个注解PathVariable ann = (PathVariable)parameter.getParameterAnnotation(PathVariable.class);Assert.state(ann != null, "No PathVariable annotation");// 这里会把注解传入这个对象,里面会把属性保存到这个对象中return new PathVariableNamedValueInfo(ann);}// 把属性保存到这个对象中private static class PathVariableNamedValueInfo extends AbstractNamedValueMethodArgumentResolver.NamedValueInfo {public PathVariableNamedValueInfo(PathVariable annotation) {// 父类NamedValueInfosuper(annotation.name(), annotation.required(), "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n");}}protected static class NamedValueInfo {private final String name;private final boolean required;@Nullableprivate final String defaultValue;public NamedValueInfo(String name, boolean required, @Nullable String defaultValue) {this.name = name;this.required = required;this.defaultValue = defaultValue;}}// 这里是对id进行赋值的源码,name:就是接口方法中使用注解属性的值,也就是@PathVariable("id")里面的id@Nullableprotected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {/***HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE的值为org.springframework.web.servlet.HandlerMapping.uriTemplateVariables*request请求信息里面在org.springframework.web.servlet.HandlerMapping.uriTemplateVariables这个key下,保存了url上的请求参数*从请求中获取到数据后,保存在map当中,其中key为:注解属性的id,value为请求携带的值,就拿@PathVariable("id"),这个注解来说吧,key-id,value-请求请来的值。(@GetMapping("/demo/{id}/test/{name}")是从这里面取到的id、name为key)这里是由于请求一进来会有一个UrlPathHelper类里面的方法将url里面的路径变量全部解析出来*然后提前保存到请求域当中。所以uriTemplateVars是请求域中的值,但是并未绑定到接口参数上,*/Map<String, String> uriTemplateVars = (Map)request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, 0);// 这里就是从请求域中的值里面获取到id对应的value        return uriTemplateVars != null ? uriTemplateVars.get(name) : null;}

 

三、Servlet API方式 

当我们的接口参数是WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId这些类型时,springmvc会有专门的处理器进行处理。也就是ServletRequestMethodArgumentResolver 这个处理器。主要源码如下。

@Overridepublic boolean supportsParameter(MethodParameter parameter) {Class<?> paramType = parameter.getParameterType();return (WebRequest.class.isAssignableFrom(paramType) ||ServletRequest.class.isAssignableFrom(paramType) ||MultipartRequest.class.isAssignableFrom(paramType) ||HttpSession.class.isAssignableFrom(paramType) ||(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||Principal.class.isAssignableFrom(paramType) ||InputStream.class.isAssignableFrom(paramType) ||Reader.class.isAssignableFrom(paramType) ||HttpMethod.class == paramType ||Locale.class == paramType ||TimeZone.class == paramType ||ZoneId.class == paramType);}

一、判断是否支持HttpservletRequest类型参数

 

 二、解析出参数


http://www.ppmy.cn/embedded/123204.html

相关文章

有开源的杀毒软件吗?

有开源的杀毒软件&#xff0c;其中较为知名和广泛使用的是ClamAV&#xff08;Clam AntiVirus&#xff09;。以下是对ClamAV的详细介绍&#xff1a; 一、软件概述 ClamAV是一款免费的、跨平台的开源防病毒软件工具包&#xff0c;用C和C编写&#xff0c;并在GNU通用公共许可证下…

使用Electron将vue项目改桌面程序

1&#xff0c;一个简单的实现案例 # 切换镜像&#xff0c;其他镜像&#xff1a;https://registry.npm.taobao.org/ npm config set registry https://registry.npmmirror.com/ # 推荐使用yarn来管理依赖包&#xff0c;相对于Node.js自带的npm包管理工具来说&#xff0c;它具有…

正则表达式的使用示例--Everything文件检索批量重命名工具

一、引言 Everything是一款非常实用的文件搜索工具&#xff0c;它可以帮助您快速定位并查找计算机中的文件和文件夹。Everything搜索文件资料之神速&#xff0c;有使用过的朋友们都深有体会&#xff0c;相对于Windows自带的搜索功能&#xff0c;使用Everything&#xff0c;可以…

YOLOv11改进 | Neck篇 | YOLOv11引入Gold-YOLO

1. Gold-YOLO介绍 1.1 摘要: 在过去的几年中,YOLO 系列模型已成为实时目标检测领域的领先方法。 许多研究通过修改架构、增加数据和设计新的损失,将基线提升到更高的水平。 然而,我们发现以前的模型仍然存在信息融合问题,尽管特征金字塔网络(FPN)和路径聚合网络(PANet)…

【python面试宝典3】遍历文件夹

题目013&#xff1a;写一个函数统计传入的列表中每个数字出现的次数并返回对应的字典。 点评&#xff1a;送人头的题目&#xff0c;不解释。 def count_letters(items):result {}for item in items:if isinstance(item, (int, float)):result[item] result.get(item, 0) 1re…

C++(string类的实现)

1. 迭代器、返回capacity、返回size、判空、c_str、重载[]和clear的实现 string类的迭代器的功能就类似于一个指针&#xff0c;所以我们可以直接使用一个指针来实现迭代器&#xff0c;但如下图可见迭代器有两个&#xff0c;一个是指向的内容可以被修改&#xff0c;另一个则是指…

新手小白在做副业时要注意哪些细节?

在当今社会&#xff0c;越来越多的人开始尝试通过副业来增加收入、拓展技能或实现自我价值。对于新手小白来说&#xff0c;进入副业领域可能充满了挑战和不确定性。然而&#xff0c;只要注意一些关键细节&#xff0c;就可以提高副业成功的几率&#xff0c;避免不必要的风险。 一…

Redis 实现分布式锁时需要考虑的问题

引言 分布式系统中的多个节点经常需要对共享资源进行并发访问&#xff0c;若没有有效的协调机制&#xff0c;可能会导致数据竞争、资源冲突等问题。分布式锁应运而生&#xff0c;它是一种保证在分布式环境中多个节点可以安全地访问共享资源的机制。而在Redis中&#xff0c;使用…