当前位置: 首页 > news >正文

Spring MVC注解Controller源码流程解析--定位HandlerMethod

Spring MVC注解Controller源码流程解析--定位HandlerMethod

  • 引言
  • 定位HandlerMethod
    • RequestMappingInfoHandlerMapping提供的getHandlerInternal实现
    • AbstractHandlerMethodMapping提供的getHandlerInternal实现
    • 根据请求路径去映射集合中寻找HandlerMethod
      • 精确匹配到一个结果
        • 对模板变量和矩阵变量的抽取
      • 最佳匹配
      • 匹配失败
  • 小结


引言

Spring MVC注解Controller源码流程解析–映射建立

上一篇中,我们对映射建立的过程做了详细的分析,既然映射关系已经建立完毕了,那么下面就是当请求来临时,如何通过请求去映射集合中寻找出对应的HandlerMethod,然后再交给RequestMappingHandlerAdapter完成请求最终处理。

如果是通过请求路径去映射集合中通过精确匹配进行查询的话,其实实现起来就很简单了,但是因为要加入@RequestMapping中相关请求限制,包括通配符匹配和占位符匹配等等内容,会让寻找HandlerMethod的过程变的不那么简单,但是也没有那么复杂,下面我们就来看看。


定位HandlerMethod

	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		        ....
		        //检查是否是文件上传请求,如果是的话,就返回封装后的MultipartHttpServletRequest
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				//通过当前请求定位到具体处理的handler--这里是handlerMethod
				mappedHandler = getHandler(processedRequest);
				....

我们本节的重点就在getHandler方法中:

	@Nullable
	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;
	}

getHandler方法中会遍历所有可用的HandlerMapping,然后尝试通过当前请求解析得到一个handler,如果不为空,说明找到了,否则借助下一个HandlerMapping继续寻找。

前面已经说过了,注解Controller的映射建立是通过RequestMappingHandlerMapping完成的,那么寻找映射当然也需要通过RequestMappingHandlerMapping完成,因此我们这里只关注RequestMappingHandlerMapping的getHandler流程链即可。


getHandler方法主要是由AbstractHandlerMapping顶层抽象基类提供了一个模板方法实现,具体根据request寻找handler的逻辑实现,是通过getHandlerInternal抽象方法交给子类实现的。

	public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	    //这里调用到的是RequestMappingInfoHandlerMapping子类提供的实现
		Object handler = getHandlerInternal(request);
		//如果没找到,尝试寻找兜底的默认handler
		if (handler == null) {
			handler = getDefaultHandler();
		}
		//如果还是兜底也不管用,就返回null
		if (handler == null) {
			return null;
		}
		//如果此时的handler拿到的还只是一个字符串名字,那么需要先去容器得到对应的实体对象
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = obtainApplicationContext().getBean(handlerName);
		}

		 ...
        
        //构建拦截器链
		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

		...
		//跨域处理
		if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
			...
		}

		return executionChain;
	}

RequestMappingInfoHandlerMapping提供的getHandlerInternal实现

  • RequestMappingInfoHandlerMapping主要作为RequestMappingInfo,Request和HandlerMethod三者之间沟通的桥梁,RequestMappingInfo提供请求匹配条件,判断当前Request是否应该交给当前HandlerMethod处理
	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
		request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
		try {
		    //调用父类AbstractHandlerMethodMapping的方法
			return super.getHandlerInternal(request);
		}
		finally {
		     //把下面这个方法进行内联后,等价于 : request.removeAttribute(MEDIA_TYPES_ATTRIBUTE);
			ProducesRequestCondition.clearMediaTypesAttribute(request);
		}
	}

清除Request相关属性,主要是因为Request对象会被复用,因此使用前,需要清空上一次的数据,这也算是对象复用增加的代码复杂性吧。


AbstractHandlerMethodMapping提供的getHandlerInternal实现

RequestMappingInfoHandlerMapping重写了父类的getHandlerInternal方法,但只是对Request对象复用进行了相关数据清除工作,核心还是在AbstractHandlerMethodMapping提供的getHandlerInternal实现中。

	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
	    //initLookupPath默认是返回Context-path后面的路径
	    //eg1: 没有设置context-path,请求路径为localhost:5200/volunteer/back/admin/pass/login,那这里返回的就是/volunteer/back/admin/pass/login
	    //eg2: 上面的例子中设置了context-path为/volunteer,那这里返回的就是/back/admin/pass/login
		String lookupPath = initLookupPath(request);
		//获取读锁
		this.mappingRegistry.acquireReadLock();
		try {
		   //通过请求路径去映射集合中寻找对应的handlerMethod
			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		}
		finally {
			this.mappingRegistry.releaseReadLock();
		}
	}

