Spring Cloud Gateway 请求转发源码分析

news2024/9/23 1:23:44

一、背景

Spring Cloud Gateway 作为一种微服务网关组件,相信大家都不陌生,一个请求经过Spring Cloud Gateway是如何转发出去的,今天我们就来分析一下这部分的源码。

二、正文

下面这张图大家在学习Spring Cloud Gateway的时候肯定见过,在分析源码之前我们再来看下这张图。
在这里插入图片描述

其中

  • Gateway Client: 发送请求到 Spring Cloud Gateway 的客户端
  • Gateway Handler Mapping: 是处理请求的组件,负责将请求映射到相应的处理器。处理请求将被路由到哪个路由规则,从而选择对应的过滤器链
  • Gateway Web Handler: 实际处理请求的组件,会依次执行过滤器链,对请求进行处理
  • Gateway Filter: 过滤器链由多个过滤器组成,每个过滤器执行一些特定代码逻辑
  • Proxied Service: 被代理的服务,当执行完过滤器链之后会将请求转发到具体的目标服务

官网上有这样一段解释:

📌Clients make requests to Spring Cloud Gateway. If the Gateway Handler Mapping determines that a request matches a route, it is sent to the Gateway Web Handler. This handler runs the request through a filter chain that is specific to the request。

客户端向 Spring Cloud Gateway 发出请求。如果 Gateway Handler Mapping 确定请求与路由匹配,则会将其发送到 Gateway Web Handler。 此 handler 通过一个特定于请求的过滤器链来运行请求(这个处理程序会让请求通过一系列只为这个请求设计的过滤器。简单来说,就是请求会经过一组专门为它设置的过滤器。

我们先来看下 gateway 的自动配置类 GatewayAutoConfiguration

Gateway的自动配置

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnProperty(
    name = {"spring.cloud.gateway.enabled"},
    matchIfMissing = true
)
@EnableConfigurationProperties
//自动配置前置条件:引入了WebFlux 和 HttpHandler 组件
@AutoConfigureBefore({HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class})
//自动配置后置组件:负载均衡组件
@AutoConfigureAfter({GatewayReactiveLoadBalancerClientAutoConfiguration.class, GatewayClassPathWarningAutoConfiguration.class})
@ConditionalOnClass({DispatcherHandler.class})
public class GatewayAutoConfiguration {
    
    @Bean
    @ConditionalOnMissingBean
    public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator(GatewayProperties properties) {
        return new PropertiesRouteDefinitionLocator(properties);
    }
    
    @Bean
    @Primary
    public RouteDefinitionLocator routeDefinitionLocator(List<RouteDefinitionLocator> routeDefinitionLocators) {
        return new CompositeRouteDefinitionLocator(Flux.fromIterable(routeDefinitionLocators));
    }
    
    @Bean
    @ConditionalOnMissingBean
    public RoutePredicateHandlerMapping routePredicateHandlerMapping(FilteringWebHandler webHandler, RouteLocator routeLocator, GlobalCorsProperties globalCorsProperties, Environment environment) {
        return new RoutePredicateHandlerMapping(webHandler, routeLocator, globalCorsProperties, environment);
    }
    
    @Bean
    public GatewayProperties gatewayProperties() {
        return new GatewayProperties();
    }
    
    @Bean
    @ConditionalOnEnabledGlobalFilter
    public RouteToRequestUrlFilter routeToRequestUrlFilter() {
        return new RouteToRequestUrlFilter();
    }
    
    /**
     * GlobalFilter自动注入使用的是 @Autowired,不需要显式写出,spring 会自动查找所有类型为 GlobalFilter 的 Bean,并将它们注入到这个List中
     */
    @Bean
    public FilteringWebHandler filteringWebHandler(List<GlobalFilter> globalFilters) {
        return new FilteringWebHandler(globalFilters);
    }
    
    ......
    
}

自动配置类GatewayAutoConfiguration在内部初始化了很多bean,列举几个重要的如下:

  • PropertiesRouteDefinitionLocator:用于从配置文件(yml/properties)中读取路由配置信息
  • RouteDefinitionLocator:把 RouteDefinition 转化为 Route
  • RoutePredicateHandlerMapping:类似于 mvc 的HandlerMapping,用于匹配对应的请求 Route
  • GatewayProperties:yml配置信息封装在 GatewayProperties 对象中
  • GlobalFilter实现类:比如RouteToRequestUrlFilter、ForwardRoutingFilter等全局过滤器
  • FilteringWebHandler:注入了所有实现了 GlobalFilter 接口的过滤器类

FilteringWebHandler 中注入的GlobalFilter如下:

{
  "org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@77856cc5": 10100,
  "org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@4f6fd101": 10000,
  "org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@32d22650": -1,
  "org.springframework.cloud.gateway.filter.ForwardRoutingFilter@106459d9": 2147483647,
  "org.springframework.cloud.gateway.filter.NettyRoutingFilter@1fbd5e0": 2147483647,
  "org.springframework.cloud.gateway.filter.ForwardPathFilter@33a71d23": 0,
  "org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@135064ea": 2147483637,
  "org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@23c05889": 2147483646
}

Gateway的工作流程

  1. 所有请求都将由ReactorHttpHandlerAdapter.apply() 方法拦截处理,此时会封装请求对象和响应对象,并传递到HttpWebHandlerAdapter.handle()方法。

  2. HttpWebHandlerAdapter.handle()方法将request和response封装成上下文对象ServerWebExchange,方法通过getDelegate()获取全局异常处理器ExceptionHandlingWebHandler执行全局异常处理

    这两步出现的类是在 spring-web 包下

  3. ExceptionHandlingWebHandler执行完成后,调用DispatcherHandler.handle(),循环所有handlerMappings查找处理当前请求的HandlerDispatcherHandler

    spring-webflux 包下,我们知道Gateway是基于webflux响应式编程

  4. 找到Handler后调用DispatcherHandler.invokeHandler()执行找到的Handler,此时会调用FilteringWebHandler.handle()

  5. DefaultGatewayFilterChain.filter()是关键流程,所有过滤器都会在这里执行,比如服务查找、负载均衡、远程调用等,都在这一块。

Gateway源码分析

📌Spring Cloud Gateway版本:spring-cloud-gateway 3.1.4

我们直接从HttpWebHandlerAdapter.handle()看起

HttpWebHandlerAdapter
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
        if (this.forwardedHeaderTransformer != null) {
            try {
                request = this.forwardedHeaderTransformer.apply(request);
            } catch (Throwable var4) {
                Throwable ex = var4;
                if (logger.isDebugEnabled()) {
                    logger.debug("Failed to apply forwarded headers to " + this.formatRequest(request), ex);
                }
​
                response.setStatusCode(HttpStatus.BAD_REQUEST);
                return response.setComplete();
            }
        }
        // 创建网关上下文对象
        ServerWebExchange exchange = this.createExchange(request, response);
        LogFormatUtils.traceDebug(logger, (traceOn) -> {
            return exchange.getLogPrefix() + this.formatRequest(exchange.getRequest()) + (traceOn ? ", headers=" + this.formatHeaders(exchange.getRequest().getHeaders()) : "");
        });
        // getDelegate()获取当前的Handler
        Mono var10000 = this.getDelegate().handle(exchange).doOnSuccess((aVoid) -> {
            this.logResponse(exchange);
        }).onErrorResume((exx) -> {
            return this.handleUnresolvedError(exchange, exx);
        });
        response.getClass();
        return var10000.then(Mono.defer(response::setComplete));
    }

