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

《学会 SpringMVC 系列 · 剖析篇(上)》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍

文章目录

    • 写在前面的话
      • 学前准备
    • 请求流程分析
      • Step1、DispatcherServlet#doDispatch
      • Step2、DispatcherServlet#getHandler
      • Step3、DispatcherServlet#getHandlerAdapter
      • Step4、DispatcherServlet#handle
      • Step5、InvocableHandlerMethod#getMethodArgumentValues
      • Step6、RequestResponseBodyMethodProcessor#readWithMessageConverters
      • Step7、RequestResponseBodyMethodProcessor#readWithMessageConverters
    • 请求分析复盘
      • 整个流程梳理(精简版)
      • doDispatch(部分源码)
    • 总结陈词

CSDN.gif

写在前面的话

通过上一篇博文《学会 SpringMVC 系列 · 基础篇》的学习,可以掌握 SpringMVC 的项目搭建和部分用法,从搭建过程中我们看到,SpringMVC 的入口是在 web.xml 中添加 DispatcherServlet,它是一个Servlet,那请求流程也遵循 Servlet 相关规范展开。
接下来,让我们进一步分析相关源码,顺带引出相关扩展点和实战运用。


学前准备

1、SpringMVC 源码分析分为初始化流程和请求流程两部分,本篇先重点介绍后者。可以把 SpringMVC的请求流程,比作一个请求(Servlet)的完整生命周期,那就是包括“请求前 - 实际请求 - 请求后”,以此思路展开。
2、本篇 SpringMVC 源码分析系列文章,将以 《搭建拥有数据交互的 SpringBoot 》的 SpringBoot3.x 为基础,学习相关源码,对应 SpringMVC 版本为 6.1.11,不过核心流程上,基本大同小异。
3、先添加一个入参和返回值都是String的简单接口(如下),麻雀虽小,但也覆盖到入参解析逻辑了,启动Boot项目,Postman测试一下,就可以开始学习了。

@ResponseBody
@RequestMapping("/studyJson")
public ZyTeacherInfo studyJson(@RequestBody ZyTeacherInfo teacherInfo) {teacherInfo.setTeaName("战神");return teacherInfo;
}

image.png


请求流程分析

Tips:先梳理一下本方法的完整链路,在此基础上扩展自定义逻辑试试。

Step1、DispatcherServlet#doDispatch

前面提到 DispatcherServlet 是 SpringMVC 的入口,不管是否 SpringBoot,从下面这张图很明显可以看出 DispatcherServlet 和 Servlet 的父子关系。
image.png
有过 JavaWeb 开发经验的人应该了解,Servlet 的请求入口方法是 service 方法,访问接口后,一步步往后跟,当断点停留在 DispatcherServlet#doDispatch 方法的时候,可以从IDEA的调试器,观察到请求顺序的堆栈信息。
这边先不展开细节,后续在按专栏展开,总之是在 DispatcherServlet、FrameworkServlet、HttpServlet 等类之间反复横跳,最后到了 doDispatch,这里面才是请求的核心逻辑。
image.png

Step2、DispatcherServlet#getHandler

关键代码:HandlerExecutionChain mappedHandler = getHandler(processedRequest);
逻辑说明:

  • 目的是获取 HandlerExecutionChain 执行链对象,除了包含 HandlerMethod 方法对象外,还包含拦截器信息;
  • 如下图所示,有多个 HandlerMapping 对象,当然最终这里里面找到了 RequestMappingHandlerMapping;

image.png
目前大部分开发都使用 @RequestMapping,示例方法也是如此,因此看名字就知道走的是 RequestMappingHandlerMapping,当然,从RequestMappingHandlerMapping#isHandler 的代码也可以看出来,只要类包含Controller注解,就可以满足。

Tips:SpringMVC的5.x,也允许只存在 RequestMapping 注解的情况,6.x 只剩下Controller了。

protected boolean isHandler(Class<?> beanType) {return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
}

这里回到断点,如下,获取到的HandlerExecutionChain对象,如下,指向具体业务接口,并且包含三个内置拦截器。
image.png
到此,DispatcherServlet#getHandler 流程结束。

