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

SpringMVC 6+源码分析(六)参数处理

一、概述

在上一篇文章中讲到整个执行流程的时候,其中一个流程就是对参数经行处理。这一块的业务由RequestMappingHandlerAdapter这个HandlerAdapter负责,接下来将详细的解读源码,看看SpringMVC对于不同参数类型是怎么做处理的。

二、参数类型

在实际的业务服务中,前后端交互约定传参方式各不相同,主要取决于业务类型、技术选型等因数。总结下来有以下接种方式:

2.1 基于注解的传参。

这是业务开发中常用的一种方式,及在Controller 层的方法入参中使用 @RequestParam、@PathVariable、@RequestBody等注解。

		@GetMapping("/param")public Publisher<String> param(@RequestParam String name) {return Flux.just("Hello ", name, "!");}@RequestMapping("{hotel}")void handleHotel(@PathVariable String hotel, Writer writer) throws IOException {writer.write("test-" + hotel);}@PostMapping("/person")Person transformPerson(@RequestBody Person person) {return new Person(person.getName().toUpperCase());}

2.2 从HttpServletRequest 对象中获取

在不作任何配置时,Controller 层的方法入参中使用HttpServletRequest 入参时,可以利用 HttpServletRequest提供的方法获取参数。

	/*** url :http://localhost:9090/system/person/personById?id=123* @param request* @return*/@GetMapping("/personById")public String getPersonById(HttpServletRequest request) {String id=request.getParameter("id");return "person id is: " + id;}

2.3 自定义参数解析器

SpringMVC提供了自定义参数解析器接口,只需要实现HandlerMethodArgumentResolver接口,当RequestMappingHandlerAdapter实例化的时候框架就会将其加载到customArgumentResolvers中。

2.4 url中获取

最后,在找不到上述几个方式的解析器之后,则会使用解析url参数的方式进行兜底。其url参数格式为 ?key=value 的形式,同时 key 要和controller 入参的参数名一致。

	/*** url :http://localhost:9090/system/person/personByname?name=123* @param request* @return*/@GetMapping("/param")public Publisher<String> param(String name) {return Flux.just("Hello ", name, "!");}

三、参数的解析过程

3.1 解析器的初始化

在讲解RequestMappingHandlerAdapter初始化的时候,在afterPropertiesSet()方法中会将系统默认和自定义的参数解析实例化并注册到argumentResolvers中。

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);// Annotation-based argument resolutionresolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));resolvers.add(new RequestParamMapMethodArgumentResolver());resolvers.add(new PathVariableMethodArgumentResolver());resolvers.add(new PathVariableMapMethodArgumentResolver());resolvers.add(new MatrixVariableMethodArgumentResolver());resolvers.add(new MatrixVariableMapMethodArgumentResolver());resolvers.add(new ServletModelAttributeMethodProcessor(false));resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));resolvers.add(new RequestHeaderMapMethodArgumentResolver());resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));resolvers.add(new SessionAttributeMethodArgumentResolver());resolvers.add(new RequestAttributeMethodArgumentResolver());// Type-based argument resolutionresolvers.add(new ServletRequestMethodArgumentResolver());resolvers.add(new ServletResponseMethodArgumentResolver());resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));resolvers.add(new RedirectAttributesMethodArgumentResolver());resolvers.add(new ModelMethodProcessor());resolvers.add(new MapMethodProcessor());resolvers.add(new ErrorsMethodArgumentResolver());resolvers.add(new SessionStatusMethodArgumentResolver());resolvers.add(new UriComponentsBuilderMethodArgumentResolver());if (KotlinDetector.isKotlinPresent()) {resolvers.add(new ContinuationHandlerMethodArgumentResolver());}// Custom argumentsif (getCustomArgumentResolvers() != null) {resolvers.addAll(getCustomArgumentResolvers());}@Nullablepublic List<HandlerMethodArgumentResolver> getCustomArgumentResolvers() {return this.customArgumentResolvers;}

3.2 执行流程

回顾完初始化流程,接下来看到执行流程中参数的解析是在哪一步做的。当请求来到HandlerAdapter之后,就会选择对应类型的处理,主要是RequestMappingHandlerAdapter,而RequestMappingHandlerAdapter的执行在
invokeHandlerMethod()方法中,在创建好ServletInvocableHandlerMethod之后,将参数解析器设置到对应的ServletInvocableHandlerMethod对象中去。

	@Nullableprotected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {//省略代码ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);if (this.argumentResolvers != null) {invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);}if (this.returnValueHandlers != null) {invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);}//省略代码return getModelAndView(mavContainer, modelFactory, webRequest);}

再看到方法的执行过程,在ServletInvocableHandlerMethod.invokeAndHandle()方法中调用 invokeForRequest(),在其父类InvocableHandlerMethod.getMethodArgumentValues()方法中完成参数解析与数据绑定。

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);//省略代码}@Nullablepublic Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);if (logger.isTraceEnabled()) {logger.trace("Arguments: " + Arrays.toString(args));}return doInvoke(args);}