initLookupPath方法中默认会返回的请求路径为剥离掉context-path后的路径,并且后续拦截器中进行路径匹配时,匹配的也是剥离掉context-path后的路径,这一点切记!


根据请求路径去映射集合中寻找HandlerMethod

lookupHandlerMethod是本文的核心关注点,该方法会通过Request定位到对应的HandlerMethod后返回。

具体处理过程,又可以分为三种情况:

  • 精确匹配到一个结果
  • 需要进行最佳匹配
  • 没有匹配到任何结果

因为这部分逻辑比较复杂,因此我们对三种情况分开讨论。


精确匹配到一个结果

	@Nullable
	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
		List<Match> matches = new ArrayList<>();
		//先通过请求路径去pathLookup集合中尝试进行精准匹配--这里的T指的是RequestMappingInfo
		List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
		//精准匹配到了结果
		if (directPathMatches != null) {
			//将结果添加进matches集合中--还会经过RequstMappingInfo的条件校验环节
			addMatchingMappings(directPathMatches, matches, request);
		}
		//如果上面精确匹配没有匹配到结果----
		if (matches.isEmpty()) {
		     //将register的keySet集合保存的所有RequestMappingInfo都加入matches集合中去
		     //然后依次遍历每个RequstMappingInfo,通过其自身提供的getMatchingCondition对当前requst请求进行条件匹配
		     //如果不满足条件,是不会加入到当前matches集合中去的
			addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
		}
		
		if (!matches.isEmpty()) {
		    //获取matches集合中第一个元素
			Match bestMatch = matches.get(0);
			//如果matches集合元素大于0,说明需要进一步进行模糊搜索
			if (matches.size() > 1) {
				...
			}
			//在request对象的属性集合中设置处理当前请求的HandlerMethod
			request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
			//处理当前最佳匹配
			handleMatch(bestMatch.mapping, lookupPath, request);
			return bestMatch.getHandlerMethod();
		}
		else {
		     //没有匹配结果
			return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
		}
	}

addMatchingMappings将得到的匹配结果RequestMappingInfo加入matches集合,但这个过程中还需要进行一些特殊处理,例如:

    @PostMapping({PASS+"login",PASS+"log"})

此时PostMapping会映射到两种请求路径上,此时这里需要做的就是,搞清楚到底是哪一个路径匹配上了当前请求,然后修改RequestMappingInfo对应的patterns集合,将多余的请求路径去除掉。

还有就是一个请求路径可能会映射到多个RequestMappingInfo上,例如:
在这里插入图片描述
请求路径相同,只是请求方法不同。

	private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
	    //遍历每个RequestMappinginfo
		for (T mapping : mappings) {
		     //判断当前RequestMappingInfo是否能够真正映射到当前请求上
			T match = getMatchingMapping(mapping, request);
			//如果返回值不为空,表示可以映射,否则跳过处理下一个
			if (match != null) {
				matches.add(new Match(match, this.mappingRegistry.getRegistrations().get(mapping)));
			}
		}
	}

getMatchingMapping的判断还是通过RequestMappingInfo自身提供的条件进行进行匹配的:

	@Override
	protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
		return info.getMatchingCondition(request);
	}

检查当前RequestMappingInfo 中的所有条件是否与提供的请求匹配,并返回一个新的RequestMappingInfo,其中包含针对当前请求量身定制的条件。