代码中先创建了网关上下文对象ServerWebExchange,然后getDelegate()获取当前的Handler,我们将断点打到getDelegate() 方法里面:

在这里插入图片描述

当前返回的 WebHandler 是ExceptionHandlingWebHandler,而ExceptionHandlingWebHandler的 delegate 是FilteringWebHandlerFilteringWebHandler的 delegate 是DispatcherHandler,所有的 delegate 的handle()方法都会依次执行。

delegate: 委托、委派 这里应该是使用了委派模式,不属于23种设计模式,类似代理模式,委派模式主要关注任务的调度和分配,注重结果,可以看作是一种特殊的静态代理,即全权代理。

接下来我们可以把断点放到DispatcherHandler.handler()方法上

DispatcherHandler
  public Mono<Void> handle(ServerWebExchange exchange) {
        if (this.handlerMappings == null) {
            return this.createNotFoundError();
        } else {
            // 1.遍历所有的handlerMapping,获取到当前网关上下文中的多个handlerMapping
            return CorsUtils.isPreFlightRequest(exchange.getRequest()) ? this.handlePreFlight(exchange) : Flux.fromIterable(this.handlerMappings).concatMap((mapping) -> {
                return mapping.getHandler(exchange);
            // 2.获取对应的适配器,调用对应的处理器
            }).next().switchIfEmpty(this.createNotFoundError()).flatMap((handler) -> {
                return this.invokeHandler(exchange, handler);
            // 3. 返回处理结果
            }).flatMap((result) -> {
                return this.handleResult(exchange, result);
            });
        }
    }

