美文网首页
spring-cloud-gateway及其应用

spring-cloud-gateway及其应用

作者: 轻舞凋零 | 来源:发表于2022-10-02 11:15 被阅读0次

网关功能

  • 流量转发
  • 用户认证
  • 服务限流
  • 服务降级
  • 灰度发布

1 概述

基于springboot和spring webflux,基于netty运行,的http网关服务。

它的目标是替换奈飞的zuul。其实它和zuul2的性能差不多。选择其一即可。

考虑到spring的生态,使用spring-cloud-gateway更加有优势

2 核心概念

Route 网关的基础(断言为真时匹配到路由)

  • id
  • 目标uri
  • 断言
  • 过滤器

Predicate java8的函数,输入类型是webflux的ServerWebExchange,允许开发人员处理http请求

Filter是gateway上的过滤器,可以在请求发出前后做一些业务上的处理

整体流程

maven配置

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-gateway -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>3.0.4</version>
        </dependency>


        <!--
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
    </dependencies>

服务配置

spring:
  application:
    name: zuul-gateway-static
  cloud:
    gateway:
      routes:
        - id: test1
          uri: "http://www.example.com/"
          predicates:
            - Path=/abc/**
server:
  port: 10011
  address: 127.0.0.1
debug: true
management:
    endpoint:
      gateway:
        enabled: true
    endpoints:
      web:
        exposure:
          include: "*"
logging:
  config: classpath:logback-spring.xml



#eureka.instance.ip-address=127.0.0.1
#eureka.client.serviceUrl.defaultZone=http://tom:123456@localhost:9010/eureka/
#eureka.instance.preferIpAddress=true
#eureka.instance.instance-id=${spring.application.name}:${server.address}:${server.port}
#eureka.client.healthcheck.enabled=true
#eureka.instance.lease-expiration-duration-in-seconds=20
#eureka.instance.lease-renewal-interval-in-seconds=15

访问页面就可以看到转发的数据 http://127.0.0.1:10011/abc

spring-cloud-gateway 接入consul转发服务信息

注册consul

consul:
      discovery:
        service-name: zuul-gateway-static
        health-check-path: /health/status
        prefer-ip-address: true
        ip-address: 127.0.0.1
      host: localhost
      port: 8500

配置consul的转发信息

- id: lb-info
          uri: lb://consul-server-producer
          predicates:
            - Path=/info/**

访问转发后的地址: http://127.0.0.1:10011/info/1231211

各类断言工厂(路由判断)

path路由断言工厂

  • 配置
        - id: pathInfo
          uri: http://www.example.com/
          predicates:
            - Path=/abcd/{segment}

query路由断言工厂

  • 配置
        - id: queryInfo
          uri: http://www.example.com/
          predicates:
            - Query= foo,bb

method路由断言工厂`

  • 配置
- id: methodnfo
          uri: http://www.example.com/
          predicates:
            - Method= DELETE
  • 访问地址:
curl --location --request DELETE 'http://127.0.0.1:10011/adfasfsdfdsd'

head 路由断言工厂

  • 配置
        - id: headinfo
          uri: http://www.example.com/
          predicates:
            - Header=x-ragnar-traceid,[\w\d]+
  • 访问地址
curl --location --request GET 'http://127.0.0.1:10011/adfasfsdfdsd12312' \
--header 'x-ragnar-traceid: 123213123'

自定义路由断言工厂

自定义断言工厂代码

@Slf4j
@Component
public class GrayRoutePredicateFactory extends AbstractRoutePredicateFactory<GrayCfg> {

    public GrayRoutePredicateFactory() {
        super(GrayCfg.class);
    }

    @Override
    public Predicate<ServerWebExchange> apply(GrayCfg cfg) {

        return serverWebExchange -> {
            log.info("enter GrayRoutePredicateFactory"+cfg.isGrayStatus());
            if (cfg.isGrayStatus()) {
                log.info(" GrayRoutePredicateFactory hit   start gray");
                return true;
            }

            return false;
        };
    }
}

自定义断言工厂配置

        - id: grayinfo
          uri: http://www.baidu.com/
          predicates:
            - Path=/eee/**
            - name: Gray
              args:
                grayStatus: true

过滤器工厂(修改请求)

AddRequestHeader过滤器工厂

- AddRequestHeader=from,abc

RemoveRequestHeader过滤器工厂

- RemoveRequestHeader=from2

SetStatus过滤器工厂

- id: statusFilter
    uri: http://www.example.com/
    predicates:
    - Query=foo,b1
    filters:
    - SetStatus=401

RedirectTo过滤器工厂

- id: redictFilter
    uri: http://www.example.com/
    predicates:
    - Query=foo,b2
    filters:
    - RedirectTo=302,http://weibo.com

自定义过滤器工厂

  • 自定义过滤器配置
 - id: lb-info
    uri: lb://consul-server-producer
    predicates:
    - Path=/info/**
    filters:
    - RemoveRequestHeader=from2
    - AddRequestHeader=from,abc
    - name: Login
        args:
        checkLogin: true
  • 自定义过滤器代码@Slf4j
@Component
public class LoginGatewayFilterFactory extends AbstractGatewayFilterFactory<LoginCfg> {


public LoginGatewayFilterFactory() {

    super(LoginCfg.class);

}

@Override
public GatewayFilter apply(LoginCfg loginCfg) {

    return (exchange, chain) -> {
        log.info("if us check login:"+ loginCfg.isCheckLogin());
        ServerHttpRequest request = exchange.getRequest().mutate()
                .build();
        if (loginCfg.isCheckLogin()) {
            log.info("do login checking.......");
        }
        return chain.filter(exchange.mutate().request(request).build());
    };
}
}

全局过滤器

[图片上传失败...(image-f49665-1664766893624)]

自定义全局过滤器

package cn.beckbi.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import reactor.core.publisher.Mono;

/**
 * @program: spring-cloud
 * @description:
 * @author: bikang
 * @create: 2022-08-21 20:21
 */
