前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >Java学习笔记-微服务(6)-网关

Java学习笔记-微服务(6)-网关

原创
作者头像
咸鱼程序员
发布2025-03-07 14:21:41
发布2025-03-07 14:21:41
810
举报

Gateway

Gateway 是在 Spring 生态系统之上构建的 API 服务网关,基于 Spring6、SpringBoot3、ProjectReactor 等技术,它旨在位微服务架构提供一种简单的有效的统一的 API 路由管理方式,并为他们提供跨领域的关注点,例如安全性、监控\度量和回复能力。

微服务架构中,首先是外部服务的访问。访问通过负载均衡配置,经过网关到达各个微服务。

Gateway 在项目中的作用点:反向代理、鉴权、流量控制、熔断、日志监控。

Spring Cloud Gateway 组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发到对应的微服务,Gateway 是加载整个微服务最前沿的防火墙和代理器,隐藏微服务节点的 IP 端口信息,从而加强安全保护。Gateway 自身也是一个微服务,需要注册进服务注册中心。

Gateway 三大核心

路由 Route、断言 Predicate、过滤 Filter

Route:路由是构建网关的基本模块,它由 ID,目标 URI,一系列的断言和过滤器组成,如果断言为 true 则匹配该路由。

Predicate:断言参考 Java 中的 Predicate,开发人员可以匹配 HTTP 请求中的所有内容,如果请求与断言匹配则进行路由。

Filter:过滤器指的是 Spring 框架中 GatewayFilter 的实例,使用过滤器可以在请求被路由前或路由后对请求进行修改。

Gateway 工作流程

客户端向 Gateway 发送请求,然后再 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到具体的服务模块执行业务逻辑,然后返回。

过滤器之间可能会在发送代理请求之前(Pre)或之后(Post)执行业务逻辑。

Pre 的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等。

Post 的过滤器可以做相应内容、响应头修改、日志输出、流量监控等。

核心逻辑:路由转发、断言判断、过滤器链。

Gateway 使用配置

在父工程下创建 cloud-gateway子工程。修改子工程的 pom 文件,添加如下依赖项

代码语言:xml
复制
    <dependencies>
        <!--gateway-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--consul-discovery-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!--actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <!--若不加版本号报错,则添加版本号和SpringBoot版本号保持一致-->
                <version>3.2.0</version>
            </plugin>
        </plugins>
    </build>

添加 application.yml

代码语言:yml
复制
server:
  port: 9527

spring:
  application:
    name: cloud-gateway

  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}

修改启动类

代码语言:java
复制
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

启动测试一下Gateway是否注册进注册中心,若未能成功注册,按照前文内容排查问题。

Gateway 实现路由映射

那么 Gateway 是如何实现路由映射的呢?我们使用服务方模块做验证。

首先在服务方模块新增一个 Controller。

代码语言:java
复制
@RestController
public class PayGatewayController {

    @Resource
    private PayService payService;

    @PostMapping("pay/gateway/get")
    public ResultVO getById(@RequestBody PayDTO payDTO) {
        // 内容自行调整, 此处仅作展示
        Pay pay = new Pay();
        BeanUtil.copyProperties(payDTO, pay);
        QueryWrapper<Pay> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("id", pay.getId());
        Pay payResult = payService.getOne(queryWrapper);
        BeanUtil.copyProperties(payResult, payDTO);
        return new ResultVO<>(200, "查询成功", payDTO);
    }

    @PostMapping("pay/gateway/info")
    public ResultVO info() {
        return new ResultVO(200, "gateway success", null);
    }
}

在服务方模块的 application.yml cloud 下添加配置

代码语言:yml
复制
    gateway:
      routes:
        - id: pay_routh1 # 路由的id,没有固定规则但要求唯一,建议配置服务名
          uri: http://localhost:8001 # 路由的转发目标地址,也就是服务方模块的地址
          predicates:
            - Path=/pay/gateway/get/** # 路由规则,当请求路径匹配时转发到uri指定的地址

        - id: pay_routh2
          uri: http://localhost:8001
          predicates:
            - Path=/pay/gateway/info/**

修改完毕后进行连通性测试,测试访问 Gateway 端口与服务方端口均能成功访问。

按照前文的流程,通过 Gateway - 调用方进行流程验证

在 api 模块中添加访问入口

代码语言:java
复制
    /**
     * 验证 gateway
     * @param payDTO
     * @return
     */
    @PostMapping("pay/gateway/get")
    public ResultVO getById(@RequestBody PayDTO payDTO);

    @PostMapping("pay/gateway/info")
    public ResultVO info();