在这里插入图片描述

可以看到这里所有的handlerMapping ,方法中会调用所有 handlerMappingsgetHandler(exchange)方法,

点进去getHandler(exchange)方法进入一个抽象类AbstractHandlerMapping

AbstractHandlerMapping
public Mono<Object> getHandler(ServerWebExchange exchange) {
        return this.getHandlerInternal(exchange).map((handler) -> {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
            }
​
            ServerHttpRequest request = exchange.getRequest();
            if (this.hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
                CorsConfiguration config = this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(exchange) : null;
                CorsConfiguration handlerConfig = this.getCorsConfiguration(handler, exchange);
                config = config != null ? config.combine(handlerConfig) : handlerConfig;
                if (config != null) {
                    config.validateAllowCredentials();
                }
​
                if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
                    return NO_OP_HANDLER;
                }
            }
​
            return handler;
        });
    }

类中的getHandler(exchange)方法会调getHandlerInternal(exchange)方法
在这里插入图片描述

可以看到getHandlerInternal(exchange)该方法由各个HandlerMapping自行实现,这里有6个重写了getHandlerInternal方法的类,

由于是网关组件,当请求进入时,会先判断路由,所以会进入实现类RoutePredicateHandlerMapping中。

RoutePredicateHandlerMapping
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
        if (this.managementPortType == RoutePredicateHandlerMapping.ManagementPortType.DIFFERENT && this.managementPort != null && exchange.getRequest().getURI().getPort() == this.managementPort) {
            return Mono.empty();
        } else {
            exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_HANDLER_MAPPER_ATTR, this.getSimpleName());
            return this.lookupRoute(exchange).flatMap((r) -> {
                exchange.getAttributes().remove(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR);
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Mapping [" + this.getExchangeDesc(exchange) + "] to " + r);
                }
​
                exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR, r);
                // 此处的webhandler是FilteringWebhandler
                return Mono.just(this.webHandler);
            }).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
                exchange.getAttributes().remove(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR);
                if (this.logger.isTraceEnabled()) {
                    this.logger.trace("No RouteDefinition found for [" + this.getExchangeDesc(exchange) + "]");
                }
​
            })));
        }
    }  
​
 protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
        return this.routeLocator.getRoutes().concatMap((route) -> {
            return Mono.just(route).filterWhen((r) -> {
                exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
                return (Publisher)r.getPredicate().apply(exchange);
            }).doOnError((e) -> {
                this.logger.error("Error applying predicate for route: " + route.getId(), e);
            }).onErrorResume((e) -> {
                return Mono.empty();
            });
        }).next().map((route) -> {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Route matched: " + route.getId());
            }
​
            this.validateRoute(route, exchange);
            return route;
        });
    }
