Tomcat源码解析(八):一个请求的执行流程(附Tomcat整体总结)

Tomcat源码系列文章

Tomcat源码解析(一):Tomcat整体架构

Tomcat源码解析(二):Bootstrap和Catalina

Tomcat源码解析(三):LifeCycle生命周期管理

Tomcat源码解析(四):StandardServer和StandardService

Tomcat源码解析(五):StandardEngine、StandardHost、StandardContext、StandardWrapper

Tomcat源码解析(六):Connector、ProtocolHandler、Endpoint

Tomcat源码解析(七):底层如何获取请求url、请求头、json数据?

Tomcat源码解析(八):一个请求的执行流程


文章目录

  • 前言
  • 一、Engine管道内容
    • 1、StandardEngineValve
    • 2、如何通过Engine找到Host
  • 二、Host管道内容
    • 1、ErrorReportValve(拼接html错误页面)
    • 2、StandardHostValve
    • 3、如何通过Host找到Context(上下文)
  • 三、Context管道内容
    • 1、StandardContextValve
    • 2、如何通过Context找到Wrapper
  • 四、Wrapper管道内容
    • 1、StandardWrapperValve
      • 1.1、创建过滤器链
      • 1.2、执行过滤器链
        • 1.2.1、Request和Response的门面模式
        • 1.2.2、doFilter方法
  • Tomcat最终总结


前言

  前文中我们介绍了NIO解析请求数据,网络字节流转化为Request和Response对象。接下来介绍拿到Req和Res之后如何走到Servelt,以及正常响应返回。

  回顾之前篇章,NioEndpoint通过socket服务端ServerSocketChannel.accept()监听8080端口接收连接,获取到连接扔给连接池处理,SocketProcessor从NioChannel通道中读取数据到ByteBuff缓冲区再赋值给对应属性,最后通过适配器CoyoteAdapter生成容器Req和Res调用容器管道的执行方法。

在这里插入图片描述

  Endpoint是连接器Connector的核心组件之一,那么NioEndpoint接受到的连接最后交给自己的连接器connector;由如下server.xml可知,Service对象由一个容器Engine和多个连接器Connector组成,所以结合上面核心代码connector.getService().getContainer()获取到的就是自己的顶级容器Engine

在这里插入图片描述

  以前第一篇文章Tomcat源码解析(一):Tomcat整体架构最后一部分有说过管道的结构。这里再简单的说下容器管道,其实可以理解为容器Engine、Host、Context、Wrapper设置的拦截器,一个请求进来,需要通过每个容器设置的拦截器(如下链状结构,可以设置多个),也就是说每个容器可能有多个处理点。作用其实就是在请求Servelt之前可以拦截请求做一些额外处理另外一方面,也是从顶级容器Engine找到Wrapper从而找到Servelt执行我们写的业务逻辑

在这里插入图片描述

一、Engine管道内容

1、StandardEngineValve

  这里感觉没啥核心内容,其实就是找到对应的Host,然后调用Host的管道执行方法。

final class StandardEngineValve extends ValveBase {...@Overridepublic final void invoke(Request request, Response response)throws IOException, ServletException {// 从request中获取虚拟主机hostHost host = request.getHost();if (host == null) {response.sendError(HttpServletResponse.SC_BAD_REQUEST,sm.getString("standardEngine.noHost",request.getServerName()));return;}if (request.isAsyncSupported()) {request.setAsyncSupported(host.getPipeline().isAsyncSupported());}// 请此主机处理此请求,调用对应主机的管道执行方法host.getPipeline().getFirst().invoke(request, response);}
}

2、如何通过Engine找到Host

  顶级容器Engine下可以有多个虚拟主机Host(主机名称和ip地址,默认localhost);在上篇文章中讲过NIO解析请求数据,里面自然包括请求ip地址,此时只要比对下即可在多个虚拟主机Host中找到。

  在解析请求后会调用如下方法,最终会将获取到Host对象的mappingData.host属性赋值给Request,这样上面request.getHost()就能获取到对应的Host了。

在这里插入图片描述

二、Host管道内容

AccessLogValve

  • 这个处理点是日志记录用的,具体也没研究干啥的
  • 这里可以理解为Host的拦截器链,这个执行点执行完,调用下一个

在这里插入图片描述

1、ErrorReportValve(拼接html错误页面)