调用方模块新增 Controller

代码语言:java
复制
@RestController
public class OrderGatewayController {

    @Resource
    private OpenFeignApi openFeignApi;
    
    @PostMapping("feign/pay/gateway/get")
    public ResultVO getById(@RequestBody PayDTO payDTO) {
        return openFeignApi.getById(payDTO);
    }
    
    @PostMapping("feign/pay/gateway/info")
    public ResultVO info() {
        return openFeignApi.info();
    }
}

再次进行连通性测试,发现请求 Gateway 端口与调用方端口均能成功访问。

但是,当请求不经过网关直接访问时,我们可以访问到数据;当网关关闭时,我们仍然可以访问到数据,这并不符合实际或不完全符合实际。此时我们就需要解决在网关关闭的情况下仍然能访问接口的问题。

对于一个模块内的环境,我们是不需要一个完整的 模块 - 网关 - 模块 的过程的。我们希望同模块内的请求可以直接访问。对于其他模块的请求,我们需要让请求经过网关来实现网关的功能。

我们通过修改 api 模块来实现

代码语言:java
复制
// 原
@Component
@FeignClient(value = "cloud-payment-service")
public interface OpenFeignApi {
......
}
// 新
@Component
@FeignClient(value = "cloud-gateway")
public interface OpenFeignApi {
......
}

当我们将 @FeignClient 的 value 更换为 Gateway 的名称时,就可以解决这个问题。原因是当网关关闭后,请求无法通过 api 模块进行转发。

通过前文的内容,我们可以通过 Gateway 网关来控制请求是否进入服务,但是上文中存在一个很大的问题。在 Gateway 模块中,我们通过映射固定的 uri 来定位了服务方模块进行请求,但这对于项目来说复用性极低,严重增加了配置难度和影响了配置的可读性,所以我们要着手解决动态配置的问题。

我们需要将 uri 替换为

代码语言:yml
复制
uri: lb://cloud-payment-service # 路由的转发目标地址

通过动态的 uri ,解决了端口固定的问题。

Gateway 三大核心进阶

路由

我们在 Gateway 官网中可以看到,路由实际上存在多个阶段。以下依次介绍

The After Route

意为在某个时间之后才允许请求通过路由,在 yml 配置的 gateway 配置 predicates 中添加 After 配置。