​

lookupRoute中的this.routeLocator.getRoutes()会通过RouteDefinitionRouteLocator拿到yml配置文件中所有的路由断言工厂(Before、After、Path等等),然后把找到的路由转换成Route,再执行apply方法,进行路由匹配,判断是否允许请求通过。

找到对应Route后会返回指定的FilterWebHandler,如下代码:
在这里插入图片描述

至此对应着DispatcherHandler中的第一步获取到当前网关上下文中的多个handlerMapping.

接下来我们看DispatcherHandler中的第二步获取对应的适配器,调用对应的处理器.

进入获取对应的适配器方法 invokeHandler(exchange, handler)
在这里插入图片描述

在这里插入图片描述


请求对应的适配器是SimpleHandlerAdapter

SimpleHandlerAdapter
public class SimpleHandlerAdapter implements HandlerAdapter {
    public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
        WebHandler webHandler = (WebHandler)handler;
        Mono<Void> mono = webHandler.handle(exchange);
        return mono.then(Mono.empty());
    }
}

在这里插入图片描述

这里调用了FilteringWebHandlerhandle()方法

FilteringWebHandler
    public Mono<Void> handle(ServerWebExchange exchange) {
        Route route = (Route)exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        List<GatewayFilter> gatewayFilters = route.getFilters();
        List<GatewayFilter> combined = new ArrayList(this.globalFilters);
        combined.addAll(gatewayFilters);
        AnnotationAwareOrderComparator.sort(combined);
        if (logger.isDebugEnabled()) {
            logger.debug("Sorted gatewayFilterFactories: " + combined);
        }
​
        return (new DefaultGatewayFilterChain(combined)).filter(exchange);
    }

在这里插入图片描述

这里面组装了所有使用到的过滤器,使用责任链的设计模式去实现调用,过滤器按照一定顺序排序,order值越小越先执行,

我们看RouteToRequestUrlFilterForwardRoutingFilter过滤器.

RouteToRequestUrlFilter:路由转换路由转换,把http://localhost:8888/order/test0 —> lb://mall-order/order/test0

ForwardRoutingFilter:转发路由网关过滤器。其根据 forward:// 前缀( Scheme )过滤处理,将请求转发到当前网关实例本地接口。

RouteToRequestUrlFilter
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 从exchange中取路由Route对象
        Route route = (Route)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        if (route == null) {
            return chain.filter(exchange);
        } else {
            log.trace("RouteToRequestUrlFilter start");
            // 取当前请求uri:http://localhost:8888/order/test0
            URI uri = exchange.getRequest().getURI();
            boolean encoded = ServerWebExchangeUtils.containsEncodedParts(uri);
            // 路由对象中保存的uri,也就是我们在yml文件中配置的值:   lb://mall-order
            URI routeUri = route.getUri();
            if (hasAnotherScheme(routeUri)) {
                exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR, routeUri.getScheme());
                routeUri = URI.create(routeUri.getSchemeSpecificPart());
            }
​
            if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) {
                throw new IllegalStateException("Invalid host: " + routeUri.toString());
            } else {
                // 转换结果为:lb://mall-order/order/test0
                URI mergedUrl = UriComponentsBuilder.fromUri(uri).scheme(routeUri.getScheme()).host(routeUri.getHost()).port(routeUri.getPort()).build(encoded).toUri();
                exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, mergedUrl);
                return chain.filter(exchange);
            }
        }
    }

再往后就是通过NettyRoutingFilter发起远程调用

NettyRoutingFilter