补充一下,RequestMappingHandlerMapping 会在初始化的时候会将url/controller的映射存到handlerMethods 变量中,url/mapping的映射存在urlMap变量中,如下所示,目的是方便快速查找。

private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>();
private final MultiValueMap<String, T> urlMap = new LinkedMultiValueMap<String, T>();

温馨提示:很多代码善可继续深挖,但建议分析源码还是要把握主线,不然深陷其中,无法自拔。

Step3、DispatcherServlet#getHandlerAdapter

关键代码:HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
逻辑说明:

  • 目的是获取 HandlerAdapter 适配器对象,逻辑有点类似前面找Mapping,如下图所示,最终从若干里面找到合适的 RequestMappingHandlerAdapter;

image.png

  • 从代码也可以看出,是根据 supports 方法判断 adapter 是否支持这个 handler,很明显这个要求很简单,RequestMappingHandlerAdapter 是肯定满足的;
// 存在父类 AbstractHandlerMethodAdapter#supports
public final boolean supports(Object handler) {//判断handler是否属于HandlerMethod  并且 supportsInternal 为truereturn (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}
  • getHandlerAdapter 也可以添加如下注释信息,看起来和前面获取 HandlerExecutionChain 差不多;
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {//遍历所有handlerAdapters 选择合适的handlerAdaptersfor (HandlerAdapter ha : this.handlerAdapters) {if (logger.isTraceEnabled()) {logger.trace("Testing handler adapter [" + ha + "]");}//判断这个adapter是否支持这个handlerif (ha.supports(handler)) {return ha;}}throw new ServletException("No adapter for handler [" + handler +"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

Step4、DispatcherServlet#handle

直接上一下示例代码,如下:

if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;
}// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());mappedHandler.applyPostHandle(processedRequest, response, mv);

功能说明:
1、此时已经得到 HandlerAdapter,先执行所有拦截器的 applyPreHandle 方法;
2、接着调用其 handle 方法,再执行拦截器的 applyPostHandle 方法;
3、这里的 handle 是主要逻辑,由于 RequestMappingHandlerAdapter 没有 handle 方法,所以进入父类的 handle,再经过一系列方法,最终进入InvocableHandlerMethod#invokeForRequest;

//请求链路:
//AbstractHandlerMethodAdapter#handle
//RequestMappingHandlerAdapter#invokeHandleMethod
//ServletInvocableHandlerMethod#invokeAndHandle
//InvocableHandlerMethod#invokeForRequest
public final Object invokeForRequest(NativeWebRequest request, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {// 得到参数Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);if (logger.isTraceEnabled()) {StringBuilder sb = new StringBuilder("Invoking [");sb.append(getBeanType().getSimpleName()).append(".");sb.append(getMethod().getName()).append("] method with arguments ");sb.append(Arrays.asList(args));logger.trace(sb.toString());}//此处执行反射调用controller的方法Object returnValue = doInvoke(args);if (logger.isTraceEnabled()) {logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]");}return returnValue;
}

Step5、InvocableHandlerMethod#getMethodArgumentValues

getMethodArgumentValues 这步通俗一点来说,就是解析参数,得到最后的参数列表。
部分代码如下所示:

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {Object[] args = new Object[parameters.length];for (int i = 0; i < parameters.length; i++) {// 判断是否支持解析这个参数,如果支持会把参数解析器加入到缓存中if (!this.resolvers.supportsParameter(parameter)) {throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));}try {解析请求参数args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);}}return args;
}

其中,最重要的方法应该是 resolveArgument,其部分代码如下,逻辑也是把所有参数解析器拿出来溜溜,看看哪个解析器的 supportsParameter 方法满足。

public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}public 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;
}

不出意外,满足的是 RequestResponseBodyMethodProcessor,运行截图如下,从27个参数解析器中选中了这个。
image.png
流程重新梳理一下:

  1. 遍历所有参数列表,依次利用 supportsParameter 方法,判断是否有解析器是否满足;
  2. 再进行下面的一系列判定逻辑,最终可以找到 RequestResponseBodyMethodProcessor 这一解析器是符合的,紧接着把参数解析器放到 argumentResolverCache 缓存中;
@Override
public boolean supportsParameter(MethodParameter parameter) {return parameter.hasParameterAnnotation(RequestBody.class);
}@Override
public boolean supportsReturnType(MethodParameter returnType) {return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||returnType.hasMethodAnnotation(ResponseBody.class));
}
  1. 最终调用RequestResponseBodyMethodProcessor 的 resolveArgument 进行参数解析;