例如,返回的实例可能包含与当前请求匹配的 URL 模式的子集,并以最佳匹配模式在顶部进行排序。

	@Override
	@Nullable
	public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
	    //检查@RequestMapping注解提供的method请求方式是否与当前请求匹配,如果不匹配返回null
		RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
		if (methods == null) {
			return null;
		}
		//判断设置的请求参数匹配条件是否匹配
		ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
		if (params == null) {
			return null;
		}
		//请求头条件
		HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
		if (headers == null) {
			return null;
		}
		//Consume条件检查
		ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
		if (consumes == null) {
			return null;
		}
		//Produce条件检查
		ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
		if (produces == null) {
			return null;
		}
		//PathPatternsRequestCondition一般为null
		PathPatternsRequestCondition pathPatterns = null;
		if (this.pathPatternsCondition != null) {
			pathPatterns = this.pathPatternsCondition.getMatchingCondition(request);
			if (pathPatterns == null) {
				return null;
			}
		}
		//@PostMapping({PASS+"login",PASS+"log"})的情况处理 
	    //RequestMappingInfo中的patterns数组中如果存在多个请求路径,需要判断当前请求是具体映射到了那个路径上
	    //然后重新构造一个patternsCondition后返回,该patternsCondition内部包含的只有匹配当前请求路径的那个pattern
		PatternsRequestCondition patterns = null;
		if (this.patternsCondition != null) {
			patterns = this.patternsCondition.getMatchingCondition(request);
			if (patterns == null) {
				return null;
			}
		}
		//自定义请求限制
		RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
		if (custom == null) {
			return null;
		}
		//上面这些条件其中一个不通过,那么返回的结果就为null
		//最后构造一个全新的RequestMappingInfo返回,该RequestMappingInfo中包含的都是匹配上当前请求路径的信息,排除了其他非匹配上的信息
		return new RequestMappingInfo(this.name, pathPatterns, patterns,
				methods, params, headers, consumes, produces, custom, this.options);
	}

如果不清楚@ReuqestMapping注解中各个属性的作用,那么把上面每个条件判断过程看一遍就明白了。


handleMatch法主要是针对模糊匹配出来的结果进行相关处理,例如: URI template variables,matrix variables和producible media types处理等等…

上面这些名词关联的注解有: @PathVariable , @MatrixVariable ,producible media types对应的是@RequestMapping中produces设置。

	@Override
	protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) {
		super.handleMatch(info, lookupPath, request);
        //一般返回的就是@RequestMapping注解中的patterns属性,注意@RequestMapping注解可以映射到多个URL上
        //这里返回的就是patterns属性对应的patternsCondition请求匹配条件对象
		RequestCondition<?> condition = info.getActivePatternsCondition();
		//condition默认实现为patternsCondition,因此这里直接走else分支
		if (condition instanceof PathPatternsRequestCondition) {
			extractMatchDetails((PathPatternsRequestCondition) condition, lookupPath, request);
		}
		else {
		    //抽取匹配细节,该方法内部会完成对上面这些模板变量,矩阵变量的处理
			extractMatchDetails((PatternsRequestCondition) condition, lookupPath, request);
		}
        //如果我们设置了@RequestMapping注解中的produces属性,那么这里会进行处理
		if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
			Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
			//设置到request对象的属性集合中,不用想,肯定会在响应的时候用到
			request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
		}
	}

父类AbstractHandlerMethodMapping中的handleMatch方法,主要是将lookup设置到当前请求对象的属性集合中去:

	protected void handleMatch(T mapping, String lookupPath, HttpServletRequest request) {
		request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath);
	}

对模板变量和矩阵变量的抽取

	private void extractMatchDetails(
	       //传入的patternsCondition主要作用在于其内部的patterns属性集合,该集合封装了@RequestMapping注解的patterns属性值内容
			PatternsRequestCondition condition, String lookupPath, HttpServletRequest request) {

		String bestPattern;
		Map<String, String> uriVariables;
		//如果patterns属性集合为空--说明我们直接标注了一个@RequestMapping注解,但是没有指定任何属性限制
		if (condition.isEmptyPathMapping()) {
		    //那就不存在什么模糊匹配了,bestPattern 就是当前请求路径
			bestPattern = lookupPath;
			//模板变量和矩阵变量当然也就不存在了,直接一个空集合
			uriVariables = Collections.emptyMap();
		}
		//我们需要考虑是否存在相关模板变量或者矩阵变量
		else {
		    //patterns集合中第一个属性为最佳匹配--这个在addMatchingMappings中被处理完成,不清楚回头看一下
			bestPattern = condition.getPatterns().iterator().next();
			//解析模板变量,eg: "/hotels/{hotel}" and path "/hotels/1" --> 返回的map就是"hotel"->"1"
			uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
			//关于矩阵变量的处理---这里不展开,感兴趣自己debug看一下源码
			if (!getUrlPathHelper().shouldRemoveSemicolonContent()) {
				request.setAttribute(MATRIX_VARIABLES_ATTRIBUTE, extractMatrixVariables(request, uriVariables));
			}
			uriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables);
		}
		//设置最佳匹配路径和URL模板变量集合到request对象的属性集合中去
		request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
		request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables);
	}