SpringCloud在实现对后端服务远程调用是基于Netty发送Http请求实现,核心代码在NettyRoutingFilter.filter()中,其中核心代码为send()方法

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI requestUrl = (URI)exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        String scheme = requestUrl.getScheme();
        if (!ServerWebExchangeUtils.isAlreadyRouted(exchange) && ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme))) {
            ServerWebExchangeUtils.setAlreadyRouted(exchange);
            ServerHttpRequest request = exchange.getRequest();
            HttpMethod method = HttpMethod.valueOf(request.getMethodValue());
            String url = requestUrl.toASCIIString();
            HttpHeaders filtered = HttpHeadersFilter.filterRequest(this.getHeadersFilters(), exchange);
            DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders();
            filtered.forEach(httpHeaders::set);
            boolean preserveHost = (Boolean)exchange.getAttributeOrDefault(ServerWebExchangeUtils.PRESERVE_HOST_HEADER_ATTRIBUTE, false);
            Route route = (Route)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
            Flux<HttpClientResponse> responseFlux = ((HttpClient.RequestSender)this.getHttpClient(route, exchange).headers((headers) -> {
                headers.add(httpHeaders);
                headers.remove("Host");
                if (preserveHost) {
                    String host = request.getHeaders().getFirst("Host");
                    headers.add("Host", host);
                }
​
            }).request(method).uri(url)).send((req, nettyOutbound) -> {
                if (log.isTraceEnabled()) {
                    nettyOutbound.withConnection((connection) -> {
                        log.trace("outbound route: " + connection.channel().id().asShortText() + ", inbound: " + exchange.getLogPrefix());
                    });
                }
​
                return nettyOutbound.send(request.getBody().map(this::getByteBuf));
            }).responseConnection((res, connection) -> {
                exchange.getAttributes().put(ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR, res);
                exchange.getAttributes().put(ServerWebExchangeUtils.CLIENT_RESPONSE_CONN_ATTR, connection);
                ServerHttpResponse response = exchange.getResponse();
                HttpHeaders headers = new HttpHeaders();
                res.responseHeaders().forEach((entry) -> {
                    headers.add((String)entry.getKey(), (String)entry.getValue());
                });
                String contentTypeValue = headers.getFirst("Content-Type");
                if (StringUtils.hasLength(contentTypeValue)) {
                    exchange.getAttributes().put("original_response_content_type", contentTypeValue);
                }
​
                this.setResponseStatus(res, response);
                HttpHeaders filteredResponseHeaders = HttpHeadersFilter.filter(this.getHeadersFilters(), headers, exchange, Type.RESPONSE);
                if (!filteredResponseHeaders.containsKey("Transfer-Encoding") && filteredResponseHeaders.containsKey("Content-Length")) {
                    response.getHeaders().remove("Transfer-Encoding");
                }
​
                exchange.getAttributes().put(ServerWebExchangeUtils.CLIENT_RESPONSE_HEADER_NAMES, filteredResponseHeaders.keySet());
                response.getHeaders().addAll(filteredResponseHeaders);
                return Mono.just(res);
            });
            Duration responseTimeout = this.getResponseTimeout(route);
            if (responseTimeout != null) {
                responseFlux = responseFlux.timeout(responseTimeout, Mono.error(new TimeoutException("Response took longer than timeout: " + responseTimeout))).onErrorMap(TimeoutException.class, (th) -> {
                    return new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, th.getMessage(), th);
                });
            }
​
            return responseFlux.then(chain.filter(exchange));
        } else {
            return chain.filter(exchange);
        }
    }
​

上面send方法最终会调用ChannelOperations#send()方法,而该方法其实是基于了Netty实现数据发送。

至此,一次Spring Cloud Gateway请求转发源码分析结束。

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

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

相关文章

NASA数据集:DC-8 飞机上收集测量数据(冰原和永久冻土融化、雪反照率降低以及海盐气溶胶)