代码语言:yml
复制
    gateway:
      routes:
        - id: pay_routh1 # 路由的id,没有固定规则但要求唯一,建议配合服务名
          uri: lb://cloud-payment-service # 路由的转发目标地址
          predicates:
            - Path=/pay/gateway/get/** # 路由规则,当请求路径匹配时转发到uri指定的地址
            - After=2020-01-01T00:00:00.000+08:00[Asia/Shanghai] # 请求在这个时间之后才允许访问

The Before Route、The Between Route 与 The After Route 类似

The Cookie Route

Cookie 需要两个参数,Cookie Name 和正则表达式。路由规则会通过获取对应的 Cookie Name 和正则表达式来匹配,匹配成功执行路由。

代码语言:yml
复制
    gateway:
      routes:
        - id: pay_routh1 # 路由的id,没有固定规则但要求唯一,建议配合服务名
          # uri: http://localhost:8001 # 路由的转发目标地址
          uri: lb://cloud-payment-service # 路由的转发目标地址
          predicates:
            - Path=/pay/gateway/get/** # 路由规则,当请求路径匹配时转发到uri指定的地址
            - After=2020-01-01T00:00:00.000+08:00[Asia/Shanghai] # 请求在这个时间之后才允许访问
            - Cookie=username,zhangsan # 请求携带这个zhangsan cookie才允许访问

The Header Route

和 Cookie 相似,需要携带一个名称和正则表达式。

代码语言:yml
复制
    gateway:
      routes:
        - id: pay_routh1 # 路由的id,没有固定规则但要求唯一,建议配合服务名
          # uri: http://localhost:8001 # 路由的转发目标地址
          uri: lb://cloud-payment-service # 路由的转发目标地址
          predicates:
            - Path=/pay/gateway/get/** # 路由规则,当请求路径匹配时转发到uri指定的地址
            - After=2020-01-01T00:00:00.000+08:00[Asia/Shanghai] # 请求在这个时间之后才允许访问
            - Cookie=username,zhangsan # 请求携带这个zhangsan cookie才允许访问
            - Header=X-Request-Id, \d+ # 请求携带这个X-Request-Id header且值为数字才允许访问

The Host Route

接收一组参数,使用<,>分割,控制访问请求。

代码语言:yml
复制
    gateway:
      routes:
        - id: pay_routh1 # 路由的id,没有固定规则但要求唯一,建议配合服务名
          # uri: http://localhost:8001 # 路由的转发目标地址
          uri: lb://cloud-payment-service # 路由的转发目标地址
          predicates:
            - Path=/pay/gateway/get/** # 路由规则,当请求路径匹配时转发到uri指定的地址
            - After=2020-01-01T00:00:00.000+08:00[Asia/Shanghai] # 请求在这个时间之后才允许访问
            - Cookie=username,zhangsan # 请求携带这个zhangsan cookie才允许访问
            - Header=X-Request-Id, \d+ # 请求携带这个X-Request-Id header且值为数字才允许访问
            - Host=.*.com # 请求的host必须以.com结尾才允许访问

The Path Route

path 路径相匹配的才能进行路由,前文已经在使用。

The Query Route

支持传入两个参数,一个是属性名,一个是属性值,值可以是正则表达式。

代码语言:yml
复制
    gateway:
      routes:
        - id: pay_routh1 # 路由的id,没有固定规则但要求唯一,建议配合服务名
          # uri: http://localhost:8001 # 路由的转发目标地址
          uri: lb://cloud-payment-service # 路由的转发目标地址
          predicates:
            - Path=/pay/gateway/get/** # 路由规则,当请求路径匹配时转发到uri指定的地址
            - After=2020-01-01T00:00:00.000+08:00[Asia/Shanghai] # 请求在这个时间之后才允许访问
            - Cookie=username,zhangsan # 请求携带这个zhangsan cookie才允许访问
            - Header=X-Request-Id, \d+ # 请求携带这个X-Request-Id header且值为数字才允许访问
            - Host=.*.com # 请求的host必须以.com结尾才允许访问
            - Query=username, \d+ # 请求的query参数username必须存在且值为数字才允许访问

The RemoteAddr Route

需要配合 CIDR(Classless Inter-Domain Routing) 网路 IP 划分使用,使用时请了解。

代码语言:yml
复制
    gateway:
      routes:
        - id: pay_routh1 # 路由的id,没有固定规则但要求唯一,建议配合服务名
          # uri: http://localhost:8001 # 路由的转发目标地址
          uri: lb://cloud-payment-service # 路由的转发目标地址
          predicates:
            - Path=/pay/gateway/get/** # 路由规则,当请求路径匹配时转发到uri指定的地址
            - After=2020-01-01T00:00:00.000+08:00[Asia/Shanghai] # 请求在这个时间之后才允许访问
            - Cookie=username,zhangsan # 请求携带这个zhangsan cookie才允许访问
            - Header=X-Request-Id, \d+ # 请求携带这个X-Request-Id header且值为数字才允许访问
            - Host=.*.com # 请求的host必须以.com结尾才允许访问
            - Query=username, \d+ # 请求的query参数username必须存在且值为数字才允许访问
            - RemoteAddr=192.168.0.1/24 # 请求的ip地址必须在192.168.0.1/24+网段才允许访问

The Method Route

只路由匹配到的请求类型。

代码语言:yml
复制
    gateway:
      routes:
        - id: pay_routh1 # 路由的id,没有固定规则但要求唯一,建议配合服务名
          # uri: http://localhost:8001 # 路由的转发目标地址
          uri: lb://cloud-payment-service # 路由的转发目标地址
          predicates:
            - Path=/pay/gateway/get/** # 路由规则,当请求路径匹配时转发到uri指定的地址
            - After=2020-01-01T00:00:00.000+08:00[Asia/Shanghai] # 请求在这个时间之后才允许访问
            - Cookie=username,zhangsan # 请求携带这个zhangsan cookie才允许访问
            - Header=X-Request-Id, \d+ # 请求携带这个X-Request-Id header且值为数字才允许访问
            - Host=.*.com # 请求的host必须以.com结尾才允许访问
            - Query=username, \d+ # 请求的query参数username必须存在且值为数字才允许访问
            - RemoteAddr=192.168.0.1/24 # 请求的ip地址必须在192.168.0.1/24+网段才允许访问
            - Method=POST # 请求的method必须为POST才允许访问

总结一下,predicates 是为了实现一个匹配规则,让请求能够找到对应的路由进行处理,使用什么路由就添加什么路由。

自定义路由断言

自定义 XXXRoutePredicateFacroty,继承 AbstractRoutePredicateFactory<XXXRoutePredicateFacroty.Config>,参考如下内容。

代码语言:java
复制
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;

import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;

/**
 * @author salt fish
 * @function
 * 定义等级userType,按照钻、金、银和yml配置的会员等级判断是否可以访问
 */