在getMethodArgumentValues()方法中,首先将遍历取出每一个参数,判断参数类型,然后选择对应的参数解析器,最后对参数进行解析。

	protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {MethodParameter[] parameters = getMethodParameters();if (ObjectUtils.isEmpty(parameters)) {return EMPTY_ARGS;}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) {continue;}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 ex) {// Leave stack trace for later, exception may actually be resolved and handled...if (logger.isDebugEnabled()) {String exMsg = ex.getMessage();if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {logger.debug(formatArgumentError(parameter, exMsg));}}throw ex;}}return args;}

3.3 不同参数解析器的解析过程

所有的请求都由HandlerMethodArgumentResolverComposite统一处理,该对象里面保存着所有类型的参数解析器,它的职责就是找个对应的类型的解析器进行具体的业务规则解析。HandlerMethodArgumentResolverComposite利用门面模式,抽象化整个业务流程的操作。

3.3.1 RequestResponseBodyMethodProcessor

用于解析带有RequestBody 注解的入参,其大致流程为:判断是否带 RequestBody注解、解析参数、根据请求的type(如:json、xml 等) 解析参数,最后将body转化为java对象。

	@Nullableprivate HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);if (result == null) {for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {if (resolver.supportsParameter(parameter)) {result = resolver;this.argumentResolverCache.put(parameter, result);break;}}}return result;}/***判断类型**/@Overridepublic boolean supportsParameter(MethodParameter parameter) {return parameter.hasParameterAnnotation(RequestBody.class);}@Nullablepublic Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {// 获取对应的 参数解析器HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);if (resolver == null) {throw new IllegalArgumentException("Unsupported parameter type [" +parameter.getParameterType().getName() + "]. supportsParameter should be called first.");}//参数解析return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);}

参数的解析在resolver.resolveArgument()方法中,该方法的具体实现在 RequestResponseBodyMethodProcessor中。

	@Overridepublic Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {parameter = parameter.nestedIfOptional();//读取参数Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());// 省略代码return adaptArgumentIfNecessary(arg, parameter);}@Overrideprotected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {//省略代码Object arg = readWithMessageConverters(inputMessage, parameter, paramType);if (arg == null && checkRequired(parameter)) {throw new HttpMessageNotReadableException("Required request body is missing: " +parameter.getExecutable().toGenericString(), inputMessage);}return arg;}

接下来的 readWithMessageConverters()操作都是在父类 AbstractMessageConverterMethodArgumentResolver 中进行。
根据下面的 代码我们可以看到,首先获取请求中的 contentType(post请求为:Content-Type=application/json),然后遍历this.messageConverters,找到支持该类型的 messageConverter
(MappingJackson2HttpMessageConverter ),最后调用canRead()方法进行逻辑处理,该方法在其父类AbstractJackson2HttpMessageConverter中。

protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {Class<?> contextClass = parameter.getContainingClass();Class<T> targetClass = (targetType instanceof Class clazz ? clazz : null);if (targetClass == null) {ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);targetClass = (Class<T>) resolvableType.resolve();}MediaType contentType;boolean noContentType = false;try {contentType = inputMessage.getHeaders().getContentType();}catch (InvalidMediaTypeException ex) {throw new HttpMediaTypeNotSupportedException(ex.getMessage(), getSupportedMediaTypes(targetClass != null ? targetClass : Object.class));}if (contentType == null) {noContentType = true;contentType = MediaType.APPLICATION_OCTET_STREAM;}HttpMethod httpMethod = (inputMessage instanceof HttpRequest httpRequest ? httpRequest.getMethod() : null);Object body = NO_VALUE;EmptyBodyCheckingHttpInputMessage message = null;try {message = new EmptyBodyCheckingHttpInputMessage(inputMessage);for (HttpMessageConverter<?> converter : this.messageConverters) {Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();GenericHttpMessageConverter<?> genericConverter =(converter instanceof GenericHttpMessageConverter ghmc ? ghmc : null);if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :(targetClass != null && converter.canRead(targetClass, contentType))) {if (message.hasBody()) {HttpInputMessage msgToUse =getAdvice().beforeBodyRead(message, parameter, targetType, converterType);body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);}else {body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);}break;}}}catch (IOException ex) {throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);}finally {if (message != null && message.hasBody()) {closeStreamIfNecessary(message.getBody());}}if (body == NO_VALUE) {if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||(noContentType && !message.hasBody())) {return null;}throw new HttpMediaTypeNotSupportedException(contentType,getSupportedMediaTypes(targetClass != null ? targetClass : Object.class), httpMethod);}MediaType selectedContentType = contentType;Object theBody = body;LogFormatUtils.traceDebug(logger, traceOn -> {String formatted = LogFormatUtils.formatValue(theBody, !traceOn);return "Read \"" + selectedContentType + "\" to [" + formatted + "]";});return body;}