关于模板变量和矩阵变量的解析细节这里不多展开了,感兴趣可以按照当前思路自行debug源码。


最佳匹配

	@Nullable
	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
		List<Match> matches = new ArrayList<>();
		List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
		if (directPathMatches != null) {
			addMatchingMappings(directPathMatches, matches, request);
		}
		if (matches.isEmpty()) {
			addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
		}
		if (!matches.isEmpty()) {
			Match bestMatch = matches.get(0);
			//如果能够处理当前请求的RequestMappingInfo存在多个,下面就需要进行最佳匹配
			if (matches.size() > 1) {
				Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
				matches.sort(comparator);
				bestMatch = matches.get(0);
				if (logger.isTraceEnabled()) {
					logger.trace(matches.size() + " matching mappings: " + matches);
				}
				if (CorsUtils.isPreFlightRequest(request)) {
					for (Match match : matches) {
						if (match.hasCorsConfig()) {
							return PREFLIGHT_AMBIGUOUS_MATCH;
						}
					}
				}
				else {
					Match secondBestMatch = matches.get(1);
					if (comparator.compare(bestMatch, secondBestMatch) == 0) {
						Method m1 = bestMatch.getHandlerMethod().getMethod();
						Method m2 = secondBestMatch.getHandlerMethod().getMethod();
						String uri = request.getRequestURI();
						throw new IllegalStateException(
								"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
					}
				}
			}
			request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
			handleMatch(bestMatch.mapping, lookupPath, request);
			return bestMatch.getHandlerMethod();
		}
		else {
			return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
		}
	}

是如何完成最佳匹配的,这个过程本文不展开论述,感兴趣自行研究。


匹配失败

如果没有寻找当前一个RequestMappingInfo能够处理当前request,那么进入handleNoMatch阶段。

handleNoMatch会再次迭代所有 RequestMappingInfo,至少通过 URL 查看是否有任何匹配,并根据不匹配的内容引发异常。

说人话就是找出不匹配的原因,然后抛出对应的异常,告诉用户。

	@Override
	protected HandlerMethod handleNoMatch(
			Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException {
        //借助PartialMatchHelper来分析那些部分匹配的请求,是因为什么原因而无法匹配成功的
        //部分匹配的意思就是请求路径匹配上了,但是因为其他条件匹配失败了,例如: 请求头限制等
		PartialMatchHelper helper = new PartialMatchHelper(infos, request);
		//如果返回的集合为空,表示连请求路径匹配上的都没有---不存在部分匹配现象
		if (helper.isEmpty()) {
			return null;
		}
        //请求方式没匹配上  
		if (helper.hasMethodsMismatch()) {
			Set<String> methods = helper.getAllowedMethods();
			if (HttpMethod.OPTIONS.matches(request.getMethod())) {
				Set<MediaType> mediaTypes = helper.getConsumablePatchMediaTypes();
				HttpOptionsHandler handler = new HttpOptionsHandler(methods, mediaTypes);
				return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
			}
			throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods);
		}
       //consume条件不满足
		if (helper.hasConsumesMismatch()) {
			Set<MediaType> mediaTypes = helper.getConsumableMediaTypes();
			MediaType contentType = null;
			if (StringUtils.hasLength(request.getContentType())) {
				try {
					contentType = MediaType.parseMediaType(request.getContentType());
				}
				catch (InvalidMediaTypeException ex) {
					throw new HttpMediaTypeNotSupportedException(ex.getMessage());
				}
			}
			throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes));
		}
        //produces条件不满足
		if (helper.hasProducesMismatch()) {
			Set<MediaType> mediaTypes = helper.getProducibleMediaTypes();
			throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(mediaTypes));
		}
        //请求参数条件不满足
		if (helper.hasParamsMismatch()) {
			List<String[]> conditions = helper.getParamConditions();
			throw new UnsatisfiedServletRequestParameterException(conditions, request.getParameterMap());
		}

		return null;
	}

PartialMatchHelper会首先获取到所有请求路径匹配成功的RequestMappingInfo:

		public PartialMatchHelper(Set<RequestMappingInfo> infos, HttpServletRequest request) {
			for (RequestMappingInfo info : infos) {
				if (info.getActivePatternsCondition().getMatchingCondition(request) != null) {
					this.partialMatches.add(new PartialMatch(info, request));
				}
			}
		}