  • 接下来这个处理点,就是本单元的主要讲的内容,错误页面的拼接
public class ErrorReportValve extends ValveBase {...@Overridepublic void invoke(Request request, Response response) throws IOException, ServletException {// 执行请求getNext().invoke(request, response);...// 此异常是执行请求时候捕获的,如我们的业务逻辑抛出的异常,这里就能获取到// 后面会讲到Throwable throwable = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);...try {// 返回html的错误页面report(request, response, throwable);} catch (Throwable tt) {ExceptionUtils.handleThrowable(tt);}}...
}
  • 在1xx、2xx和3xx状态下不执行任何操作
  • 4xx客户端错误,5xx服务端错误则需要组装错误响应业务
  • html字符串拼接完成后,将数据通过网络写出到客户端
// ErrorReportValve类方法
protected void report(Request request, Response response, Throwable throwable) {int statusCode = response.getStatus();// 在 1xx、2xx 和 3xx 状态下不执行任何操作// 4xx客户端错误,5xx服务端错误则需要组装错误响应业务if (statusCode < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) {return;}...// sb即为拼接的html返回字符串StringBuilder sb = new StringBuilder();sb.append("<!doctype html><html lang=\"");sb.append(smClient.getLocale().getLanguage()).append("\">");sb.append("<head>");sb.append("<title>");sb.append(smClient.getString("errorReportValve.statusHeader",String.valueOf(statusCode), reason));sb.append("</title>");sb.append("<style type=\"text/css\">");sb.append(TomcatCSS.TOMCAT_CSS);sb.append("</style>");sb.append("</head><body>");sb.append("<h1>");sb.append(smClient.getString("errorReportValve.statusHeader",String.valueOf(statusCode), reason)).append("</h1>");if (isShowReport()) {sb.append("<hr class=\"line\" />");sb.append("<p><b>");sb.append(smClient.getString("errorReportValve.type"));sb.append("</b> ");if (throwable != null) {sb.append(smClient.getString("errorReportValve.exceptionReport"));} else {sb.append(smClient.getString("errorReportValve.statusReport"));}sb.append("</p>");if (!message.isEmpty()) {sb.append("<p><b>");sb.append(smClient.getString("errorReportValve.message"));sb.append("</b> ");sb.append(message).append("</p>");}sb.append("<p><b>");sb.append(smClient.getString("errorReportValve.description"));sb.append("</b> ");sb.append(description);sb.append("</p>");if (throwable != null) {String stackTrace = getPartialServletStackTrace(throwable);sb.append("<p><b>");sb.append(smClient.getString("errorReportValve.exception"));sb.append("</b></p><pre>");sb.append(Escape.htmlElementContent(stackTrace));sb.append("</pre>");int loops = 0;Throwable rootCause = throwable.getCause();while (rootCause != null && (loops < 10)) {stackTrace = getPartialServletStackTrace(rootCause);sb.append("<p><b>");sb.append(smClient.getString("errorReportValve.rootCause"));sb.append("</b></p><pre>");sb.append(Escape.htmlElementContent(stackTrace));sb.append("</pre>");// In case root cause is somehow heavily nestedrootCause = rootCause.getCause();loops++;}sb.append("<p><b>");sb.append(smClient.getString("errorReportValve.note"));sb.append("</b> ");sb.append(smClient.getString("errorReportValve.rootCauseInLogs"));sb.append("</p>");}sb.append("<hr class=\"line\" />");}if (isShowServerInfo()) {sb.append("<h3>").append(ServerInfo.getServerInfo()).append("</h3>");}sb.append("</body></html>");try {try {response.setContentType("text/html");response.setCharacterEncoding("utf-8");} catch (Throwable t) {ExceptionUtils.handleThrowable(t);}Writer writer = response.getReporter();if (writer != null) {// 将响应的html写到响应对象Response的一个字符缓冲区CharBuffer中writer.write(sb.toString());// 将响应缓冲区中的数据通过网络发送给客户端response.finishResponse();}} catch (IOException e) {// Ignore} catch (IllegalStateException e) {// Ignore}
}
  • html字符串对应业务关键内容

在这里插入图片描述

2、StandardHostValve