canRead()方法判断完其转化后,将进行上面requestBodyAdvice 的前置拓展处理,处理完之后 调用 read()方法进行类型转换,获取到对应的java对象后,再请求requestBodyAdvice的后置拓展处理。

	public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {if (!canRead(mediaType)) {return false;}JavaType javaType = getJavaType(type, contextClass);ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), mediaType);if (objectMapper == null) {return false;}AtomicReference<Throwable> causeRef = new AtomicReference<>();if (objectMapper.canDeserialize(javaType, causeRef)) {return true;}logWarningIfNecessary(javaType, causeRef.get());return false;}@Overridepublic Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)throws IOException, HttpMessageNotReadableException {JavaType javaType = getJavaType(type, contextClass);return readJavaType(javaType, inputMessage);}

最后的 具体逻辑再readJavaType() 方法中完成。直至,参数的解析就已经结束了。

private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {MediaType contentType = inputMessage.getHeaders().getContentType();Charset charset = getCharset(contentType);ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), contentType);Assert.state(objectMapper != null, () -> "No ObjectMapper for " + javaType);boolean isUnicode = ENCODINGS.containsKey(charset.name()) ||"UTF-16".equals(charset.name()) ||"UTF-32".equals(charset.name());try {InputStream inputStream = StreamUtils.nonClosing(inputMessage.getBody());if (inputMessage instanceof MappingJacksonInputMessage mappingJacksonInputMessage) {Class<?> deserializationView = mappingJacksonInputMessage.getDeserializationView();if (deserializationView != null) {ObjectReader objectReader = objectMapper.readerWithView(deserializationView).forType(javaType);objectReader = customizeReader(objectReader, javaType);if (isUnicode) {return objectReader.readValue(inputStream);}else {Reader reader = new InputStreamReader(inputStream, charset);return objectReader.readValue(reader);}}}ObjectReader objectReader = objectMapper.reader().forType(javaType);objectReader = customizeReader(objectReader, javaType);if (isUnicode) {return objectReader.readValue(inputStream);}else {Reader reader = new InputStreamReader(inputStream, charset);return objectReader.readValue(reader);}}catch (InvalidDefinitionException ex) {throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);}catch (JsonProcessingException ex) {throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);}}

3.3.2 RequestParamMethodArgumentResolver

RequestParamMethodArgumentResolver不仅仅是用于解析带有RequestParam注解的参数,还是作为最后兜底的处理,即将 url的参数映射到对应的方法入参中,即便其没有带任何注解。一下是判断方法的代码,可以看到其先判断是否存在注解,然后判断是否为多媒体,最后进入兜底方法this.useDefaultResolution 在初始化的时候被赋予了true,其处理的是java的一些基础类型,如String,int 之类的。
请添加图片描述

	@Overridepublic boolean supportsParameter(MethodParameter parameter) {if (parameter.hasParameterAnnotation(RequestParam.class)) {if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);return (requestParam != null && StringUtils.hasText(requestParam.name()));}else {return true;}}else {if (parameter.hasParameterAnnotation(RequestPart.class)) {return false;}parameter = parameter.nestedIfOptional();if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {return true;}else if (this.useDefaultResolution) {return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());}else {return false;}}}public static boolean isSimpleValueType(Class<?> type) {return (Void.class != type && void.class != type &&(ClassUtils.isPrimitiveOrWrapper(type) ||Enum.class.isAssignableFrom(type) ||CharSequence.class.isAssignableFrom(type) ||Number.class.isAssignableFrom(type) ||Date.class.isAssignableFrom(type) ||Temporal.class.isAssignableFrom(type) ||URI.class == type ||URL.class == type ||Locale.class == type ||Class.class == type));}

resolveArgument()方法先创建NamedValueInfo对象,分为带RequestParam注解、不带任何注解及文件类型的对象。RequestParam注解会获取 name属性,默认为 “”。然后根据 name从request 获取value,最后调用handleResolvedValue()方法,最后放回。

	@Nullablepublic final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);MethodParameter nestedParameter = parameter.nestedIfOptional();Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);if (resolvedName == null) {throw new IllegalArgumentException("Specified name must not resolve to null: [" + namedValueInfo.name + "]");}//根据 name从request 获取value。Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);if (arg == null) {if (namedValueInfo.defaultValue != null) {arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);}else if (namedValueInfo.required && !nestedParameter.isOptional()) {handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);}arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());}else if ("".equals(arg) && namedValueInfo.defaultValue != null) {arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);}if (binderFactory != null) {WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);try {arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);}catch (ConversionNotSupportedException ex) {throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),namedValueInfo.name, parameter, ex.getCause());}catch (TypeMismatchException ex) {throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),namedValueInfo.name, parameter, ex.getCause());}// Check for null value after conversion of incoming argument valueif (arg == null && namedValueInfo.defaultValue == null &&namedValueInfo.required && !nestedParameter.isOptional()) {handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest);}}handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);return arg;}