PartialMatch的构造方法会判断当前RequestMappingInfo不匹配的原因是什么:

			public PartialMatch(RequestMappingInfo info, HttpServletRequest request) {
				this.info = info;
				this.methodsMatch = (info.getMethodsCondition().getMatchingCondition(request) != null);
				this.consumesMatch = (info.getConsumesCondition().getMatchingCondition(request) != null);
				this.producesMatch = (info.getProducesCondition().getMatchingCondition(request) != null);
				this.paramsMatch = (info.getParamsCondition().getMatchingCondition(request) != null);
			}

相关has*Mismatch就是遍历partialMatches集合,然后挨个判断是否存在对应的不匹配原因:

		public boolean hasMethodsMismatch() {
			for (PartialMatch match : this.partialMatches) {
				if (match.hasMethodsMatch()) {
					return false;
				}
			}
			return true;
		}

小结

到此为止,根据request请求去HandlerMethod注册中心寻找对应HandlerMethod的过程就分析完毕了,下一节,会对handlerMethod的调用过程进行分析。

相关文章:

  • 常用排序方法、sort的实现原理、快排的优化
  • centos7 离线安装httpd
  • Redis学习之路(三)--key键操作
  • 为什么这么多品牌迫切想要改变Logo?
  • 郁锦香、凯里亚德亮相“2022锦江行”,如何走出一条酒店破题之路
  • 拓展:Microsoft密钥类型说明
  • 基本 nosql 和 mongodb等数据库对比基本 nosql 和 mongodb等数据库对比
  • 使用打表法找规律
  • dockerkubernets篇(二十八)
  • 32. 最长有效括号 java解决
  • startActivityForResult废弃了,用Activity Result API吧
  • Go 学习笔记(87) — 函数式选项,初始化结构体对象可变参数
  • Android开发学习——2.Android开发环境准备
  • HNU工训中心STC-B学习板大作业-基于OLED模块的多功能MP4
  • Python爬虫之Js逆向案例(10)-爬虫数据批量写入mysql数据库
  • $translatePartialLoader加载失败及解决方式
  • “寒冬”下的金三银四跳槽季来了,帮你客观分析一下局面
  • 《Javascript高级程序设计 (第三版)》第五章 引用类型
  • js ES6 求数组的交集,并集,还有差集
  • Redux 中间件分析
  • webgl (原生)基础入门指南【一】
  • 初识MongoDB分片
  • 对超线程几个不同角度的解释
  • 更好理解的面向对象的Javascript 1 —— 动态类型和多态
  • 可能是历史上最全的CC0版权可以免费商用的图片网站
  • 全栈开发——Linux
  • 如何正确配置 Ubuntu 14.04 服务器?
  • 学习笔记DL002:AI、机器学习、表示学习、深度学习,第一次大衰退
  • 如何通过报表单元格右键控制报表跳转到不同链接地址 ...
  • #LLM入门|Prompt#2.3_对查询任务进行分类|意图分析_Classification
  • (done) NLP “bag-of-words“ 方法 (带有二元分类和多元分类两个例子)词袋模型、BoW
  • (附源码)spring boot公选课在线选课系统 毕业设计 142011
  • (转)树状数组
  • (自适应手机端)响应式新闻博客知识类pbootcms网站模板 自媒体运营博客网站源码下载
  • .NET Core实战项目之CMS 第一章 入门篇-开篇及总体规划
  • .net MVC中使用angularJs刷新页面数据列表
  • .NET/C# 使用反射调用含 ref 或 out 参数的方法
  • [Assignment] C++1
  • [C++]类和对象(中)
  • [CLR via C#]11. 事件
  • [codevs] 1029 遍历问题
  • [flume$2]记录一个写自定义Flume拦截器遇到的错误
  • [IE技巧] 如何关闭Windows Server版IE的安全限制
  • [javaSE] GUI(事件监听机制)
  • [Jenkins] Docker 安装Jenkins及迁移流程
  • [Linux]进程信号(信号入门 | 信号产生的方式 | 信号捕捉初识)
  • [Linux]使用CentOS镜像与rpm来安装虚拟机软件
  • [MRCTF2020]Ez_bypass1
  • [OPEN SQL] 修改数据
  • [OpenCV学习笔记]获取鼠标处图像的坐标和像素值
  • [PAT练级笔记] 34 Basic Level 1034 有理数四则运算
  • [python] dataclass 快速创建数据类
  • [pytorch] 2. tensorboard
  • [SSD综述1.8] 固态存储市场发展分析与预测_固态存储技术发展方向(2022to2023)
  • [Windows][Linux]字体相关