  • 回到执行请求那里,继续向里走

在这里插入图片描述

  • 一旦我们业务代码抛出异常,这里会获取到,然后设置响应码response.setStatus(500)等等
  • 这些都是为上面说的拼接html错误页面做准备
// StandardHostValve类方法
@Override
public final void invoke(Request request, Response response)throws IOException, ServletException {// 获取请求的Context(上下文)Context context = request.getContext();if (context == null) {return;}...try {...try {if (!response.isErrorReportRequired()) {// 下一个执行点NonLoginAuthenticatorcontext.getPipeline().getFirst().invoke(request, response);}} catch (Throwable t) {...}...// 此异常是执行请求时候捕获的,如我们的业务逻辑抛出的异常,这里就能获取到Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);// Look for (and render if found) an application level error pageif (response.isErrorReportRequired()) {if (t != null) {// 设置响应信息// public static final int SC_INTERNAL_SERVER_ERROR = 500;// response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);throwable(request, response, t);} else {status(request, response);}}	...} finally {...}
}

NonLoginAuthenticator

  • 此处理点主要是tomcat登录权限以及其他权限校验,暂不做研究
  • 接着继续下一个就是Context的处理点

在这里插入图片描述

3、如何通过Host找到Context(上下文)

  虚拟主机Host下可能有多个项目,即webapps目录下的文件夹,每个文件夹就是一个应用项目,而这个文件夹的名称即请求url的统一前缀。

  在解析请求后调用如下方法,最终会将获取到Context对象的mappingData.context属性赋值给Request,这样上面request.getContext()就能获取到上下文Context。

在这里插入图片描述

三、Context管道内容

1、StandardContextValve

  • 禁止直接访问 WEB-INFMETA-INF 下的资源
  • 获取Wrapper,找不到设置错误码404,最后调用Wrapper的处理点
// StandardContextValve类方法
@Override
public final void invoke(Request request, Response response)throws IOException, ServletException {// 禁止直接访问 WEB-INF 或 META-INF 下的资源MessageBytes requestPathMB = request.getRequestPathMB();if ((requestPathMB.startsWithIgnoreCase("/META-INF/", 0))|| (requestPathMB.equalsIgnoreCase("/META-INF"))|| (requestPathMB.startsWithIgnoreCase("/WEB-INF/", 0))|| (requestPathMB.equalsIgnoreCase("/WEB-INF"))) {response.sendError(HttpServletResponse.SC_NOT_FOUND);return;}// 获取请求的WrapperWrapper wrapper = request.getWrapper();if (wrapper == null || wrapper.isUnavailable()) {response.sendError(HttpServletResponse.SC_NOT_FOUND);return;}// 确认请求try {// 最终会调用 Http11Processor#ack() 方法// 也就是简单地将 HTTP/1.1 100 加上回车换行符写给客户端// public static final byte[] ACK_BYTES = ByteChunk.convertToBytes("HTTP/1.1 100 " + CRLF + CRLF);response.sendAcknowledgement();} catch (IOException ioe) {container.getLogger().error(sm.getString("standardContextValve.acknowledgeException"), ioe);request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, ioe);response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);return;}if (request.isAsyncSupported()) {request.setAsyncSupported(wrapper.getPipeline().isAsyncSupported());}// 调用Wrapper的处理点wrapper.getPipeline().getFirst().invoke(request, response);
}

2、如何通过Context找到Wrapper

  tomcat启动时候,在将所有Servelt实例化以后,会将所有的映射url和Wrapper组成MappedWrapper统一放到esactWrappers集合中。

  在解析请求后调用如下方法,通过请求解析的path找到esactWrappers集合中对应的MappedWrapper,最终会将获取到Wrapper对象的mappingData.wrapper属性赋值给Request,这样上面request.getWrapper()就能获取到Wrapper,从而找到Servelt。

在这里插入图片描述

  之前篇章Tomcat源码解析(五):StandardEngine、StandardHost、StandardContext、StandardWrapper最后一节Mapper组件介绍过Mapper的组成,下面再来看下Mapper中对应映射和Wrapper的位置

在这里插入图片描述

四、Wrapper管道内容

1、StandardWrapperValve