ARCTAS DC-8 Aircraft Merge Data 简介 ARCTAS_Merge_DC8_Aircraft_Data 是在 "从飞机和卫星收集对流层成分的北极研究 "亚轨道活动期间&#xff0c;从 DC-8 飞机上收集测量数据的各种现场仪器中预先生成的合并文件。该产品的数据收集工作已经完成。 北极是了解气…

网络编程-阻塞、非阻塞、多路复用、Selector对于accept、read、write事件监听实现详解

阻塞 理论 阻塞模式下&#xff0c;相关方法&#xff08;accept、read、write&#xff09;都会导致线程暂停。 ServerSocketChannel.accept 会在没有连接建立时让线程暂停。SocketChannel.read 会在没有数据可读时让线程暂停。阻塞的表现其实就是线程暂停了&#xff0c;暂停期…

Spring 解决bean的循环依赖

Spring循环依赖-博客园 1. 什么是循环依赖 2. 循环依赖能引发什么问题 循环依赖可能引发以下问题&#xff1a; 初始化顺序不确定&#xff1a;循环依赖导致无法确定哪个对象应该先被创建和初始化&#xff0c;从而造成初始化顺序的混乱。这可能导致错误的结果或意外的行为。死…

YouTube 创作者起诉 Nvidia 和 OpenAI

- **YouTube 创作者 David Millette**&#xff1a;一位 YouTube 创作者 David Millette 起诉了 AI 巨头 Nvidia&#xff0c;指控该公司未经许可使用他的视频来训练 AI 模型。此前不久&#xff0c;Millette 还起诉了 OpenAI&#xff0c;但并未指控这两家公司侵犯版权。 - **指控…

YOLOV8网络结构|搞懂Backbone-C2f

c2f没有改变图像的输入通道数和分辨率 传进去的和传出去的没变 n = 3*d d是模型缩放里面的depth split成两个分支:a和b 经过一个bottleneck就是加一个c 有n个bottleneck 一共是(n+2)个 学习资料:

加密创投周期进化论(下篇):失落的未来

“中心化主义收编”后时代&#xff0c;叙事枯竭怎么破&#xff1f; 作者&#xff1a;Wenser&#xff1b;编辑&#xff1a;郝方舟 出品 | Odaily星球日报&#xff08;ID&#xff1a;o-daily&#xff09; 在《加密创投周期进化论&#xff08;上篇&#xff09;&#xff1a;再造新世…

Win 10录屏也能如此专业?2024年还有3款免费工具,让你大开眼界

无论你是想捕捉游戏中的精彩瞬间&#xff0c;还是打算制作专业的教学视频&#xff0c;或者需要录制在线会议&#xff0c;Win 10都自带了很强的屏幕录制功能。今天我会告诉你怎么用Win 10自带的录屏工具&#xff0c;还会给你推荐三个好用的录屏软件&#xff0c;并且告诉你它们各…

【区块链+金融服务】农业大宗供应链线上融资平台 | FISCO BCOS应用案例

释放数据要素价值&#xff0c;FISCO BCOS 2024 应用案例征集 粮食贸易受季节性影响显著。每年的粮收季节&#xff0c;粮食收储企业会根据下游订单需求&#xff0c;从上游粮食贸易商或粮农手 里大量采购粮食&#xff0c;并分批销售给下游粮食加工企业&#xff08;面粉厂、饲料厂…

HTML—css

css概述 C S S 是 C a s c a d i n g S t y l e S h e e t s &#xff08; 级 联 样 式 表 &#xff09; 。 C S S 是 一 种 样 式 表 语 言 &#xff0c; 用 于 为 H T M L 文 档 控 制 外 观 &#xff0c; 定 义 布 局 。 例 如 &#xff0c; C S S 涉 及 字 体 、 颜 色 、…

社区帮扶对象管理系统pf

TOC springboot419社区帮扶对象管理系统pf 第1章 绪论 1.1 课题背景 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。所以各…

谷歌的高级指令有哪些

