SpringCloudAlibaba集成Gateway動態(tài)路由Nacos服務(wù)

官方git:https://github.com/spring-cloud/spring-cloud-gateway
Spring Cloud Gateway網(wǎng)關(guān)是用來代替zuul1.x作為微服務(wù)架構(gòu)中的網(wǎng)關(guān)組件,zuul1.x是最早的網(wǎng)關(guān)組件,由于使用單線程阻塞式鏈接,所以性能有問題塔鳍,gateway是搭建在webflux框架之上的響應(yīng)式網(wǎng)關(guān)服務(wù)柒爸,底層使用Netty框架作為通訊框架受裹。zuul2.x也使用了Netty斗这。性能上gateway是zuul1.x的1.5~2倍绪氛,與zuul2.x相當。
我們使用的是spring-cloud-alibaba的2021.1版本涝影,還踩了一個小小的坑枣察,下面說。

什么是gateway

我個人理解gateway就是一個業(yè)務(wù)nginx燃逻,它支持請求轉(zhuǎn)發(fā)序目,負載均衡,統(tǒng)一埋點伯襟,限流降級猿涨、安全認證等等很多功能,gateway收集所有請求根據(jù)路由規(guī)則轉(zhuǎn)發(fā)請求姆怪,使用統(tǒng)一的過濾器處理請求參數(shù)等叛赚,是不是聽起來有點像nginx,但是比nginx擁有更多功能稽揭。

gateway三大組件

image.png

如何使用

springcloud 提供了gateway的springboot版本啟動器俺附,我們可以很方便的創(chuàng)建一個網(wǎng)關(guān)項目
首先創(chuàng)建一個springboot項目,引入依賴

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

修改application.yml配置

server:
  port: 9000