@Slf4j
@Configuration
public class GlobalFilterConfig {


    @Bean
    @Order(-2)
    public GlobalFilter filter1() {
        return (exchange, chain) -> {
            log.info("filter1 pre");
            return chain.filter(
                    exchange
            ).then(
                Mono.fromRunnable(()->{
                    log.info("filter1 post");
                })
            );
        };
    }

    @Bean
    @Order(0)
    public GlobalFilter filter2() {
        return (exchange, chain) -> {
            log.info("filter2 pre");
            return chain.filter(
                    exchange
            ).then(
                    Mono.fromRunnable(()->{
                        log.info("filter2 post");
                    })
            );
        };
    }

    @Bean
    @Order(2)
    public GlobalFilter filter3() {
        return (exchange, chain) -> {
            log.info("filter3 pre");
            return chain.filter(
                    exchange
            ).then(
                    Mono.fromRunnable(()->{
                        log.info("filter3 post");
                    })
            );
        };
    }
}

自定义拦截器

package cn.beckbi.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.loadbalancer.ResponseData;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Map;
import java.util.Objects;

/**
 * @program: spring-cloud
 * @description:
 * @author: bikang
 * @create: 2022-08-21 20:29
 */
@Slf4j
@Component
public class UserFilter implements GlobalFilter, Ordered {

    @Builder
    @Data
    static class Resp {
        private int code;
        private String msg;
    }

    private static final String BAD_CID = "123";

    private ObjectMapper mapper = new ObjectMapper();

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

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();

        boolean matchFilter = false;

        if (Objects.nonNull(queryParams) && Objects.nonNull(queryParams.get("cid"))) {
            String cid = queryParams.get("cid").get(0);
            if (Objects.nonNull(cid) && BAD_CID.equals(cid)) {
                matchFilter = true;
            }
        }






        if (matchFilter) {
            ServerHttpResponse serverHttpResponse = exchange.getResponse();
            Resp resp = Resp.builder()
                    .code(401)
                    .msg("非法请求")
                    .build();
            DataBuffer dataBuffer = serverHttpResponse.bufferFactory().wrap(
                    this.getJsonBytes(resp)
            );
            serverHttpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
            serverHttpResponse.getHeaders().add("Content-Type", "application/json; charset=utf-8");


            return serverHttpResponse.writeWith(Mono.just(dataBuffer));
        }

        return chain.filter(exchange);
    }

    private byte[] getJsonBytes(Object o) {
        try {
            return mapper.writeValueAsBytes(o);
        }catch (JsonProcessingException e) {
            log.error("json error", e);
        }
        return "".getBytes();
    }


}

http://127.0.0.1:10011/info/1231212312?cid=123

限流器代码:基于redis做限流

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
package cn.beckbi.limiter;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;

import java.util.Objects;

/**
 * @program: spring-cloud
 * @description:
 * @author: bikang
 * @create: 2022-08-21 21:02
 */
@Configuration
public class LimiterConfig {

    @Bean("ipKeyResolver")
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(
                Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName()
        );
    }

    @Bean("cidKeyResolver")
    public KeyResolver cidKeyResolver() {
        return exchange -> Mono.just(
                Objects.requireNonNull(exchange.getRequest().getQueryParams().getFirst("cid"))
        );
    }

    @Primary
    @Bean("apiKeyResolver")
    public KeyResolver apiKeyResolver() {
        return exchange -> {
            Route route = (Route) exchange.getAttributes().get(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
            return Mono.just(
                    route.getId()+"#"+exchange.getRequest().getPath().value()
            );
        };
    }
}