3.3.3 PathVariableMethodArgumentResolver

PathVariableMethodArgumentResolver 用于处理带有 PathVariable注解的方法,其作用是将URL中的占位符转换为入参,其中name()和value()属性互为别名。
可以看到在构建完对应的NamedValueInfo之后,直接使用 resolveName从reuqest 获取对应的值。

	@Overridepublic boolean supportsParameter(MethodParameter parameter) {if (!parameter.hasParameterAnnotation(PathVariable.class)) {return false;}if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class);return (pathVariable != null && StringUtils.hasText(pathVariable.value()));}return true;}@Overrideprotected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);Assert.state(ann != null, "No PathVariable annotation");return new PathVariableNamedValueInfo(ann);}@Override@SuppressWarnings("unchecked")@Nullableprotected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);return (uriTemplateVars != null ? uriTemplateVars.get(name) : null);}

3.3.4 ServletRequestMethodArgumentResolver

最后这是ServletRequestMethodArgumentResolver,它的作用是将 ServletRequest赋值到对应的方法入参中去。下面是其支持的基础数据类型:

	@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.class.isAssignableFrom(paramType) ||(Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations()) ||InputStream.class.isAssignableFrom(paramType) ||Reader.class.isAssignableFrom(paramType) ||HttpMethod.class == paramType ||Locale.class == paramType ||TimeZone.class == paramType ||ZoneId.class == paramType);}

解析参数的逻辑就是对类型判断,根据不同的参数类型从webRequest取对象。

@Overridepublic Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {Class<?> paramType = parameter.getParameterType();// WebRequest / NativeWebRequest / ServletWebRequestif (WebRequest.class.isAssignableFrom(paramType)) {if (!paramType.isInstance(webRequest)) {throw new IllegalStateException("Current request is not of type [" + paramType.getName() + "]: " + webRequest);}return webRequest;}// ServletRequest / HttpServletRequest / MultipartRequest / MultipartHttpServletRequestif (ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType)) {return resolveNativeRequest(webRequest, paramType);}// HttpServletRequest required for all further argument typesreturn resolveArgument(paramType, resolveNativeRequest(webRequest, HttpServletRequest.class));}

四、总结

本文介绍了最主要的几个参数解析的流程,主要是由HandlerMethodArgumentResolverComposite作为门面根据不同的类型选择对应的处理方式。

http://www.lryc.cn/news/619774.html

相关文章:

  • 基于R语言的现代贝叶斯统计学方法(贝叶斯参数估计、贝叶斯回归、贝叶斯计算实践过程
  • Datawhale AI夏令营第三期多模态RAG方向 Task3
  • 算法详细讲解 - 离散化/区间合并
  • 【慕伏白】Kali 系统下安装 docker
  • 弹性扩展新范式:分布式LLM计算的FastMCP解决方案
  • Python(二):MacBook安装 Python并运行第一个 Python 程序
  • 【QT】QT实现鼠标左右滑动切换图片
  • MySQL中的缓存机制
  • 如何在VS里使用MySQL提供的mysql Connector/C++的debug版本
  • 如何把ubuntu 22.04下安装的mysql 8 的 数据目录迁移到另一个磁盘目录
  • 设计模式笔记_行为型_策略模式
  • OpenJDK 17 源码 安全点轮询的信号处理流程
  • 资源查看-lspci命令
  • 如何准备一场技术演讲
  • 各种排序算法(二)
  • 磁悬浮轴承转子设计避坑指南:深度解析核心要点与高可靠性策略
  • 基于js和html的点名应用
  • 【电气】NPN与PNP
  • B系列树详细讲解
  • 16-docker的容器监控方案-prometheus实战篇
  • Python 类元编程(导入时和运行时比较)
  • Windows也能用!Claude Code硬核指南
  • [激光原理与应用-259]:理论 - 几何光学 - 平面镜的反射、平面透镜的折射、平面镜的反射成像、平面透镜的成像的规律
  • 网刻软件iVentoy软件使用方法
  • @进程管理工具 - Glances工具详细指南
  • Django REST Framework视图
  • Java 大视界 -- Java 大数据机器学习模型在金融资产配置优化与风险收益平衡中的应用(395)
  • 解惑rust中的 Send/Sync(译)
  • 基于Java的Markdown转Word工具(标题、段落、表格、Echarts图等)
  • 18.10 SQuAD数据集实战:5步高效获取与预处理,BERT微调避坑指南