GateWay配置使用


是什么

  • Cloud全家桶中有個很重要的組建就是網關,在1.x版本中都是采用的Zuul網關
  • 但在2.X版本中,zuul的升級一直跳票,SpringCloud最后自己研發了一個網關替代Zuul
  • SpringCloudGateway:gateway是原zuul1.X版的替代.

image.png

  • Gateway是在Spring生態系統之上構建的APL網關服務,基於Spring5、Spring Boot2和Project Reactor等技術
  • Gateway指在提供一種簡單的有效的方式來對API進行路由,以及提供一些強大的過濾器功能,例如:熔斷、限流、重試等

image.png

  • SpringCloud Gateway是SpringCloud的一個全新項目,基於Spring 5.0+SpringBoot 2.0 和 ProjectReactor等技術開發的網關,它指在為微服務架構提供一種簡單有效的統一的API路由管理方式

  • SpringCloud Gateway作為SpringCloud生態系統中的網關,目標是替代Zuul,在SpringCloud 2.0以上版本中,沒有對新版本的Zuul 2.0以上最新高性能版本進行集成,仍然還是使用的Zuul 1.X非Reactor模式的老版本。而為了提升網關的性能,SpringCloud Gateway是基於WebFlux框架實現的,而WebFlux框架底層則使用了高性能的Reactor模式通信框架Netty

  • SpringCloud Gateway的目標提供統一的路由方式且基於Filter鏈的方式提供了網關基本的功能,例如:安全、監控/指標、和限流

  • SpringCloud Gateway使用的是WebFlux中的reactor-netty響應式編程組建,底層使用了Netty通訊框架

作用

  • 反向代理
  • 鑒權
  • 流量控制
  • 熔斷
  • 日志監控

位置

image.png

Gateway與Zuul的區別

在SpringCloud Finchley正式版之前,SpringCloud 推薦的網關是Netflix提供的Zuul

  1. Zuul 1.X,是一個基於阻塞I/O的API
  2. Zuul 1.X基於Servlet2.5使用阻塞架構它不支持任何長鏈接(如:WebSocket)Zuul的設計模式和Nginx較像,每次I/O操作都是從工作線程中選擇一個執行,請求線程被阻塞到工作線程完成,但是差別是Nginx用C++實現,Zuul用Java實現,而JVM本身會有第一次加載較慢的情況,使的Zuul的性能相對較差
  3. Zuul 2.X理念更先進,想基於Netty非阻塞和支持長連接,但SpringCloud目前還沒有整合。Zuul 2.X的性能較Zuul 1.X有較大的提升,在性能方面,根據官方提供的基准測試,SpringCloud Gateway的RPS(每秒請求數)是Zuul的1.6倍
  4. SpringCloud Gateway 建立在Spring Framework 5 、ProjectReactor和SpringBoot 2之上,使用非阻塞API
  5. SpringCloud Gateway還支持WebSocket,而且與Spring緊密集成擁有更好的開發體驗

三大核心概念

Route(路由)

  • 路由是構建網關的基本模塊,它由ID,目標URI,一系列的斷言和過濾器組成,如斷言為true則匹配該路由

Predicate(斷言)

  • 參考的是Java8的java.util.function.Predicate
  • 開發人員可以匹配HTTP請求中的所有內容(例如請求頭或請求參數),如果請求與斷言相匹配則進行路由

Filter(過濾)

  • 指的是Spring框架中GatewayFilter的實例,使用過濾器,可以在請求被路由前或者之后對請求進行修改

理論總結

image.png

  • web請求,通過一些匹配條件,定位到真正的服務節點,並在這個轉發過程的前后,進行一些精細化控制
  • predicate就是我們的匹配條件,而Filter,就可以理解為一個無所不能的攔截器,有了這兩個元素,再加上目標url,就可以實現一個具體的路由了

Gateway工作流程

