zuul源码分析

news2024/11/25 15:57:09

zuul源码解析

zuul与springboot整合的依赖

   <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>

看到starter第一反应就是springboot的自动装配?
我们去到zuul的包下下边的spring.factories文件中可以看到文件内容

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration,\
org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration

可以看到配置的两个类ZuulServerAutoConfiguration,ZuulProxyAutoConfiguration
在这里插入图片描述
在这里插入图片描述
这两个类的区别是生效条件

@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)

zuul网关在使用的时候一般在启动类上加@EnableZuulProxy注解
点进去看看

@EnableCircuitBreaker
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({ZuulProxyMarkerConfiguration.class})
public @interface EnableZuulProxy {
}

为我们导入了ZuulProxyMarkerConfiguration 类
在这里插入图片描述
只干了又1件事就是为我们导入mark使启动来生效

那么问题来了ZuulServerAutoConfiguration 和 ZuulProxyAutoConfiguration 的区别是啥

在这里插入图片描述
ZuulServerAutoConfiguration的静态内部类ZuulFilterConfiguration

@Configuration(
        proxyBeanMethods = false
    )
    protected static class ZuulFilterConfiguration {
        @Autowired
        private Map<String, ZuulFilter> filters;

        protected ZuulFilterConfiguration() {
        }

        @Bean
        public ZuulFilterInitializer zuulFilterInitializer(CounterFactory counterFactory, TracerFactory tracerFactory) {
            FilterLoader filterLoader = FilterLoader.getInstance();
            FilterRegistry filterRegistry = FilterRegistry.instance();
            return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry);
        }
    }

上边代码就是初始化过滤器

其中

      @Autowired
        private Map<String, ZuulFilter> filters;

会把所有继承了ZuulFilter 并被spring管理的过滤器全部加在进来,这是spring 实现的

请求拦截器的确定
接下来问题来了,当我用postman请求zuul,会被哪个类处理呢?
在这里插入图片描述
框架处理请求用的是springMvc,熟悉springMvc源码的都知道请求是由DispatcherServlet处理的,这里我在DispatcherServlet.doDispatch方法上打了一个断点
在这里插入图片描述
getHandler方法:根据request请求获取Handler
在这里插入图片描述
回过来再看:ZuulServerAutoConfiguration
在这里插入图片描述
向容器中注入了ZuulHandlerMapping,ZuulController

ZuulController
在这里插入图片描述
ZuulController源码发现继承ServletWrappingController
于是搜索ServletWrappingController是做什么的
在这里插入图片描述

ServletWrappingController 会拦截请求,交给内部包装的的servlet进行处理
最终确定所有请求由ZuulController拦截,由内部包装的ZuulServlet进行处理

请求到ZuulController的handleRequest方法后会调用
在这里插入图片描述
这个servletInstance实例就是ZuulServlet
处理每次http请求
service方法

  public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        try {
            this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse);
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

            try {
                this.preRoute();
            } catch (ZuulException var13) {
                this.error(var13);
                this.postRoute();
                return;
            }

            try {
                this.route();
            } catch (ZuulException var12) {
                this.error(var12);
                this.postRoute();
                return;
            }

            try {
                this.postRoute();
            } catch (ZuulException var11) {
                this.error(var11);
            }
        } catch (Throwable var14) {
            this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }

我们看下preRoute方法

   public void preRoute() throws ZuulException {
        try {
            this.runFilters("pre");
        } catch (ZuulException var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new ZuulException(var3, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + var3.getClass().getName());
        }
    }

runFilters方法
在这里插入图片描述

主要就是获取所有pre类型的过滤器并排序
然后滴哦用processZuulFilter方法

processZuulFilter方法