今天会分享一些组合用法&#xff0c;这样就能节省许多时间可以放在跟进客户上面&#xff08;本文只介绍谷歌的搜索指令&#xff0c;并无推广&#xff09; part one 谷歌常用的搜索引擎指令&#xff1a; 1、Inurl&#xff0c;在网址中 2、Intext&#xff0c;在网页内容中 3、…

Python使用BeautifulSoup进行网页爬虫技术详解

目录 一、BeautifulSoup简介 1.1 安装BeautifulSoup 1.2 引入BeautifulSoup 二、基本使用 2.1 实例化BeautifulSoup对象 2.2 解析HTML文档 2.2.1 查找标签 2.2.2 获取标签属性和内容 2.3 异常处理 三、进阶使用 3.1 复杂标签查找 3.1.1 CSS选择器 3.1.2 正则表达式…

【1-4】设计模式概述

目录 一.设计模式产生背景 二.软件设计模式的概念 三.学习设计模式的必要性 四.设计模式分类 一.设计模式产生背景 二.软件设计模式的概念 软件设计模式&#xff0c;又称设计模式&#xff0c;是一套被反复使用、多人知晓的、经过分类编目的、代码设计经验的总结。它描述了…

skynet的消息发送:send和call

skynet是一个轻量级的游戏服务器框架。 skynet的核心是服务&#xff0c;服务之间通过消息来通信&#xff0c;消息的来源主要有&#xff1a; 定时器网络服务之间的调用(skynet.send或skynet.call) skynet.send和skynet.call 假设我们有两个服务A和B&#xff0c;A发了两条消息…

AI+服装电商细分赛道的落地应用:图应AI模特的进化史干货篇

文章目录 AI绘制人物的效果进化史2022年2023年2024年 摄影师、设计师、模特三方在AI商拍领域的位置国家统计局的一些服装行业数据遇到的一些问题以及相应的解决方案图应AI这个产品未来可能怎么走统一回答某些投资人的一个问题 AI绘制人物的效果进化史 2022年 还记得我2022年从…

Understanding the Overheads of Launching CUDA Kernels (理解启动 CUDA Kernels 的开销)

Understanding the Overheads of Launching CUDA Kernels {理解启动 CUDA Kernels 的开销} Understanding the Overheads of Launching CUDA Kernels1. INTRODUCTION2. MICRO-BENCHMARKS USED IN OUR STUDY3. OVERHEAD OF LAUNCHING KERNELS3.1. Experimental Environment3.2. …

【Validation + i18n】✈️运行时参数校验+国际化上下文实现自定义参数校验规则

目录 &#x1f44b;前言 &#x1f440;一、环境准备 &#x1f4eb;二、代码实现 2.1 Validation 自定义验证类 2.2 自定义注解代码实现 &#x1f49e;️三、测试 &#x1f331;四、章末 &#x1f44b;前言 小伙伴们大家好&#xff0c;最近在和一位读者讨论国际化上下文工具…

SpringBoot-01-全局异常处理器

在之前的项目中每一个异常的地方都要进行处理&#xff0c;十分的麻烦。 在springBoot项目中&#xff0c;提供了全局的异常处理器&#xff0c;可能出现异常的地方直接抛出即可。 RestControllerAdvice public class GlobalException {ExceptionHandlerpublic Result<String…

Golang | Leetcode Golang题解之第342题4的幂

题目&#xff1a; 题解&#xff1a; func isPowerOfFour(n int) bool {return n > 0 && n&(n-1) 0 && n%3 1 }

【电路笔记】-桥接 T 型衰减器

桥接 T 型衰减器 文章目录 桥接 T 型衰减器1、概述2、桥接 T 型衰减器示例 13、可变桥接 T 型衰减器4、完全可调衰减器5、可切换桥接 T 型衰减器Bridged-T 衰减器是另一种电阻衰减器设计,它是标准对称 T 垫衰减器的变体。 1、概述 顾名思义,桥接 T 形衰减器具有一个额外的电…