@Component
public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<MyRoutePredicateFactory.Config> {

    public MyRoutePredicateFactory() {
        super(Config.class);
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return new Predicate<ServerWebExchange>() {
            @Override
            public boolean test(ServerWebExchange serverWebExchange)
            {
                String userType = serverWebExchange.getRequest().getQueryParams().getFirst("userType");
                if (userType == null) {
                    return false;
                }
                //如果说参数存在,就和config的数据进行比较
                if(userType.equalsIgnoreCase(config.getUserType())){
                    return true;
                }
                return false;
            }
        };
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("userType");
    }

    @Data
    @Validated
    public static class Config {
        @NotEmpty
        private String userType;

    }
}

在配置中新加入一个自定义断言,名为 My,当携带参数内容为 data 时允许通过。

代码语言:yml
复制
    gateway:
      routes:
        - id: pay_routh1 # 路由的id,没有固定规则但要求唯一,建议配合服务名
          # uri: http://localhost:8001 # 路由的转发目标地址
          uri: lb://cloud-payment-service # 路由的转发目标地址
          predicates:
            - Path=/pay/gateway/get/** # 路由规则,当请求路径匹配时转发到uri指定的地址
            - After=2020-01-01T00:00:00.000+08:00[Asia/Shanghai] # 请求在这个时间之后才允许访问
            - Cookie=username,zhangsan # 请求携带这个zhangsan cookie才允许访问
            - Header=X-Request-Id, \d+ # 请求携带这个X-Request-Id header且值为数字才允许访问
            - Host=.*.com # 请求的host必须以.com结尾才允许访问
            - Query=username, \d+ # 请求的query参数username必须存在且值为数字才允许访问
            - RemoteAddr=192.168.0.1/24 # 请求的ip地址必须在192.168.0.1/24+网段才允许访问
            - Method=POST # 请求的method必须为POST才允许访问
            - My=data # 自定义断言

过滤器

pre 和 post 分别会在请求被执行前后调用,用于修改请求和响应信息。

Gateway 过滤器能够实现请求鉴权与异常处理,记录接口调用时长统计。其中包含三种类型:

全局默认过滤器 Global Filters:Gateway 出厂默认已有的,可以直接使用,主要用于配置所有的路由。不需要再配置文件中配置,作用在所有路由上,实现 GlobalFilter 接口即可。

单一内置过滤器 GatewayFilter:也可称为网关过滤器,主要作用于单一路由或某个路由分组。

自定义过滤器;

过滤器内容过多,仅学习常用的内置过滤器。

请求头相关

RequestHeader GatewayFilter Factory

按照名称指定请求头

调用方测试代码

代码语言:java
复制
@GetMapping("pay/gateway/filter")
    public ResultVO filter(HttpServletRequest request) {
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            System.out.println(name + ":" + request.getHeader(name));
        }
        return new ResultVO(200, "gateway success", headerNames);
    }

gateway 模块新增 id 配置

代码语言:yml
复制
    - id: pay_routh3
          uri: lb://cloud-payment-service # 路由的转发目标地址
          predicates:
            - Path=/pay/gateway/filter/**
          filters:
            - AddRequestHeader=X-Request-demo1, demoValue1 # 添加请求头

连通后进行请求,可以发现控制台日志中多出如下内容,其中显示了请求头的一些信息,也显示了我们配置的 demo1 参数。

代码语言:txt
复制
user-agent:PostmanRuntime/7.29.0
accept:*/*
cache-control:no-cache
postman-token:9da2955a-db6c-408f-a1b9-25761c55d5a7
accept-encoding:gzip, deflate, br
x-request-demo1:demoValue1
forwarded:proto=http;host="localhost:9527";for="[0:0:0:0:0:0:0:1]:54462"
x-forwarded-for:0:0:0:0:0:0:0:1
x-forwarded-proto:http
x-forwarded-port:9527
x-forwarded-host:localhost:9527
host:LAPTOP-OG2C363M:8001
content-length:0