public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        boolean bDebug = ctx.debugRouting();
        String metricPrefix = "zuul.filter-";
        long execTime = 0L;
        String filterName = "";

        try {
            long ltime = System.currentTimeMillis();
            filterName = filter.getClass().getSimpleName();
            RequestContext copy = null;
            Object o = null;
            Throwable t = null;
            if (bDebug) {
                Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
                copy = ctx.copy();
            }

            ZuulFilterResult result = filter.runFilter();
            ExecutionStatus s = result.getStatus();
            execTime = System.currentTimeMillis() - ltime;
            switch (s) {
                case FAILED:
                    t = result.getException();
                    ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                    break;
                case SUCCESS:
                    o = result.getResult();
                    ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
                    if (bDebug) {
                        Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
                        Debug.compareContextState(filterName, copy);
                    }
            }

            if (t != null) {
                throw t;
            } else {
                this.usageNotifier.notify(filter, s);
                return o;
            }
        } catch (Throwable var15) {
            if (bDebug) {
                Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + var15.getMessage());
            }

            this.usageNotifier.notify(filter, ExecutionStatus.FAILED);
            if (var15 instanceof ZuulException) {
                throw (ZuulException)var15;
            } else {
                ZuulException ex = new ZuulException(var15, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
                ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                throw ex;
            }
        }
    }
**最重要就是调用runfliter方法**
 public ZuulFilterResult runFilter() {
        ZuulFilterResult zr = new ZuulFilterResult();
        if (!this.isFilterDisabled()) {
            if (this.shouldFilter()) {
                Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());

                try {
                    Object res = this.run();
                    zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
                } catch (Throwable var7) {
                    t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
                    zr = new ZuulFilterResult(ExecutionStatus.FAILED);
                    zr.setException(var7);
                } finally {
                    t.stopAndLog();
                }
            } else {
                zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
            }
        }
	

循环执行过滤器的run方法
执行完preRoute()方法,若出现异常,则依次执行类型为error、post的过滤器,逻辑与pre过滤器一致,区别在于过滤器类型不一样;否则依次执行route()、postRoute()方法。

内部主要过滤器执行逻辑

pre类型
SevletDetectionFilter:决定由ZuulServlet还是DispatchServlet执行请求。

@Override
public Object run() {
	RequestContext ctx = RequestContext.getCurrentContext();
	HttpServletRequest request = ctx.getRequest();
	if (!(request instanceof HttpServletRequestWrapper)
			&& isDispatcherServletRequest(request)) {
        //由DispatchServlet执行请求时,HttpServletRequest没有被包装过并且Attributes中包含
        //DispatchServlet上下文参数
		ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, true);
	}
	else {
        //由ZuulServlet执行请求
		ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, false);
	}
	return null;
}

FormBodyWrapperFilter:表单数据解析过滤器

@Override
public boolean shouldFilter() {
	RequestContext ctx = RequestContext.getCurrentContext();
	HttpServletRequest request = ctx.getRequest();
	String contentType = request.getContentType();
	//GET 请求不执行
	if (contentType == null) {
		return false;
	}
    //仅处理表单数据和在DispatchServlet中的Multipart数据。
	try {
		MediaType mediaType = MediaType.valueOf(contentType);
		return MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType)
			|| (isDispatcherServletRequest(request)
					&& MediaType.MULTIPART_FORM_DATA.includes(mediaType));
	}
	catch (InvalidMediaTypeException ex) {
		return false;
	}
}

@Override
public Object run() {
	RequestContext ctx = RequestContext.getCurrentContext();
	HttpServletRequest request = ctx.getRequest();
	FormBodyRequestWrapper wrapper = null;
	if (request instanceof HttpServletRequestWrapper) {
		HttpServletRequest wrapped = (HttpServletRequest) ReflectionUtils
				.getField(this.requestField, request);
        //FormBodyRequestWrapper其实就是将request中的表单数据提取出来
		wrapper = new FormBodyRequestWrapper(wrapped);
        //把wrapper对象赋值给request中的request属性,供后续使用
		ReflectionUtils.setField(this.requestField, request, wrapper);
		//若请求已经被包装过,则将wrapper对象赋值给包装过的request中的request属性,供后续使用
        if (request instanceof ServletRequestWrapper) {
			ReflectionUtils.setField(this.servletRequestField, request, wrapper);
		}
	}
	else {
		wrapper = new FormBodyRequestWrapper(request);
		ctx.setRequest(wrapper);
	}
	if (wrapper != null) {
		ctx.getZuulRequestHeaders().put("content-type", wrapper.getContentType());
	}
	return null;
}