  • 第一步:获取Wrapper中的Servelt实例(loadOnStartup>0的已经在项目启动时候实例化和初始化),如果loadOnStartup默认值-1则表示此时才会实例化和初始化Servelt并返回
  • 第二步:为此请求创建过滤器链(包括要执行的Servelt),过滤器链先添加Servelt,再通过过滤器的urlPatterns和servletNames匹配当前servelt添加到过滤器链中
  • 第三步:过滤器链执行完以后,释放过滤器链,将过滤器链中的过滤器和Servelt置为空,因为下个请求还需要重新创建过滤器链
// StandardWrapperValve类方法
@Override
public final void invoke(Request request, Response response)throws IOException, ServletException {..// 获取WrapperStandardWrapper wrapper = (StandardWrapper) getContainer();Servlet servlet = null;Context context = (Context) wrapper.getParent();...// 分配一个 servlet 实例来处理此请求// 如果是loadOnStartup>0的Servlet直接从Wrapper中获取即可,否则需要实例化创建try {if (!unavailable) {servlet = wrapper.allocate();}} catch (xxxException e) {...} // 解析请求的mapping映射// 如:http://localhost:8080/springmvc/servletTomcat,这里为/serveltTomcatMessageBytes requestPathMB = request.getRequestPathMB();...	// 为此请求创建过滤器链(包括要执行的Servelt)ApplicationFilterChain filterChain =ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);// 为此请求调用过滤器链// 注意:这也调用了servlet的service()方法try {if ((servlet != null) && (filterChain != null)) {...// 执行连接器链filterChain.doFilter(request.getRequest(),response.getResponse());...}} catch (xxxException e) {throwable = e;// 将异常添加到request,并设置错误码500exception(request, response, e);}// 释放过滤器链,将过滤器链中的过滤器和Servelt置为空if (filterChain != null) {filterChain.release();}...    
}

1.1、创建过滤器链

  • 从req从获取过滤器链,没有的话创建ApplicationFilterChain过滤器链对象
  • servelt添加到过滤器链中
  • 获取项目启动时候实例化的所有过滤器
  • 先根据过滤器的urlPatterns匹配当前servelt,匹配成功添加到过滤器链中
  • 再根据过滤器的servletNames匹配当前servelt,匹配成功添加到过滤器链中
// ApplicationFilterFactory类方法
public static ApplicationFilterChain createFilterChain(ServletRequest request,Wrapper wrapper, Servlet servlet) {// 如果servelt为空,则返回nullif (servlet == null)return null;// 创建过滤器链对象,并设置给requestApplicationFilterChain filterChain = null;if (request instanceof Request) {Request req = (Request) request;if (Globals.IS_SECURITY_ENABLED) {// Security: Do not recyclefilterChain = new ApplicationFilterChain();} else {filterChain = (ApplicationFilterChain) req.getFilterChain();if (filterChain == null) {filterChain = new ApplicationFilterChain();req.setFilterChain(filterChain);}}} else {// Request dispatcher in usefilterChain = new ApplicationFilterChain();}// 将servelt添加到过滤器链中filterChain.setServlet(servlet);filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());// 获取上下文及项目启动加载的所有过滤器StandardContext context = (StandardContext) wrapper.getParent();FilterMap filterMaps[] = context.findFilterMaps();// 如果没有过滤器,我们就完成了,自己返回,里面只有serveltif ((filterMaps == null) || (filterMaps.length == 0))return (filterChain);// 拦截方式配置也就是资源被访问的形式(没明白)DispatcherType dispatcher =(DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);...String servletName = wrapper.getName();// 根据过滤器的urlPatterns匹配当前serveltfor (int i = 0; i < filterMaps.length; i++) {if (!matchDispatcher(filterMaps[i] ,dispatcher)) {continue;}if (!matchFiltersURL(filterMaps[i], requestPath)){continue;}ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMaps[i].getFilterName());if (filterConfig == null) {continue;}// 添加到过滤器链filterChain.addFilter(filterConfig);}// 根据过滤器的servletNames匹配当前serveltfor (int i = 0; i < filterMaps.length; i++) {if (!matchDispatcher(filterMaps[i] ,dispatcher)) {continue;}if (!matchFiltersServlet(filterMaps[i], servletName)){continue;}ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMaps[i].getFilterName());if (filterConfig == null) {// FIXME - log configuration problemcontinue;}// // 添加到过滤器链filterChain.addFilter(filterConfig);}// 返回完整的过滤器链return filterChain;
}

1.2、执行过滤器链

在这里插入图片描述

1.2.1、Request和Response的门面模式

  在调用拦截器链之前,先看下request.getRequest(), response.getResponse()这两个方法,在这之前Request指的是Request implements HttpServletRequest,Response指的是Response implements HttpServletResponse。从这个方法进入以后,Request指的是RequestFacade implements HttpServletRequest,Response指的是ResponseFacade implements HttpServletResponse这里是利用门面模式,将Req和Res的内容分别包装在RequestFacade和ResponseFacade里面,后者就是起到一个传递作用,为的是保护Req和Res中的属性方法,只在后者暴露想让业务调用者调用的属性和方法。

获取RequestFacade和ResponseFacade

  • 其实很简单,就是在RequestFacade和ResponseFacade对象中分别设置request和response属性
  • 外界获取属性方法都是在RequestFacade的方法中调用Req和Res所得
// Request类方法
public HttpServletRequest getRequest() {if (facade == null) {facade = new RequestFacade(this);}if (applicationRequest == null) {applicationRequest = facade;}return applicationRequest;
}
// RequestFacade构造方法
protected Request request = null;
public RequestFacade(Request request) {this.request = request;
}// Response类方法
public HttpServletResponse getResponse() {if (facade == null) {facade = new ResponseFacade(this);}if (applicationResponse == null) {applicationResponse = facade;}return applicationResponse;
}
// ResponseFacade构造方法
protected Response response = null;
public ResponseFacade(Response response) {this.response = response;
}
1.2.2、doFilter方法

在这里插入图片描述

  • 核心方法,先执行拦截器链,再执行Servelt实例
private void internalDoFilter(ServletRequest request,ServletResponse response)throws IOException, ServletException {// 如果有,请调用下一个过滤器。n是过滤器的个数,pos默认值是0if (pos < n) {ApplicationFilterConfig filterConfig = filters[pos++];try {Filter filter = filterConfig.getFilter();...filter.doFilter(request, response, this);...} catch (IOException | ServletException | RuntimeException e) {throw e;} return;}// 调用Servelt实例try {...servlet.service(request, response);...} catch (IOException | ServletException | RuntimeException e) {throw e;} 
}

  拦截器实例,拦截器的foFilter方法最后一定要调用filterChain.doFilter(servletRequest,servletResponse)这样整个拦截器链包括Servelt实例才能调用完整。

在这里插入图片描述

  就这样,一个请求的执行流程执行完毕。

Tomcat最终总结

看着server.xml更容易理解

在这里插入图片描述

  • 一个Server类的实例就代表了一个Tomcat的容器,一个Tomcat进程只会有一个Server实例,也是Tomcat的主线程Socket监听8005端口,ServerSocket服务端只要接受到Socket客户端发送消息“SHUTDOWN”(不论大小写),就会停止Tomcat应用
  • 一个Server实例可以包含多个Service对象,Service对象由一个容器和多个连接器组成
    • 容器:加载和管理Servlet,以及具体处理Request请求
    • 连接器:处理Socket连接,负责网络字节流与Request和Response对象的转化
  • 容器分为:顶级容器Engine,虚拟主机Host,Web应用程序Context,Servelt包装类Wrapper
    • Engine:从一个或多个Connector中接受请求并处理,并将完成的响应返回给Connector,最终传递给客户端。解析server.xml获取它下面所有的Host引用
    • Host:运行多个Web应用(一个Context代表一个Web应用 ),并负责安装、展开、启动和结束每个Web应用。Context和Wrapper中解析出的请求映射和Servelt的内容统一放到Mapper中获取
    • Context:一个web应用。加载webapps目录下的web应用,实例化和初始化监听器、过滤器、Servlet
  • 考虑到不同网络通信和应用层协议,所以会有不同的连接器
    • 默认8080端口的http协议,8009的AJP协议
    • 连接器核心组件Endpoint使用三种线程接受处理请求
      • Acceptor线程:一直死循环通过SocketChannel的accept方法接受连接,阻塞方法
      • Poller线程:获取到Acceptor线程的连接,通过SocketChannel注册监听读事件,交给连接池处理
      • 任务线程:读取解析socket请求数据封装为request和response调用Servelt方法
  • 请求的处理流程(结合上面server.xml理解)
    • 连接器Connector监听解析拿到请求,通过Service对象找到唯一的顶级容器Engine
    • 顶级容器下有多个虚拟主机Host,与本次请求解析的url对比,获取本次请求的Host
    • 虚拟主机下的webapps下有多个web应用,与本次请求url的path对比,获取本次请求web应用
    • web应用下有多个Servelt,通过Mapper中记录的请求Mapping映射和Servelt对应关系找到Servevlt

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Seq2seq、编码器解码器神经网络

目录 一、Seq2seq 简介二、编码器三、解码器四、编码器-解码器的训练 遇到看不明白的地方&#xff0c;欢迎在评论中留言呐&#xff0c;一起讨论&#xff0c;一起进步&#xff01; 需掌握的前提知识&#xff1a; LSTM、词嵌入 本文参考&#xff1a;【官方双语】编码、解码神经网…

在Cisco Packet Tracer上配置NAT

目录 前言一、搭建网络拓扑1.1 配置PC机1.2 配置客户路由器1.3 配置ISP路由器 二、配置NAT2.1 在客户路由器中配置NAT2.2 测试是否配置成功 总结 前言 本篇文章是在了解NAT的原理基础上&#xff0c;通过使用Cisco Packet Tracer 网络模拟器实现模拟对NAT的配置&#xff0c;以加…

C语言学习系列:GCC编译器Windows版本MinGW-w64的安装教程

本文图文分享如何安装C语言编译器——MinGW-w64。 只要看到这篇文章&#xff0c;就可以按照文中步骤正确安装MinGW-w64并使用。 一、什么是 MinGW-w64 &#xff1f; 我们知道C语言是高级语言&#xff0c;必须编译为二进制文件&#xff0c;才能为计算机运行&#xff0c;MinGW…

二开版微交易系统

下载地址&#xff1a;二开版微交易系统

1035 插入与归并(测试点6)

solution 类型判断&#xff1a;插入排序中已排序的部分有序&#xff0c;未排序的和原数组元素相同&#xff1b;否则为归并排序测试点6&#xff1a;对于归并排序的子序列长度&#xff0c;不能简单视为前k个有序则子序列长度就是k 例如该测试用例的归并排序的子序列长度应该为2&…

2024海南省大数据教师培训-Hadoop集群部署

前言 本文将详细介绍Hadoop分布式计算框架的来源&#xff0c;架构和应用场景&#xff0c;并附上最详细的集群搭建教程&#xff0c;能更好的帮助各位老师和同学们迅速了解和部署Hadoop框架来进行生产力和学习方面的应用。 一、Hadoop介绍 Hadoop是一个开源的分布式计算框架&…

牛客java基础(一)

A 解析 : java源程序只允许一个public类存在 &#xff0c;且与文件名同名 ; D hashCode方法本质就是一个哈希函数&#xff0c;这是Object类的作者说明的。Object类的作者在注释的最后一段的括号中写道&#xff1a;将对象的地址值映射为integer类型的哈希值。但hashCode()并不…

SQL Chat:从SQL到SPEAKL的数据库操作新纪元

引言 SQL Chat是一款创新的、对话式的SQL客户端工具。 它采用自然语言处理技术&#xff0c;让你能够像与人交流一样&#xff0c;通过日常对话的形式对数据库执行查询、修改、创建及删除操作 极大地简化了数据库管理流程&#xff0c;提升了数据交互的直观性和效率。 在这个框…

【虚拟现实】一、AR与VR的基本原理

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 增强现实&#xff08;AR&#xff09;和虚拟现实&#xff08;VR&#xff09;技术已经从科幻小说走入现实&#xf…

UltraScale+系列模块化仪器,可以同时用作控制器、算法加速器和高速数字信号处理器

基于 XCZU7EG / XCZU4EG / XCZU2EG • 灵活的模块组合 • 易于嵌入的紧凑型外观结构 • 高性能的 ARM Cortex 处理器 • 成熟的 FPGA 可编程逻辑 &#xff0c;基于 IP 核的软件库 基于 Xilinx Zynq UltraScaleMPSoC 的 FPGA 技术&#xff0c;采用 Xilinx Zynq UltraScale&a…

Spring-Security(二)OAuth2认证详解(持续更新)

Spring Security & Oauth2系列&#xff1a; Spring Security&#xff08;一&#xff09; 源码分析及认证流程 Spring Security&#xff08;二&#xff09;OAuth2认证详解及自定义异常处理 文章目录 1、OAuth2.0 简介1.1 OAuth2.0 相关名词解释1.2 四种授权模式 1.3 、OAu…

接口(API)开发,测试工具-apifox

前言 为什么需要接口&#xff08;API&#xff09;? 因为不同的平台或系统可能使用不同的技术栈、编程语言或数据格式。API提供了一个标准化的方式&#xff0c;使得这些不同的系统可以相互交换数据和功能调用&#xff0c;实现互操作性 在开发日常的项目交互中&#xff0c;不…

内网穿透的方式有哪些——快解析的优势

外网穿透内网技术&#xff0c;即内网映射&#xff0c;是把目标本地内网地址和端口发布到互联网&#xff0c;是一种由内网开放到外网的权限操作。那么&#xff0c;内网穿透的方法有哪些呢&#xff1f;做映射外网的方法。需要结合自己本地网络环境和应用场景来实施。这里分享三种…

Python Excel 指定内容修改

需求描述 在处理Excel 自动化时,财务部门经常有一个繁琐的场景,需要读取分发的Excel文件内容复制到汇总Excel文件对应的单元格内,如下图所示: 这种需求可以延申为,财务同事制作一个模板,将模板发送给各员工,财务同事需收取邮件将员工填写的excel文件下载到本机,再类似…

[FreeRTOS 基础知识] 任务调度 与 链表

文章目录 任务并行的概念RTOS如何实现多任务调度&#xff1f; 任务并行的概念 在生活中&#xff0c;经常出现一心多用的情况。比如你需要一边吃饭一边手机回复信息&#xff0c;这里面就存在两个任务&#xff1a;任务一、吃饭。任务二、手机回复信息。 假如你无法一心多用&…

FreeRTOS实时系统 在任务中增加数组等相关操作 导致单片机起不来或者挂掉

在调试串口任务中增加如下代码&#xff0c;发现可以用keil进行仿真&#xff0c;但是烧录程序后&#xff0c;调试串口没有打印&#xff0c;状态灯也不闪烁&#xff0c;单片机完全起不来 博主就纳了闷了&#xff0c;究竟是什么原因&#xff0c;这段代码可是公司永流传的老代码了&…

平面设计神器CorelDRAW2021精简版,你值得拥有!

亲爱的设计师小伙伴们&#xff0c;今天我要为大家种草一款神奇的软件——CorelDRAW平面设计软件2021精简版&#xff01;&#x1f929;✨作为一名专业的图形设计师&#xff0c;我深知一个好工具对于我们的工作有多么重要。而这款软件简直就是我们设计师的救星&#xff01;&#…

【MySQL数据库】:MySQL索引特性

目录 索引的概念 磁盘 磁盘的基本特征 MySQL与磁盘交互的基本单位 索引的理解 建立测试表 理解单个Page 理解多个Page 页目录 单页情况 多页情况 索引的数据结构 聚簇索引 VS 非聚簇索引 索引操作 创建主键索引 创建唯一索引 创建普通索引 创建全文索引 查询…

每日一题——Python实现PAT乙级1099 性感素数(举一反三+思想解读+逐步优化)

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 我的写法 专业点评 时间复杂度分析 空间复杂度分析 综合点评 我要更强 优化点 …

力扣每日一题130:被围绕的区域

题目 中等 相关标签 相关企业 给你一个 m x n 的矩阵 board &#xff0c;由若干字符 X 和 O &#xff0c;找到所有被 X 围绕的区域&#xff0c;并将这些区域里所有的 O 用 X 填充。 示例 1&#xff1a; 输入&#xff1a;board [["X","X","X"…