Tips:这里其实又是一个适配器的套路(适配器模式),Spring为我们提供了多种场景的支持。

【@RequestParam 注解】
如果接口的参数使用 @RequestParam 注解,那么这里满足的是 RequestParamMethodArgumentResolver ,运行截图如下,当然它也是第一顺位。
值得一提,如果没找到合适的参数处理器,那么最后还是会用 RequestParamMethodArgumentResolver 兜底,此时后话,暂且不提。
image.png

Step6、RequestResponseBodyMethodProcessor#readWithMessageConverters

如果使用 RequestBody 注解的,resolver一般为 RequestResponseBodyMethodProcessor,从下面它的父类可以看出来,它既是一个参数解析器,也是一个返回值处理器,对应两个注解的处理。

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
}public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolverimplements HandlerMethodReturnValueHandler {
}
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam,Type paramType) throws IOException, HttpMediaTypeNotSupportedException {HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);HttpInputMessage inputMessage = new ServletServerHttpRequest(servletRequest);// 得到注解RequestBody ann = methodParam.getParameterAnnotation(RequestBody.class);// 一般是trueif (!ann.required()) {InputStream inputStream = inputMessage.getBody();if (inputStream == null) {return null;}else if (inputStream.markSupported()) {inputStream.mark(1);if (inputStream.read() == -1) {return null;}inputStream.reset();}else {final PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);int b = pushbackInputStream.read();if (b == -1) {return null;}else {pushbackInputStream.unread(b);}inputMessage = new ServletServerHttpRequest(servletRequest) {@Overridepublic InputStream getBody() {// Form POST should not get herereturn pushbackInputStream;}};}}//一般会走到这里return super.readWithMessageConverters(inputMessage, methodParam, paramType);
}

继续走到AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters,代码如下:

protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage,MethodParameter methodParam, Type targetType) throws IOException, HttpMediaTypeNotSupportedException {
... 省略...for (HttpMessageConverter<?> converter : this.messageConverters) {if (converter instanceof GenericHttpMessageConverter) {GenericHttpMessageConverter genericConverter = (GenericHttpMessageConverter) converter;if (genericConverter.canRead(targetType, contextClass, contentType)) {if (logger.isDebugEnabled()) {logger.debug("Reading [" + targetType + "] as \"" +contentType + "\" using [" + converter + "]");}//json数据转为对象return genericConverter.read(targetType, contextClass, inputMessage);}} ...省略...throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
}

这里的messageConverters变量的值就是我们前面注册的,MappingJackson2HttpMessageConverter,如下图所示。
image.png

//最后调用MappingJackson2HttpMessageConverter#read
//再走到下面这个方法
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) {try {// 是Jackson的API,请求的InputStream流转为对象// 如果json数据是空的,此处会抛出 Could not read JSON: No content to map due to end-of-inputreturn this.objectMapper.readValue(inputMessage.getBody(), javaType);}catch (IOException ex) {throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);}
}

Step7、RequestResponseBodyMethodProcessor#readWithMessageConverters

上面取到了实体对象了,如下图所示:
image.png
接着往前追溯:
RequestResponseBodyMethodProcessor#resolveArgument
InvocableHandlerMethod#getMethodArgumentValues
InvocableHandlerMethod#invokeForRequest
InvocableHandlerMethod#doInvoke
开始真实调用逻辑了,传递了解析后的参数了,再下来就是真实的控制层请求。
请求完毕之后,又回到DispatcherServlet#doDispatch,再来就是返回值的处理了。


请求分析复盘

整个流程梳理(精简版)

Tips:上述分析流程精简一下,可以得出如下结论。

1、总入口 DispatcherServlet,最底层其实是 HttpServlet#service
2、根据请求URL,找到处理方法Method,DispatcherServlet#getHandler
3、参数处理,HandlerMethodArgumentResolver,RequestResponseBodyMethodProcessor
4、执行原方法逻辑,invoke
5、返回值处理,HandlerMethodReturnValueHandler,RequestResponseBodyMethodProcessor

【补充:上述流程分析】
1、HandlerMapping 阶段,匹配到一个HandlerMapping,通过Url找到某个controller的某个方法。返回HandlerExecutionChain 对象;
2、根据HandlerMethod匹配到某个HandlerAdapter,也就是我们的RequestMappingHandlerAdapter;
3、this.argumentResolvers.supportsParameter(parameter)匹配参数处理器,这里会匹配到RequestResponseBodyMethodProcessor;
4、从messageConverters集合中匹配出参数转换器,这里是MappingJackson2HttpMessageConverter。调用Jackson API转换成对象;
5、得到参数后,利用反射执行某个controller中某个方法;
从这里我们可以看出,Spring框架是相当灵活的,适配器模式是被其发挥得淋漓尽致。支持我们自定义HandlerMapping、HandlerAdapter、messageConverters等等,以适应更多的应用场景。

Tips:虽然可以注册多个 argumentResolver 和 messageConverters,但最终只会选择一个合适的执行。

【补充:@RequestBody 介绍】
@RequestBody 注解是 Spring MVC 中的一个注解,用于指示控制器方法参数应该绑定到 HTTP 请求的主体(body)部分。当客户端向服务器发送 POST 或 PUT 请求时,请求的数据通常包含在请求主体中,而不是在 URL 参数中。@RequestBody 注解告诉 Spring MVC 框架将请求主体中的数据反序列化为指定的 Java 对象,并将其作为方法的参数传递给控制器方法。
具体来说,@RequestBody 注解的作用包括以下几个方面:
反序列化请求主体: 当客户端发送一个包含 JSON、XML 等格式的数据主体的 POST 或 PUT 请求时,Spring MVC 框架将请求主体中的数据反序列化为指定的 Java 对象。这个过程通常使用 Jackson、JAXB 等库来完成,将请求主体中的数据转换为相应的 Java 对象。
绑定到方法参数: 反序列化后的 Java 对象将作为 @RequestBody 注解标注的方法参数的值传递给控制器方法。通过使用 @RequestBody 注解,你可以直接将请求主体中的数据映射到方法参数,而不必手动解析请求主体或处理输入流。
处理多种数据格式: @RequestBody 注解不仅可以处理 JSON 格式的数据,还可以处理 XML、表单数据等多种格式的数据。Spring MVC 框架会根据请求的 Content-Type 头部来确定请求主体的数据格式,并使用相应的消息转换器(MessageConverter)来进行反序列化。
综上所述,@RequestBody 注解在 Spring MVC 中的作用是将请求主体中的数据反序列化为指定的 Java 对象,并绑定到控制器方法的参数上,使得控制器方法能够直接处理请求主体中的数据。

【补充:关于 HandlerMapping 和 HandlerAdapter 的初始化】

protected void initStrategies(ApplicationContext context) {...省略...//初始化HandlerMappingsinitHandlerMappings(context);//初始化HandlerAdaptersinitHandlerAdapters(context);...省略...
}

HandlerMapping和HandlerAdapter是可以多个的,Spring默认会注册几个HandlerMapping(如BeanNameUrlHandlerMapping、SimpleUrlHandlerMapping),有请求来的时候,去匹配到合适的那个。
在Spring MVC时代,我们通常会显式去注册一个HandlerMapping和HandlerAdapter,分别是RequestMappingHandlerMapping和RequestMappingHandlerAdapter。
当然要使用MappingJackson2HttpMessageConverter转换器还需要jackson-databind、jackson-core、jackson-mapper-lgpl、jackson-mapper-asl、jackson-core-lgpl、jackson-core-asl这些Jar包。
HandlerAdapter 和 HandlerMapping 也是类似的逻辑,一样是支持多个,默认注册HttpRequestHandlerAdapter、SimpleControllerHandlerAdapter。

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);// Determine handler for the current request.//找出处理这个请求的handler链mappedHandler = getHandler(processedRequest, false);if (mappedHandler == null || mappedHandler.getHandler() == null) {noHandlerFound(processedRequest, response);return;}// Determine handler adapter for the current request.//根据handler得到adapterHandlerAdapter 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)) {//RequestMappingHandlerAdapter#getLastModified返回-1long lastModified = ha.getLastModified(request, mappedHandler.getHandler());if (logger.isDebugEnabled()) {logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);}// 检测是否未改变 并且 是get请求if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {return;}}//拦截器if (!mappedHandler.applyPreHandle(processedRequest, response)) { //不为true则returnreturn;}//如果是注解方式使用的是RequestMappingHandlerAdapter然后到父类AbstractHandlerMethodAdapter#handle方法// Actually invoke the handler.mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}//mv不为null并且view不存在则应用默认的viewNameapplyDefaultViewName(request, mv);//主要执行拦截器postHandle方法mappedHandler.applyPostHandle(processedRequest, response, mv);}catch (Exception ex) {dispatchException = ex;}//处理处理程序选择和处理程序调用的结果,即要解析为ModelAndView或异常。processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);}catch (Exception ex) {//主要是发生异常时执行拦截器afterCompletion方法triggerAfterCompletion(processedRequest, response, mappedHandler, ex);}catch (Error err) {triggerAfterCompletionWithError(processedRequest, response, mappedHandler, 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);}}}
}

总结陈词

此篇文章介绍了SpringMVC 请求流程的源码分析,仅供参考。
无论是 handlerAdapters 还是 argumentResolvers,或者其他,基本几个用法都类似。

  1. 预先加载List
  2. 遍历List判断是否满足
  3. 适配器思想
  4. 缓存思想
  5. 。。。

篇幅所限,主要介绍了一个具体请求的流程,后续系列文章会针对其中重要步骤继续展开介绍,同时添加实战说明。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

CSDN_END.gif

相关文章:

《学会 SpringMVC 系列 · 剖析篇(上)》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…...

【Vulnhub系列】Vulnhub_SecureCode1靶场渗透(原创)

【Vulnhub系列靶场】Vulnhub_SecureCode1靶场渗透 原文转载已经过授权 原文链接&#xff1a;Lusen的小窝 - 学无止尽&#xff0c;不进则退 (lusensec.github.io) 一、环境配置 1、从百度网盘下载对应靶机的.ova镜像 2、在VM中选择【打开】该.ova 3、选择存储路径&#xff0…...

【C语言】结构体详解 -《探索C语言的 “小宇宙” 》

目录 C语言结构体&#xff08;struct&#xff09;详解结构体概览表1. 结构体的基本概念1.1 结构体定义1.2 结构体变量声明 2. 结构体成员的访问2.1 使用点运算符&#xff08;.&#xff09;访问成员输出 2.2 使用箭头运算符&#xff08;->&#xff09;访问成员输出 3. 结构体…...

基于DTW距离的KNN算法实现股票高相似筛选案例

使用DTW算法简单实现曲线的相似度计算-CSDN博客 前文中股票高相关k线筛选问题的延伸。基于github上的代码迁移应用到股票高相关预测上。 这里给出一个相关完整的代码实现案例。 1、数据准备 假设你已经有了一些历史股票的k线数据。如果数据能打标哪些股票趋势是上涨的、下跌…...

GD32 - IIC程序编写

一、初始化 理论知识链接&#xff1a; IIC理论知识 二、代码实现 1、SDA和SCL设置成开漏输出模式 开漏输出的作用&#xff1a; 因为IIC总线是一种双向的通信协议&#xff0c;需要使用开漏输出实现共享总线。开漏输出类似于一种线与的方式&#xff0c;即无论总线上哪个设备…...

将项目部署到docker容器上

通过docker部署前后端项目 前置条件 需要在docker中拉去jdk镜像、nginx镜像 docker pull openjdk:17 #拉取openjdk17镜像 docker pull nginx #拉取nginx镜像部署后端 1.打包后端项目 点击maven插件下面的Lifecycle的package 对后端项目进行打包 等待打包完成即可 2.将打…...

免费【2024】springboot宠物美容机构CRM系统设计与实现

博主介绍&#xff1a;✌CSDN新星计划导师、Java领域优质创作者、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流✌ 技术范围&#xff1a;SpringBoot、Vue、SSM、HTML、Jsp、PHP、Nodejs、Python、爬虫、数据可视化…...

搞懂数据结构与Java实现

文章链接&#xff1a;搞懂数据结构与Java实现 (qq.com) 代码链接&#xff1a; Java实现数组模拟循环队列代码 (qq.com) Java实现数组模拟栈代码 (qq.com) Java实现链表代码 (qq.com) Java实现哈希表代码 (qq.com) Java实现二叉树代码 (qq.com) Java实现图代码 (qq.com)...

Stable Diffusion 图生图

区别于文生图&#xff0c;所谓的图生图&#xff0c;俗称的垫图&#xff0c;就是比文生图多了一张参考图&#xff0c;由参考一张图来生成图片&#xff0c;影响这个图片的要素不仅只靠提示词了&#xff0c;还有这个垫图的因素&#xff0c;这个区域就上上传垫图的地方&#xff0c;…...

语言转文字

因为工作原因需要将语音转化为文字&#xff0c;经常搜索终于找到一个免费的好用工具&#xff0c;记录下使用方法 安装Whisper 搜索Colaboratory 右上方链接服务 执行 !pip install githttps://github.com/openai/whisper.git !sudo apt update && sudo apt install f…...

ref函数

Vue2 中的ref 首先我们回顾一下 Vue2 中的 ref。 ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用&#xff0c;引用指向的就是 DOM 元素&#xff1b;如果用在子组件上&#xff0c;引用就指向组件实例&#xff1…...

7/30 bom和dom

文档对象mox 浏览器对象模型...

【Golang 面试 - 进阶题】每日 3 题(五)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/UWz06 &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏…...

MySQL,GROUP BY子句的作用是什么?having和where的区别在哪里说一下jdbc的流程

GROUP BY 子句的作用是什么 GROUP BY 字段名 将数据按字段值相同的划为一组&#xff0c;经常配合聚合函数一起使用。 having和where的区别在哪里 where是第一次检索数据时候添加过滤条件&#xff0c;确定结果集。而having是在分组之后添加结果集&#xff0c;用于分组之后的过…...

1._专题1_双指针_C++

双指针 常见的双指针有两种形式&#xff0c;一种是对撞指针&#xff0c;一种是左右指针。对撞指针&#xff1a;一般用于顺序结构中&#xff0c;也称左右指针。 对撞指针从两端向中间移动。一个指针从最左端开始&#xff0c;另一个从最右端开始&#xff0c;然后逐渐往中间逼近…...

Spring集成ES

RestAPI ES官方提供的java语言客户端用以组装DSL语句,再通过http请求发送给ES RestClient初始化 引入依赖 <dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId> </d…...

力扣高频SQL 50题(基础版)第二十六题

文章目录 力扣高频SQL 50题&#xff08;基础版&#xff09;第二十六题1667.修复表中的名字题目说明实现过程准备数据实现方式结果截图总结 力扣高频SQL 50题&#xff08;基础版&#xff09;第二十六题 1667.修复表中的名字 题目说明 表&#xff1a; Users ----------------…...

WIFI 接收机和发射机同步问题+CFO/SFO频率偏移问题

Synchronization Between Sender and Receiver & CFO Correction 解决同步问题和频率偏移问题是下面论文的关键&#xff0c;接下来结合论文进行详细解读 解读论文&#xff1a;Verification and Redesign of OFDM Backscatter 论文pdf&#xff1a;https://www.usenix.org/s…...

ubuntu安装并配置flameshot截图软件

参考&#xff1a;flameshot key-bindins 安装 sudo apt install flameshot自定义快捷键 Settings->Keyboard->View and Customize Shortcuts->Custom Shortcuts&#xff0c;输入该快捷键名称&#xff08;自定义&#xff09;&#xff0c;然后输入command&#xff08;…...

【Linux】CentOS更换国内阿里云yum源(超详细)

目录 1. 前言2. 打开终端3. 确保虚拟机已经联网4. 备份现有yum配置文件5. 下载阿里云yum源6. 清理缓存7. 重新生成缓存8. 测试安装gcc 1. 前言 有些同学在安装完CentOS操作系统后&#xff0c;在系统内安装比如&#xff1a;gcc等软件的时候出现这种情况&#xff1a;&#xff08…...

KubeSphere 容器平台高可用:环境搭建与可视化操作指南

Linux_k8s篇 欢迎来到Linux的世界&#xff0c;看笔记好好学多敲多打&#xff0c;每个人都是大神&#xff01; 题目&#xff1a;KubeSphere 容器平台高可用&#xff1a;环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...

Admin.Net中的消息通信SignalR解释

定义集线器接口 IOnlineUserHub public interface IOnlineUserHub {/// 在线用户列表Task OnlineUserList(OnlineUserList context);/// 强制下线Task ForceOffline(object context);/// 发布站内消息Task PublicNotice(SysNotice context);/// 接收消息Task ReceiveMessage(…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地

借阿里云中企出海大会的东风&#xff0c;以**「云启出海&#xff0c;智联未来&#xff5c;打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办&#xff0c;现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

基于数字孪生的水厂可视化平台建设:架构与实践

分享大纲&#xff1a; 1、数字孪生水厂可视化平台建设背景 2、数字孪生水厂可视化平台建设架构 3、数字孪生水厂可视化平台建设成效 近几年&#xff0c;数字孪生水厂的建设开展的如火如荼。作为提升水厂管理效率、优化资源的调度手段&#xff0c;基于数字孪生的水厂可视化平台的…...

Robots.txt 文件

什么是robots.txt&#xff1f; robots.txt 是一个位于网站根目录下的文本文件&#xff08;如&#xff1a;https://example.com/robots.txt&#xff09;&#xff0c;它用于指导网络爬虫&#xff08;如搜索引擎的蜘蛛程序&#xff09;如何抓取该网站的内容。这个文件遵循 Robots…...

深度学习习题2

1.如果增加神经网络的宽度&#xff0c;精确度会增加到一个特定阈值后&#xff0c;便开始降低。造成这一现象的可能原因是什么&#xff1f; A、即使增加卷积核的数量&#xff0c;只有少部分的核会被用作预测 B、当卷积核数量增加时&#xff0c;神经网络的预测能力会降低 C、当卷…...

Pinocchio 库详解及其在足式机器人上的应用

Pinocchio 库详解及其在足式机器人上的应用 Pinocchio (Pinocchio is not only a nose) 是一个开源的 C 库&#xff0c;专门用于快速计算机器人模型的正向运动学、逆向运动学、雅可比矩阵、动力学和动力学导数。它主要关注效率和准确性&#xff0c;并提供了一个通用的框架&…...

JAVA后端开发——多租户

数据隔离是多租户系统中的核心概念&#xff0c;确保一个租户&#xff08;在这个系统中可能是一个公司或一个独立的客户&#xff09;的数据对其他租户是不可见的。在 RuoYi 框架&#xff08;您当前项目所使用的基础框架&#xff09;中&#xff0c;这通常是通过在数据表中增加一个…...

C#学习第29天:表达式树(Expression Trees)

目录 什么是表达式树&#xff1f; 核心概念 1.表达式树的构建 2. 表达式树与Lambda表达式 3.解析和访问表达式树 4.动态条件查询 表达式树的优势 1.动态构建查询 2.LINQ 提供程序支持&#xff1a; 3.性能优化 4.元数据处理 5.代码转换和重写 适用场景 代码复杂性…...

Qt 事件处理中 return 的深入解析

Qt 事件处理中 return 的深入解析 在 Qt 事件处理中&#xff0c;return 语句的使用是另一个关键概念&#xff0c;它与 event->accept()/event->ignore() 密切相关但作用不同。让我们详细分析一下它们之间的关系和工作原理。 核心区别&#xff1a;不同层级的事件处理 方…...