PreDecorationFilter:主要负责决定客户端请求的路由以及发送给下游服务的额外请求头。

@Override
public boolean shouldFilter() {
	RequestContext ctx = RequestContext.getCurrentContext();
	return !ctx.containsKey(FORWARD_TO_KEY) //还未经过决定请求转发的过滤器
			&& !ctx.containsKey(SERVICE_ID_KEY); //还未经过决定serviceId的过滤器
}
@Override
public Object run() {
	RequestContext ctx = RequestContext.getCurrentContext();
	final String requestURI = this.urlPathHelper
			.getPathWithinApplication(ctx.getRequest());
	if (insecurePath(requestURI)) {
		throw new InsecureRequestPathException(requestURI);
	}
    //根据URI以及配置的属性获取路由
	Route route = this.routeLocator.getMatchingRoute(requestURI);
	if (route != null) {
		String location = route.getLocation();
		if (location != null) {
			ctx.put(REQUEST_URI_KEY, route.getPath());
			ctx.put(PROXY_KEY, route.getId());
			if (!route.isCustomSensitiveHeaders()) {
                //将需要过滤的敏感头信息放入RequestContext
				this.proxyRequestHelper.addIgnoredHeaders(
						this.properties.getSensitiveHeaders().toArray(new String[0]));
			}
			else {
				this.proxyRequestHelper.addIgnoredHeaders(
						route.getSensitiveHeaders().toArray(new String[0]));
			}
				if (route.getRetryable() != null) {
				ctx.put(RETRYABLE_KEY, route.getRetryable());
			}
            //网关配置为url
			if (location.startsWith(HTTP_SCHEME + ":")
					|| location.startsWith(HTTPS_SCHEME + ":")) {
		//设置下游服务的地址,供后续SimpleHostRoutingFilter(以httpClient的方式请求下游服务)使用
                ctx.setRouteHost(getUrl(location));
				ctx.addOriginResponseHeader(SERVICE_HEADER, location);
			}
            //网关配置的url以forward:开头,表明请求转发。
			else if (location.startsWith(FORWARD_LOCATION_PREFIX)) {
		        //去除forward标识字符,重新设置下游服务url放入RequestContext,
                //供后续SendForwardFilter过滤器使用        
                ctx.set(FORWARD_TO_KEY,
						StringUtils.cleanPath(
								location.substring(FORWARD_LOCATION_PREFIX.length())											+ route.getPath()));
					ctx.setRouteHost(null);
					return null;
			}
			else {
				//设置serviceId,供后续RibbonRoutingFilter使用
                //由注册中心以及ribbon负载均衡决定最终下游服务地址
				ctx.set(SERVICE_ID_KEY, location);
				ctx.setRouteHost(null);
				ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location);
			}
            //配置文件是否设置加入代理头
            //X-Forwarded-Host:请求的主机列表,用逗号隔开,越靠后越接近服务器
            //X-Forwarded-Port:请求的端口列表,用逗号隔开,越靠后越接近服务器
            //Forwarded-Proto:请求的协议列表,用逗号隔开,越靠后越接近服务器
            //X-Forwarded-Prefix:请求网关的前缀
            //X-Forwarded-For:请求的ip列表,用逗号隔开,越靠后越接近服务器
			if (this.properties.isAddProxyHeaders()) {
				addProxyHeaders(ctx, route);
				String xforwardedfor = ctx.getRequest()
						.getHeader(X_FORWARDED_FOR_HEADER);
				String remoteAddr = ctx.getRequest().getRemoteAddr();
				if (xforwardedfor == null) {
					xforwardedfor = remoteAddr;
				}
				else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates
					xforwardedfor += ", " + remoteAddr;
				}
				ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor);
			}
			if (this.properties.isAddHostHeader()) {
				ctx.addZuulRequestHeader(HttpHeaders.HOST,
					toHostHeader(ctx.getRequest()));
			}
		}
	}
	else {
		log.warn("No route found for uri: " + requestURI);
		String forwardURI = getForwardUri(requestURI);
		ctx.set(FORWARD_TO_KEY, forwardURI);
	}
	return null;
}