RemoveRequestHeader GetewatFilter Factory

按照名称删除请求头

yml 文件新增一条配置:

代码语言:yml
复制
- RemoveRequestHeader=X-Request-demo1 # 删除请求头

SetRequestHeader GetewatFilter Factory

修改请求头(使用时查询官网)

请求参数相关

AddRequestParameter GetewatFilter Factory

添加请求头(使用时查询官网)

RemoveRequestParameter GetewatFilter Factory

删除请求头(使用时查询官网)

返回参数相关

AddResponseHeader GetewatFilter Factory

添加返回参数(使用时查询官网)

SetResponseHeader GatewayFilter Factory

修改返回参数(使用时查询官网)

RemoveResponseHeader GatewayFilter Factory

删除返回参数(使用时查询官网)

前缀和路径相关

PrefixPath GatewayFilter Factory

为了实现 PrefixPath 过滤,我们需要对原有的配置进行一定的修改。修改 Gateway 模块的 yml 文件。

原配置

代码语言:yml
复制
......
          predicates:
            - Path=/pay/gateway/filter/**
          filters:
            # - AddRequestHeader=X-Request-demo1, demoValue1 # 添加响应头

新配置

代码语言:yml
复制
          predicates:
            - Path=/gateway/filter/**
          filters:
            PrefixPath=/pay # 前缀转发

路由被拆分为两部分拼接组成,实现前缀的统一管理。此时,我们的访问 url 就应该修改为 /gateway/filter 而不是原有的 /pay/gateway/filter。

SetPath GatewayFilter Factory

更新地址(使用时查询官网)

RedirectTo GatewayFilter Factory

重定向地址(使用时查询官网)

自定义过滤器 - 全局过滤

案例:统计接口调用耗时情况。

如果使用 aop,那么就需要对每个接口新增注解或代码,比较复杂。

在 Gateway 模块新建一个 MyGlobalFilter 并实现 GlobalFilter、Ordered

代码语言:java
复制
@Component
@Slf4j
public class MyGlobalFilter implements GlobalFilter, Ordered {

    public static final String VISIT_TIME = "visit_time";
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 统计接口访问时间
        exchange.getAttributes().put(VISIT_TIME, System.currentTimeMillis());
        // 调用完成后计算时间区间
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            Long startTime = exchange.getAttribute(VISIT_TIME);
            if (startTime != null) {
                log.info("访问接口:{},耗时:{}毫秒", exchange.getRequest().getPath(), System.currentTimeMillis() - startTime);
            }
        }));
    }

    @Override
    public int getOrder() {
        // 越小优先级越高
        return 0;
    }
}

这样我们就无需在业务代码中添加任何代码,即可实现统计一个接口的调用时长。当然,如果有数据库存储需求,也可以将 log 替换为数据库落表操作,此处仅作展示。

自定义条件过滤器

自定义条件过滤器,和自定义路由断言比较相似,都需要符合 Factory 类的规范。

首先新建 MyGatewayFilterFactory 并继承 AbstractGatewayFilterFactory。

其次新建 Config 内部类。

再重写 apply、shortcutFieldOrder,定义空构造方法并调用 super 即可。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Gateway
    • Gateway 三大核心
    • Gateway 工作流程
    • Gateway 使用配置
      • Gateway 实现路由映射
    • Gateway 三大核心进阶
      • 路由
      • 自定义路由断言
      • 过滤器
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档