限流器配置

- name: RequestRateLimiter
    args:
    redis-rate-limiter.replenishRate: 1
    redis-rate-limiter.burstCapacity: 2
    key-resolver: "#{@apiKeyResolver}"

全局路由处理器

package cn.beckbi.errorhandler;



import cn.beckbi.util.JsonUtil;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.util.annotation.Nullable;


/**
 * @program: spring-cloud
 * @description:
 * @author: bikang
 * @create: 2022-08-21 21:36
 */
@Component
@Slf4j
@Order(-1)
public class JsonHandler implements ErrorWebExceptionHandler {

    @Builder
    @Data
    static class Msg {
        int code;
        String msg;
    }
    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {

        ServerHttpResponse response = exchange.getResponse();

        if (response.isCommitted()) {
            return Mono.error(ex);
        }

        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);


        ServerHttpRequest request = exchange.getRequest();
        String rawQuery = request.getURI().getRawQuery();
        String query = StringUtils.hasText(rawQuery) ? "?" + rawQuery : "";
        String path = request.getPath() + query ;
        String message ;
        HttpStatus status = null;
        if (ex instanceof ResponseStatusException) {
            status = ((ResponseStatusException) ex).getStatus();
        }

        if (status == null){
            status = HttpStatus.INTERNAL_SERVER_ERROR;
        }

        // 通过状态码自定义异常信息
        if (status.value() >= 400 && status.value() < 500){
            message = "路由服务不可达或禁止访问!";
        }else {
            message = "路由服务异常!";
        }
        message += " path:" + path;

        Msg msg = Msg.builder().code(status.value())
                .msg(message)
                .build();


        return response
                .writeWith(Mono.fromSupplier(() -> {
                    DataBufferFactory bufferFactory = response.bufferFactory();
                    return bufferFactory.wrap(JsonUtil.getJsonBytes(msg));
                }));
    }

}

代码路径: https://github.com/beckbikang/spring-cloud/tree/main/kgateway

spring-cloud-gateway动态路由

实现了spring-cloud的动态路由,一个真正可用的网关就成型了,从应用的角度来看,这就是spring-cloud的最后的一课了

动态路由其实不难实现 RouteDefinitionRepository 接口即可。

配置

spring:
  application:
    name: zuul-gateway-dynamic
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
    consul:
      discovery:
        service-name: zuul-gateway-dynamic
        health-check-path: /health/status
        prefer-ip-address: true
        ip-address: 127.0.0.1
      host: localhost
      port: 8500
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    connect-timeout: 100ms
    timeout: 500ms
    jedis:
      pool:
        max-active: 200
        min-idle: 10
        max-idle: 20
        max-wait: 10000ms

server:
  port: 10013
  address: 127.0.0.1
debug: true
management:
    endpoint:
      gateway:
        enabled: true
    endpoints:
      web:
        exposure:
          include: "*"
logging:
  config: classpath:logback-spring.xml

代码

package cn.beckbi.route;

import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;

/**
 * @program: spring-cloud
 * @description:
 * @author: bikang
 * @create: 2022-08-22 07:47
 */
@Slf4j
@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {

    private static final String GATEWAY_ROUTE = "SPRING_CLOUD_GATEWAY_ROUTE_LIST";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        List<RouteDefinition> routeDefinitionList = new ArrayList<>();
        stringRedisTemplate.opsForHash().values(GATEWAY_ROUTE).forEach(route -> {
            routeDefinitionList.add(JSON.parseObject(route.toString(), RouteDefinition.class));
        });
        return Flux.fromIterable(routeDefinitionList);
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return route.flatMap(routeDefinition -> {
            stringRedisTemplate.opsForHash().put(GATEWAY_ROUTE, routeDefinition.getId(), JSON.toJSONString(routeDefinition));
            return Mono.empty();
        });
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeId.flatMap(id->{
            if(stringRedisTemplate.opsForHash().hasKey(GATEWAY_ROUTE, id)){
                stringRedisTemplate.opsForHash().delete(GATEWAY_ROUTE, id);
                return Mono.empty();
            }else {
                return Mono.defer(() -> Mono.error(new Exception("routeDefinition not found:" + routeId)));
            }
        });
    }
}

动态增加路由

curl --location --request POST 'http://localhost:10013/actuator/gateway/routes/test1' \
--header 'Content-Type: application/json' \
--data-raw '{"uri":"http://www.example.com/","predicates":[{"name":"Path","args":{"pattern":"/abc/**"}}],"filters":[{"name":"StripPrefix","args":{"value":1}}],"order":0}'

相关文章

网友评论

      本文标题:spring-cloud-gateway及其应用

      本文链接:https://www.haomeiwen.com/subject/eiklirtx.html