route类型

RibbonRoutingFilter:由Ribbon负载均衡决定下游服务地址并且进行请求,将下游服务响应结果存入RequestContext

@Override
public boolean shouldFilter() {
	RequestContext ctx = RequestContext.getCurrentContext();
    //以serviceId的方式配置路由时执行该过滤器
	return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
			&& ctx.sendZuulResponse());
}
@Override
public Object run() {
	RequestContext context = RequestContext.getCurrentContext();
	this.helper.addIgnoredHeaders();
	try {
        //根据请求头、请求参数、请求体、请求方法、serviceId、loadBalancerKey等构成
        //ribbon上下文
		RibbonCommandContext commandContext = buildCommandContext(context);
        //创建command并且进行远程调用
		ClientHttpResponse response = forward(commandContext);
		//将下游服务响应结果存放在RequestContext,供后post过滤器SendResponseFilter使用。
        setResponse(response);
		return response;
	}catch (ZuulException ex) {
		throw new ZuulRuntimeException(ex);
	}catch (Exception ex) {
		throw new ZuulRuntimeException(ex);
	}
}

SimpleHostRoutingFilter:以简单Apache HttpClient的方式调用下游服务

@Override
public boolean shouldFilter() {
   //路由配置的是URL时执行该过滤器
   return RequestContext.getCurrentContext().getRouteHost() != null
         && RequestContext.getCurrentContext().sendZuulResponse();
}

@Override
public Object run() {
   RequestContext context = RequestContext.getCurrentContext();
   HttpServletRequest request = context.getRequest();
   //组装请求头,将忽略的请求头过滤
   MultiValueMap<String, String> headers = this.helper
         .buildZuulRequestHeaders(request);
   //组装请求参数
   MultiValueMap<String, String> params = this.helper
         .buildZuulRequestQueryParams(request);
   //组装请求方法
   String verb = getVerb(request);
   InputStream requestEntity = getRequestBody(request);
   if (getContentLength(request) < 0) {
      context.setChunkedRequestBody();
   }

   String uri = this.helper.buildZuulRequestURI(request);
   this.helper.addIgnoredHeaders();

   try {
   	  //远程调用下游服务
      CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,
            headers, params, requestEntity);
      //保存下游服务响应结果
      setResponse(response);
   }
   catch (Exception ex) {
      throw new ZuulRuntimeException(handleException(ex));
   }
   return null;
}

post类型

SendResponseFilter:将响应信息从RequestContext中取出并组装,响应给浏览器。

@Override
public boolean shouldFilter() {
	RequestContext context = RequestContext.getCurrentContext();
    //不发生异常,下游服务有响应并且响应信息没被清空时执行
	return context.getThrowable() == null
			&& (!context.getZuulResponseHeaders().isEmpty()
					|| context.getResponseDataStream() != null
					|| context.getResponseBody() != null);
}
@Override
public Object run() {
	try {
        //组装下游服务的响应头和浏览器响应头
		addResponseHeaders();
        //响应给浏览器
		writeResponse();
	}
	catch (Exception ex) {
		ReflectionUtils.rethrowRuntimeException(ex);
	}
	return null;
}

```


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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

简历制作、投递、(实习)面试技巧!!

目录 1.为什么要找实习 2.什么时候找实习 3.制作简历 4.简历注意事项 5.IT后端的校招的要求 6.简历里面写什么&#xff1f; 7.简历模板的选择 8.个人信息 9.求职意向 10.专业技能 11.项目经验 12.其他注意事项 13.找工作的手段 14.找工作的态度 ​编辑 15.面试…

IntelliJ IDEA 修改内存大小

idea有个配置文件&#xff0c;可以设置内存大小的&#xff0c;就跟咱的jvm的内存里面的堆大小&#xff0c;栈大小等等&#xff0c;可以设置的&#xff0c;而且设置了之后&#xff0c;你这个的性能就会得到提升。具体看下面怎么修改。 先说所要修改的文件 idea.vmoptions 的位置…

总结常见评价指标

整理一下在机器学习中常见的评价指标&#xff0c;包括&#xff1a; 混淆矩阵&#xff0c;TPR&#xff0c;FPR&#xff0c;TNR&#xff0c;FNR&#xff1b;Precision&#xff0c;Recall&#xff0c;Accuracy&#xff0c;F-score(F1-meature)ROC曲线&#xff0c;AUC&#xff1b; …

[Vue warn]: You may have an infinite update loop in a component render function

老板让该一个bug&#xff0c;结果一连出现好几个问题&#xff0c;然后报错也是很奇葩&#xff0c;在源代码上不能直接定位到&#xff0c;只知道在当前页面上出现的问题&#xff0c;弄了好久&#xff0c;给大家分享一下解决的经验&#xff1a; You may have an infinite update …

2023 年Windows MySql 5.7,MySql 8.0 下载安装教程, 附详细图解

文章目录 下载 MySQL 安装程序安装 MySQL 数据库安装示例数据库连接到 MySQL 服务器 在本教程中&#xff0c;我们展示如何在 Windows 平台上下载和安装 MySQL 的详细步骤。 在 Windows 平台上安装 MySQL 很简单&#xff0c;并不需要太复杂的步骤。按照本文的步骤操练起来就可以…

【Win10错误】从80190001错误码恢复

目录 一、说明 二、操作过程和错误显示 三、修复过程 四、网上的其它参考意见 一、说明 出现0x80190001错误码&#xff0c;其原因是网络认证问题引起。但不是网络断开或路由不通而引起。一般是本地身份cooki无法认证而引起&#xff0c;一般出现在登录认证过程。本篇告诉大家…

2.4G无线游戏手柄方案开发

对于游戏玩家来说&#xff0c;好的外设才能有更好的游戏体验。相比于传统的有线手柄&#xff0c;2.4G无线游戏手柄采用2.4GHz射频无线连接方式&#xff0c;摆脱了连线的困扰。相比于鼠标键盘&#xff0c;游戏手柄在大部分游戏上的使用体验都会更好&#xff0c;让你的游戏体验更…

【MATLAB第30期】基于MATLAB的adaboost多分类预测集成学习模型(四种模型GDA高斯判别分析、Knn、NB朴素贝叶斯、SVM)

【MATLAB第30期】基于MATLAB的adaboost多分类预测集成学习模型&#xff08;四种模型GDA高斯判别分析、Knn、NB朴素贝叶斯、SVM&#xff09; 一、简介 弱分类器 %1.GDA高斯判别分析 %2.Knn (NumNeighbors 5) K邻近 %3.Naive Bayes 朴素贝叶斯 %4.SVM 支持向量机 强分类器 1.a…

【Vue】生命周期

文章目录 生命周期概念一、生命周期图示二、生命周期1.beforeCreate&#xff08;&#xff09;{}2.created&#xff08;&#xff09;{}3.beforeMount&#xff08;&#xff09;{}4.mounted&#xff08;&#xff09;{}5.beforeUpdate&#xff08;&#xff09;{}6.updated&#xff…

C语言预处理详解

参考文章&#xff1a;c语言预处理 目录 程序的翻译环境和执行条件 翻译环境 编译本身也分为几个阶段 预处理 预处理指令 运行环境 程序执行的过程 预处理 预定义符号 #define #define定义标识符 #define定义宏 宏的申明方式 #define替换规则 #和## #的作用 ##…

OTP语音芯片 NV170D在充电桩的语音方案应用

新能源汽车是我国应对气候变化、推动绿色发展的战略举措&#xff0c;在政策和市场需求的推动下&#xff0c;我国新能源汽车产销量双双增长&#xff0c;新能源汽车保有量地稳步增长将会促进充电桩需求的扩大&#xff0c;企业也将进一步在电动汽车充电桩领域布局。 2022年10月11日…

Linux下的Tomcat的安装详解--值得一看

如有错误&#xff0c;敬请谅解&#xff01; 此文章仅为本人学习笔记&#xff0c;仅供参考&#xff0c;如有冒犯&#xff0c;请联系作者删除&#xff01;&#xff01; 目录 简述静态网页和动态网页的区别。 简述 Webl.0 和 Web2.0 的区别。 tomcat8的安装&#xff0c;配置服…

物流行业如何运用IPD?

物流是供应链活动的一部分&#xff0c;是为了满足客户需要而对商品、服务消费以及相关信息从产地到消费地的高效、低成本流动和储存进行的规划、实施与控制的过程。物流以仓储为中心&#xff0c;促进生产与市场保持同步。物流是为了满足客户的需要&#xff0c;以最低的成本&…

奇葩的 Git 签名错误

最近公司电脑升级后又抽风了。 在访问 Git 的时候提示了证书签名错误。 主要提示的错误为&#xff1a; git.exe fetch -v --progress "origin" fatal: unable to access https://src.ossez.com/yhu-docs.git/: SSL certificate problem: unable to get local issue…

大学4年做出来这个算不算丢人

前言&#xff1a;相信看到这篇文章的小伙伴都或多或少有一些编程基础&#xff0c;懂得一些linux的基本命令了吧&#xff0c;本篇文章将带领大家服务器如何部署一个使用django框架开发的一个网站进行云服务器端的部署。 文章使用到的的工具 Python&#xff1a;一种编程语言&…

C51 - 红外遥控NEC协议

Infrared 1> 实验概述2> 红外遥控器2.1> 硬件电路 3> NEC红外传输协议3.1> 5部分构成3.2> 引导码3.3> 38KHz载波长啥样?3.4> 咋表示 0 / 1; 4> 红外接收5> 程序设计 1> 实验概述 通过红外遥控器&#xff0c;控制DAYi&#xff08;51开发板&a…

【多线程初阶五】线程池常考面试题

目录 &#x1f31f;一、线程池 &#x1f308;1、线程池是什么&#xff1f; &#x1f308;2、为什么要使用线程池&#xff1f; &#x1f308;3、怎么使用线程池&#xff1f; 1、使用标准库中的线程池&#xff08;6种&#xff09;——>不推荐使用。 2、自定义一个线程池…

【移动端网页布局】flex 弹性布局子项目属性 ① ( flex 属性用法说明 | 代码示例 : 占有剩余布局 / 平均分成若干等份 )

文章目录 一、flex 子项目常用属性1、子项目常用属性介绍2、flex 属性用法说明 二、flex 属性代码示例1、代码示例 - 左右两侧 100 像素 / 中间元素占有所有剩余布局3、代码示例 - 平均分成三等份 一、flex 子项目常用属性 1、子项目常用属性介绍 flex 子项目 的常用属性 : fl…

FL Studio 2023年最新安装使用图文教程,FL Studio 21怎么激活解锁?

Image-Line宣布针对Win和Mac版本的数字音频工作站FL Studio的21版本更新。FL Studio2023是一个完整的软件音乐制作环境或数字音频工作站&#xff08;DAW&#xff09;。代表超过 25年的创新发展&#xff0c;它包含了您在一个包装中编排&#xff0c;编排&#xff0c;录制&#xf…

查询数据(数据库)——简单查询

目录 1&#xff0e;最简单的查询 &#xff08;1&#xff09;查询指定列 &#xff08;2&#xff09;查询所有列 &#xff08;3&#xff09;查询计算列 &#xff08;4&#xff09;为列起别名 &#xff08;5&#xff09;使用DISTINCT关键字消除重复元组 2&#xff0e;查询满…