spring:
  application:
    name: gateway-web
  cloud:
    gateway:
      discovery:
        routes:
        - id: demo-1
          uri: 127.0.0.1:9004
          predicates:
            - Path=/demo/** ##基于Path匹配的路由規(guī)則 還有其他的路由規(guī)則

集成Nacos 實現(xiàn)動態(tài)路由

引入依賴

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- Spring Cloud 2020 中重磅推薦的負載均衡器 Spring Cloud LoadBalancer 不引用無法實現(xiàn)動態(tài)路由  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

修改application.yml配置

server:
  port: 9000
spring:
  application:
    name: gateway-web
  cloud:
    gateway:
      discovery:
        routes:
        - id: demo-1
          uri: lb://demo-web   ##'lb://'代表發(fā)現(xiàn)注冊中心的服務(wù) 'demo-web'服務(wù)提供方的name 
          predicates:
            - Path=/demo/**
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        password: nacos
        username: nacos

啟動類增加服務(wù)發(fā)現(xiàn)注解

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayWebApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayWebApplication.class, args);
    }

}

可以自nacos中發(fā)現(xiàn)該gateway服務(wù)


image.png

基于約定的服務(wù)動態(tài)路由 自動配置routes

修改application.yml配置

server:
  port: 9000
spring:
  application:
    name: gateway-web
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true ##開啟自動發(fā)現(xiàn)
          lower-case-service-id: true ##轉(zhuǎn)小寫
#      routes:
#        - id: demo-1
#          uri: lb://demo-web
#          predicates:
#            - Path=/demo/**
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        password: nacos
        username: nacos

注意這里在訪問服務(wù)的時候需要添加一級以服務(wù)名稱為路徑的資源
比如:http://127.0.0.1:9004/demo/userinfo
要訪問 http://127.0.0.1:9004/demo-web/demo/userinfo demo-web為服務(wù)提供方的name

image.png

2021.1版本踩坑

網(wǎng)上很多教程都沒有提示需要引入spring-cloud-starter-loadbalance溪掀,可能文章都比較老事镣,loadbalance是2020版本提供的負載均衡器,這里是默認使用的揪胃,所以不引用就沒辦反實現(xiàn)動態(tài)路由璃哟,連個日志都沒有,害我debug半天喊递。
說幾個在服務(wù)轉(zhuǎn)發(fā)中幾個比較重要的組件:

DispatcherHandler 入口
@Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        if (this.handlerMappings == null) {
            return createNotFoundError();
        }
        return Flux.fromIterable(this.handlerMappings)
                .concatMap(mapping -> mapping.getHandler(exchange))
                .next()
                .switchIfEmpty(createNotFoundError())
                .flatMap(handler -> invokeHandler(exchange, handler))
                .flatMap(result -> handleResult(exchange, result));
    }
RoutePredicateHandlerMapping 負責獲取所有路由規(guī)則
    @Override
    protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
        // don't handle requests on management port if set and different than server port
        if (this.managementPortType == DIFFERENT && this.managementPort != null
                && exchange.getRequest().getURI().getPort() == this.managementPort) {
            return Mono.empty();
        }
        exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName());

        return lookupRoute(exchange)
                // .log("route-predicate-handler-mapping", Level.FINER) //name this
                .flatMap((Function<Route, Mono<?>>) r -> {
                    exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
                    if (logger.isDebugEnabled()) {
                        logger.debug("Mapping [" + getExchangeDesc(exchange) + "] to " + r);
                    }

                    exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
                    return Mono.just(webHandler);
                }).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
                    exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
                    if (logger.isTraceEnabled()) {
                        logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
                    }
                })));
    }

FilteringWebHandler 執(zhí)行過濾器鏈
    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
        List<GatewayFilter> gatewayFilters = route.getFilters();

        List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
        combined.addAll(gatewayFilters);
        // TODO: needed or cached?
        AnnotationAwareOrderComparator.sort(combined);

        if (logger.isDebugEnabled()) {
            logger.debug("Sorted gatewayFilterFactories: " + combined);
        }

        return new DefaultGatewayFilterChain(combined).filter(exchange);
    }
RouteToRequestUrlFilter 負責根據(jù)路由規(guī)則轉(zhuǎn)換url
@Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
        if (route == null) {
            return chain.filter(exchange);
        }
        log.trace("RouteToRequestUrlFilter start");
        URI uri = exchange.getRequest().getURI();
        boolean encoded = containsEncodedParts(uri);
        URI routeUri = route.getUri();

        if (hasAnotherScheme(routeUri)) {
            // this is a special url, save scheme to special attribute
            // replace routeUri with schemeSpecificPart
            exchange.getAttributes().put(GATEWAY_SCHEME_PREFIX_ATTR, routeUri.getScheme());
            routeUri = URI.create(routeUri.getSchemeSpecificPart());
        }

        if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) {
            // Load balanced URIs should always have a host. If the host is null it is
            // most
            // likely because the host name was invalid (for example included an
            // underscore)
            throw new IllegalStateException("Invalid host: " + routeUri.toString());
        }

        URI mergedUrl = UriComponentsBuilder.fromUri(uri)
                // .uri(routeUri)
                .scheme(routeUri.getScheme()).host(routeUri.getHost()).port(routeUri.getPort()).build(encoded).toUri();
        exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, mergedUrl);
        return chain.filter(exchange);
    }

ReactiveLoadBalancerClientFilter 負責對遠程分布式服務(wù)做負載均衡

以前使用的是LoadBalancerClientFilter

@Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
        if (url == null || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
            return chain.filter(exchange);
        }
        // preserve the original url
        addOriginalRequestUrl(exchange, url);

        if (log.isTraceEnabled()) {
            log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url);
        }

        URI requestUri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        String serviceId = requestUri.getHost();
        Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
                .getSupportedLifecycleProcessors(clientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
                        RequestDataContext.class, ResponseData.class, ServiceInstance.class);
        DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(new RequestDataContext(
                new RequestData(exchange.getRequest()), getHint(serviceId, loadBalancerProperties.getHint())));
        return choose(lbRequest, serviceId, supportedLifecycleProcessors).doOnNext(response -> {

            if (!response.hasServer()) {
                supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
                        .onComplete(new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, response)));
                throw NotFoundException.create(properties.isUse404(), "Unable to find instance for " + url.getHost());
            }

            ServiceInstance retrievedInstance = response.getServer();

            URI uri = exchange.getRequest().getURI();

            // if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
            // if the loadbalancer doesn't provide one.
            String overrideScheme = retrievedInstance.isSecure() ? "https" : "http";
            if (schemePrefix != null) {
                overrideScheme = url.getScheme();
            }

            DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(retrievedInstance,
                    overrideScheme);

            URI requestUrl = reconstructURI(serviceInstance, uri);

            if (log.isTraceEnabled()) {
                log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
            }
            exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
            exchange.getAttributes().put(GATEWAY_LOADBALANCER_RESPONSE_ATTR, response);
            supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, response));
        }).then(chain.filter(exchange))
                .doOnError(throwable -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
                        .onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(
                                CompletionContext.Status.FAILED, throwable, lbRequest,
                                exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR)))))
                .doOnSuccess(aVoid -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
                        .onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(
                                CompletionContext.Status.SUCCESS, lbRequest,
                                exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR),
                                new ResponseData(exchange.getResponse(), new RequestData(exchange.getRequest()))))));
    }

NettyRoutingFilter 負責通訊 使用HttpClient進行請求的發(fā)送
@Override
    @SuppressWarnings("Duplicates")
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);

        String scheme = requestUrl.getScheme();
        if (isAlreadyRouted(exchange) || (!"http".equals(scheme) && !"https".equals(scheme))) {
            return chain.filter(exchange);
        }
        setAlreadyRouted(exchange);

        ServerHttpRequest request = exchange.getRequest();

        final HttpMethod method = HttpMethod.valueOf(request.getMethodValue());
        final String url = requestUrl.toASCIIString();

        HttpHeaders filtered = filterRequest(getHeadersFilters(), exchange);

        final DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders();
        filtered.forEach(httpHeaders::set);

        boolean preserveHost = exchange.getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false);
        Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);

        Flux<HttpClientResponse> responseFlux = getHttpClient(route, exchange).headers(headers -> {
            headers.add(httpHeaders);
            // Will either be set below, or later by Netty
            headers.remove(HttpHeaders.HOST);
            if (preserveHost) {
                String host = request.getHeaders().getFirst(HttpHeaders.HOST);
                headers.add(HttpHeaders.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) -> {

            // Defer committing the response until all route filters have run
            // Put client response as ServerWebExchange attribute and write
            // response later NettyWriteResponseFilter
            exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
            exchange.getAttributes().put(CLIENT_RESPONSE_CONN_ATTR, connection);

            ServerHttpResponse response = exchange.getResponse();
            // put headers and status so filters can modify the response
            HttpHeaders headers = new HttpHeaders();

            res.responseHeaders().forEach(entry -> headers.add(entry.getKey(), entry.getValue()));

            String contentTypeValue = headers.getFirst(HttpHeaders.CONTENT_TYPE);
            if (StringUtils.hasLength(contentTypeValue)) {
                exchange.getAttributes().put(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR, contentTypeValue);
            }

            setResponseStatus(res, response);

            // make sure headers filters run after setting status so it is
            // available in response
            HttpHeaders filteredResponseHeaders = HttpHeadersFilter.filter(getHeadersFilters(), headers, exchange,
                    Type.RESPONSE);

            if (!filteredResponseHeaders.containsKey(HttpHeaders.TRANSFER_ENCODING)
                    && filteredResponseHeaders.containsKey(HttpHeaders.CONTENT_LENGTH)) {
                // It is not valid to have both the transfer-encoding header and
                // the content-length header.
                // Remove the transfer-encoding header in the response if the
                // content-length header is present.
                response.getHeaders().remove(HttpHeaders.TRANSFER_ENCODING);
            }

            exchange.getAttributes().put(CLIENT_RESPONSE_HEADER_NAMES, filteredResponseHeaders.keySet());

            response.getHeaders().putAll(filteredResponseHeaders);

            return Mono.just(res);
        });

        Duration responseTimeout = getResponseTimeout(route);
        if (responseTimeout != null) {
            responseFlux = responseFlux
                    .timeout(responseTimeout,
                            Mono.error(new TimeoutException("Response took longer than timeout: " + responseTimeout)))
                    .onErrorMap(TimeoutException.class,
                            th -> new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, th.getMessage(), th));
        }

        return responseFlux.then(chain.filter(exchange));
    }
NettyWriteResponseFilter 處理response
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // NOTICE: nothing in "pre" filter stage as CLIENT_RESPONSE_CONN_ATTR is not added
        // until the NettyRoutingFilter is run
        // @formatter:off
        return chain.filter(exchange)
                .doOnError(throwable -> cleanup(exchange))
                .then(Mono.defer(() -> {
                    Connection connection = exchange.getAttribute(CLIENT_RESPONSE_CONN_ATTR);

                    if (connection == null) {
                        return Mono.empty();
                    }
                    if (log.isTraceEnabled()) {
                        log.trace("NettyWriteResponseFilter start inbound: "
                                + connection.channel().id().asShortText() + ", outbound: "
                                + exchange.getLogPrefix());
                    }
                    ServerHttpResponse response = exchange.getResponse();

                    // TODO: needed?
                    final Flux<DataBuffer> body = connection
                            .inbound()
                            .receive()
                            .retain()
                            .map(byteBuf -> wrap(byteBuf, response));

                    MediaType contentType = null;
                    try {
                        contentType = response.getHeaders().getContentType();
                    }
                    catch (Exception e) {
                        if (log.isTraceEnabled()) {
                            log.trace("invalid media type", e);
                        }
                    }
                    return (isStreamingMediaType(contentType)
                            ? response.writeAndFlushWith(body.map(Flux::just))
                            : response.writeWith(body));
                })).doOnCancel(() -> cleanup(exchange));
        // @formatter:on
    }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末随闪,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子骚勘,更是在濱河造成了極大的恐慌铐伴,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件调鲸,死亡現(xiàn)場離奇詭異盛杰,居然都是意外死亡,警方通過查閱死者的電腦和手機藐石,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門即供,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人于微,你說我怎么就攤上這事逗嫡∏嘧裕” “怎么了?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵驱证,是天一觀的道長延窜。 經(jīng)常有香客問我,道長抹锄,這世上最難降的妖魔是什么逆瑞? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮伙单,結(jié)果婚禮上获高,老公的妹妹穿的比我還像新娘。我一直安慰自己吻育,他們只是感情好念秧,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著布疼,像睡著了一般摊趾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上游两,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天砾层,我揣著相機與錄音,去河邊找鬼器罐。 笑死梢为,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的轰坊。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼祟印,長吁一口氣:“原來是場噩夢啊……” “哼肴沫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蕴忆,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤颤芬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后套鹅,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體站蝠,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年卓鹿,在試婚紗的時候發(fā)現(xiàn)自己被綠了菱魔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡吟孙,死狀恐怖澜倦,靈堂內(nèi)的尸體忽然破棺而出聚蝶,到底是詐尸還是另有隱情,我是刑警寧澤藻治,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布碘勉,位于F島的核電站,受9級特大地震影響桩卵,放射性物質(zhì)發(fā)生泄漏验靡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一雏节、第九天 我趴在偏房一處隱蔽的房頂上張望晴叨。 院中可真熱鬧,春花似錦矾屯、人聲如沸兼蕊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽孙技。三九已至,卻和暖如春排作,著一層夾襖步出監(jiān)牢的瞬間牵啦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工妄痪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留哈雏,地道東北人。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓衫生,卻偏偏與公主長得像裳瘪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子罪针,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容