2.jpg

  • 客戶端向 Spring Cloud Gateway發出請求。然后在 Gateway Handler Mapping中找到與請求相匹配的路由,將其發送到 Gateway Web Handler.
  • Handler再通過指定的過濾器鏈來將請求發送到我們實際的服務執行業務邏輯,然后返回
  • 過濾器之間用虛線分開是因為過濾器可能會在發送代理請求之前或之后執行業務邏輯
  • Filter在"pre"類型的過濾器可以做參數校驗、權限校驗、流量監控、日志輸出、協議轉換等
  • 在"post"類型的過濾器中可以做響應內容、響應頭的修改,日志的輸出,流量監控等有着非常重要的作用

項目搭建

導包

注:gateway無需導入web包

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>

注冊到Eureka

@SpringBootApplication
@EnableEurekaClient
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

指定某一台服務跳轉

修改YML文件

server:
  port: 8888
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: route1 # 路由的id,保證唯一,推薦以業務、微服務起名
          uri: http://192.168.1.2 #匹配后提供服務的路由地址 訪問192.168.1.2:8888實際執行的是192.168.1.2
          predicates:
            Path=/gateway/** # 路徑匹配
        - id: route2
          uri: http://192.168.1.2
          predicates:
            Path=/route2/** 
eureka: #注冊中心配置
  instance:
    instance-id: gateway
    prefer-ip-address: true
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://192.168.1.2:8761/eureka/

測試

  • 在client項目中創建一個方法feignGetInstance,路徑為/gateway/feignGetInstance

  • 啟動gateway及之前搭建好的eureka\service\client項目(每個項目啟動一個還是多個無所謂,本次主要測試gateway轉發)

  • 訪問http://192.168.1.2:8888/gateway/feignGetInstance(gateway項目) 就相當於請求http://192.168.1.2/gateway/feignGetInstance(client項目)

指定服務名稱跳轉

上面配置的uri是某一台服務的地址,但如果是集群,需要配置Eureka中的服務名稱

修改YML

新增如下配置

discovery:
locator:
enabled: true # 開啟從注冊中心動態創建路由的功能,利用微服務名稱進行路由

修改如下配置

​ uri: lb://CLIENT-PROJECT

server:
  port: 8888
spring:
  application:
    name: gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 開啟從注冊中心動態創建路由的功能,利用微服務名稱進行路由
      routes:
        - id: route1 # 路由的id,保證唯一,推薦以業務、微服務起名
          uri: lb://CLIENT-PROJECT #匹配后提供服務的服務名稱,從Eureka中Application一列就是
          predicates:
            Path=/gateway/** # 路徑匹配
        - id: route2
          uri: http://192.168.1.2
          predicates:
            Path=/route2/** 
eureka: #注冊中心配置
  instance:
    instance-id: gateway
    prefer-ip-address: true
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://192.168.1.2:8761/eureka/

測試

  • 啟動多個client項目

  • 訪問http://192.168.1.2:8888/gateway/feignGetInstance會自動通過Application服務名通過LB負載均衡找到對應的某服務

通過JAVA代碼配置路由規則

添加如下Bean

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(r -> r.path("/getInstanceIdByTimeOut/*")
                        .uri("lb://SERVICE-PROJECT") //也可使用http://192.168.1.2....
                        .id("java_route")
                )
                .build();
    }

測試

訪問http://192.168.1.2:8888/getInstanceIdByTimeOut/1可發現也成功跳轉

注意

path配置的路徑要有對應的方法

過濾規則配置

routes:
        - id: route1 # 路由的id,保證唯一,推薦以業務、微服務起名
          uri: lb://CLIENT-PROJECT #匹配后提供服務的路由地址 訪問192.168.1.2:8888實際執行的是192.168.1.2
          predicates: #各種過濾條件
            - Path=/gateway/** # 路徑匹配
            #- After=2020-11-13T16:35:15.064+08:00[Asia/Shanghai] #在指定時間之后才可以訪問
            #- Before=2021-11-13T16:38:15.064+08:00[Asia/Shanghai] #在指定時間之前才可以訪問
            #- Cookie=mycookie,test #有指定的cookie並且與val一致才可以訪問
            #- Header=X-Request-Id, 123 #請求頭要有X-Request-Id屬性,並且值為123才可以訪問
            #- Host=*.xxx.com #必須是某些域名才可以訪問
            #- Method=GET #必須是GET才能訪問
            #- Query=param, 123 # 要有參數名param並且值是123才能訪問
          filters:
            #- AddRequestHeader=myHeader, hval #接口方可通過request.getHeader()獲得這個值
            #- AddRequestParameter=myParameter, pval #接口方可通過request.getParameter()獲得這個值

自定義過濾器

可以攔截符合路由條件的所有請求,從而進行權限、攔截、限流等等操作,添加如下配置即可

package com.project.gateway.filter;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.DefaultClientResponse;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.*;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;

@Component
@Slf4j
public class GateWayFilter implements GlobalFilter, Ordered {
    @Override
    public int getOrder() {
        // 控制在NettyWriteResponseFilter后執行
        return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return processResponse(exchange, chain);
    }

    private Mono<Void> processResponse(ServerWebExchange exchange, GatewayFilterChain chain) {

        ServerHttpRequest request = exchange.getRequest();//獲得入參\cookies\headers....
        //List<String> headersval = request.getHeaders().get("headersval");
        String param = request.getQueryParams().getFirst("param");
        if (!StringUtils.isEmpty(param)) {
            //可在這進行校驗是否登錄、權限等等
            if(true) {
                //權限如果不符合等等邏輯,進入
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            }else{
                //權限、限流等條件正常進入
                //獲得響應值start
                ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(exchange.getResponse()) {
                    @Override
                    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                        HttpHeaders httpHeaders = new HttpHeaders();
                        //httpHeaders.add(HttpHeaders.CONTENT_TYPE, 設置一些值);
                        ResponseAdapter responseAdapter = new ResponseAdapter(body, httpHeaders);
                        DefaultClientResponse clientResponse = new DefaultClientResponse(responseAdapter, ExchangeStrategies.withDefaults());
                        Mono<String> rawBody = clientResponse.bodyToMono(String.class).map(s -> s);
                        BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(rawBody, String.class);
                        CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, exchange.getResponse().getHeaders());
                        return bodyInserter.insert(outputMessage, new BodyInserterContext())
                                .then(Mono.defer(() -> {
                                    Flux<DataBuffer> messageBody = outputMessage.getBody();
                                    Flux<DataBuffer> flux = messageBody.map(buffer -> {
                                        CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
                                        DataBufferUtils.release(buffer);
                                        // 將響應信息轉化為字符串
                                        String responseStr = charBuffer.toString();
                                        responseStr="可編輯響應值";
                                        return getDelegate().bufferFactory().wrap(responseStr.getBytes(StandardCharsets.UTF_8));

                                    });
                                    HttpHeaders headers = getDelegate().getHeaders();
                                    // 修改響應包的大小,不修改會因為包大小不同被瀏覽器丟掉
                                    flux = flux.doOnNext(data -> headers.setContentLength(data.readableByteCount()));
                                    return getDelegate().writeWith(flux);
                                }));
                    }
                };
                //獲得響應值end
                return chain.filter(exchange.mutate().response(responseDecorator).build());
            }
        }
        return chain.filter(exchange);
    }
    private class ResponseAdapter implements ClientHttpResponse {
        private final Flux<DataBuffer> flux;
        private final HttpHeaders headers;

        @SuppressWarnings("unchecked")
        private ResponseAdapter(Publisher<? extends DataBuffer> body, HttpHeaders headers) {
            this.headers = headers;
            if (body instanceof Flux) {
                flux = (Flux) body;
            } else {
                flux = ((Mono) body).flux();
            }
        }

        @Override
        public Flux<DataBuffer> getBody() {
            return flux;
        }

        @Override
        public HttpHeaders getHeaders() {
            return headers;
        }

        @Override
        public HttpStatus getStatusCode() {
            return null;
        }

        @Override
        public int getRawStatusCode() {
            return 0;
        }

        @Override
        public MultiValueMap<String, ResponseCookie> getCookies() {
            return null;
        }
    }

}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM