SpringCloud Gateway路由网关的学习

3 SpringCloud Gateway路由网关

3.1 SpringCloud Gateway简介

Gateway网关是我们服务的守门神,所有前端访问微服务的统一入口。Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

在SpringCloudGateway之前,SpringCloud并不自己开发网关,而是使用Netflix公司的Zuul框架,不过zuul2.0更新迭代缓慢,难以满足Spring的更新需求。于是就有了SpringCloudGateway。其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

核心功能特性

  • 请求路由
  • 集成 Hystrix 断路器
  • 权限控制
  • 限流

路由:gateway加入后,一切请求都必须先经过gateway,因此gateway就必须根据某种规则,把请求转发到某个微服务,这个过程叫做路由。

权限控制:请求经过路由时,我们可以判断请求者是否有请求资格,如果没有则进行拦截。

限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。

**网关的实质:**网关中的每一个功能都是由过滤器链完成的。

优点如下:

  • 安全 ,只有网关系统对外进行暴露,微服务可以隐藏在内网,通过防火墙保护。
  • 易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
  • 易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
  • 减少了客户端与各个微服务之间的交互次数
  • 易于统一授权。

3.2 快速入门

首先,我们来研究下Gateway的路由功能,基本步骤如下:

  1. 创建SpringBoot工程gateway_server,引入网关依赖
  2. 编写启动类
  3. 编写基础配置:服务端口,应用名称
  4. 编写路由规则
  5. 启动网关服务进行测试

3.2.1 新建工程

添加gateway依赖:


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud-demoartifactId>
        <groupId>com.itheima.sh.demogroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>gateway-serverartifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-gatewayartifactId>
        dependency>
    dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>
project>

3.2.2 编写启动类

package com.itheima.sh;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GatewayApplication {

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

3.2.3 编写配置

server:
  port: 10010 #服务端口
spring: 
  application:  
    name: gateway-server #指定服务名

3.2.4 编写路由规则

我们需要用网关来代理user-service服务,先看一下控制面板中的服务状态:

  • ip为:127.0.0.1
  • 端口为:8081

映射规则:

server:
  port: 10010 #服务端口
spring:
  application:
    name: gateway-server #指定服务名
  cloud:
    gateway:
      routes:
        - id: user-service # 当前路由的唯一标识
          uri: http://127.0.0.1:8081 # 路由的目标微服务地址
          predicates: # 断言  判断 前端浏览器发送url和当前path是否匹配
            - Path=/user/** # 按照路径匹配的规则
#        - id: order-service # 当前路由的唯一标识
#          uri: http://127.0.0.1:9081 # 路由的目标微服务地址
#          predicates: # 断言  判断 前端浏览器发送url和当前path是否匹配
#            - Path=/order/** # 按照路径匹配的规则

我们将符合Path 规则的一切请求,都代理到 uri参数指定的地址

本例中,我们将 /user/**开头的请求,代理到http://127.0.0.1:8081

3.3.5 启动测试

我们访问:http://localhost:8081/user/itcast,可以正常请求直达用户微服务

当我们访问:http://localhost:10010/user/itcast,符合/user/**的规则,因此请求被代理到http://localhost:8081/user/itcast

3.3 面向服务的路由

在刚才的路由规则中,我们把路径对应的服务地址写死了!如果同一服务有多个实例的话,这样做显然就不合理了。

我们应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,并且对服务列表进行负载均衡才对!

3.3.1 添加Eureka客户端依赖

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>

3.3.2 添加Eureka配置

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

3.3.3 修改映射配置

因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
spring:
  cloud:
    gateway:
      routes:
      - id: user-service # 当前路由的唯一标识
        uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
        predicates: # 断言
        - Path=/user/** # 按照路径匹配的规则

这里修改了uri的路由方式:

  • lb:负载均衡的协议,将来会使用Ribbon实现负载均衡
  • user-service:服务的id

3.3.4 启动测试

再次启动,这次gateway进行代理时,会利用Ribbon进行负载均衡访问,日志中可以看到使用了负载均衡器:

3.4 其它路由方式

Gateway中支持各种路由方式,看官方的目录:

主要包括:

  • After Route Predicate Factory、Before Route Predicate Factory和Between Route Predicate Factory:基于请求时间的路由规则
  • Cookie Route Predicate Factory:基于cookie值的路由匹配规则
  • Header Route Predicate Factory:基于请求头的路由匹配规则
  • Host Route Predicate Factory:基于主机名的路由匹配规则
  • Method Route Predicate Factory:基于请求方式的路由匹配规则
  • Path Route Predicate Factory:基于请求路径的路由匹配规则
  • Query Route Predicate Factory:基于请求参数的路由匹配规则
  • RemoteAddr Route Predicate Factory:基于请求者ip地址的路由匹配规则

有兴趣的可以自己查看。

3.5 局部过滤器

GatewayFilter Factories是Gateway中的局部过滤器工厂,作用于某个特定路由,允许以某种方式修改传入的HTTP请求或返回的HTTP响应。

下面我们以几个过滤器的配置为示例:

3.5.1 添加请求头

示例:AddRequestHeader GatewayFilter Factory,可以在请求中添加请求头,配置如下:

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
spring:
  cloud:
    gateway:
      routes:
      - id: user-service # 当前路由的唯一标识
        uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
        predicates: # 断言
          - Path=/user/** # 按照路径匹配的规则
        filters: # 过滤项
          - AddRequestHeader=name,itheima

其中:

  • filters:就是当前路由规则的所有过滤器配置
  • AddRequestHeader是添加一个头信息

重启后,再user-service的内部断点,查看请求头:

3.5.2 Hystrix支持

网关做请求路由转发,如果被调用的请求阻塞,需要通过Hystrix来做线程隔离和熔断,防止出现故障。

1)引入Hystrix的依赖

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>

2)定义降级的处理函数

定义一个controller,用来编写失败的处理逻辑:

package com.itheima.sh.gateway.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class FallbackController {

    @RequestMapping(value = "/fallbackTest")
    public Map<String, String> fallBackController() {
        Map<String, String> response = new HashMap<>();
        response.put("code", "502");
        response.put("msg", "服务超时");
        return response;
    }
}

3)定义降级处理规则

可以通过default-filter来配置,会作用于所有的路由规则。

server:
  port: 10010 #服务端口
spring:
  application:
    name: gateway-server #指定服务名
  cloud:
    gateway:
      routes:
        - id: user-service # 当前路由的唯一标识
          uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
#          uri: http://127.0.0.1:8081 # 路由的目标微服务地址
          predicates: # 断言
            - Path=/user/** # 按照路径匹配的规则
          filters: # 过滤项
            - AddRequestHeader=name,itheima
      default-filters: # 默认过滤项
        - name: Hystrix # 指定过滤工厂名称
          args: # 指定过滤的参数
            name: fallbackcmd  # hystrix的指令名
            fallbackUri: forward:/fallbackTest # 失败后的跳转路径
hystrix:
  command:
    default:
      execution.isolation.thread.timeoutInMilliseconds: 1000 # 失败超时时长
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  • default-filters:默认过滤项,作用于所有的路由规则
    • name:过滤工厂名称,这里指定Hystrix,意思是配置Hystrix类型
    • args:配置过滤工厂的配置
      • name:Hystrix的指令名称,用于配置例如超时时长等信息
      • fallbackUri:失败降级时的跳转路径

4)启动测试

再UserController的业务中打断点,让请求超时,查看页面的效果

3.5.3 路由前缀

3.5.3.1 问题呈现

我们之前用/user/**这样的映射路径代表user-service这个服务。因此请求user-service服务的一切路径要以/user/**开头

比如,访问:localhost:10010/user/2会被代理到:localhost:8081/user/2

现在,我们在user-service中的com.itheima.sh.user.web中定义一个新的接口:

package com.itheima.sh.user.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("address")
public class AddressController {

    @GetMapping("me")
    public String myAddress() {
        return "上海市浦东新区航头镇航头路18号传智播客";
    }
}

这个接口的路径是/address/me,并不是以/user/开头。当访问:localhost:10010/address/me时,并不符合映射路径,因此会得到404.

无论是 /user/**还是/address/**都是user-service中的一个controller路径,都不能作为网关到user-service的映射路径。

因此我们需要定义一个额外的映射路径,例如:/user-service,配置如下:

spring:
  cloud:
    gateway:
      routes:
      - id: user-service # 当前路由的唯一标识
        uri: lb://user-service # 路由的目标微服务,user-service:代表服务id
        predicates: # 断言
        - Path=/user-api/** # 按照路径匹配的规则

那么问题来了:

当我们访问:http://localhost:10010/user-api/user/itcast时,映射路径/user-service指向用户服务,会被代理到:http://localhost:8081/user-api/user/itcast.

当我们访问:http://localhost:10010/user-api/address/me时,映射路径/user-service指向用户服务,会被代理到:http://localhost:8081/user-api/address/me

这个/user-api是gateway中的映射路径,不应该被代理到微服务,怎办吧?

3.5.3.2 去除路由前缀

解决思路很简单,当我们访问http://localhost:10010/user-api/user/1时,网关利用/user-api这个映射路径匹配到了用户微服务,请求代理时,只要把/user-api这个映射路径去除不就可以了吗。

恰好有一个过滤器:StripPrefixFilterFactory可以满足我们的需求。

https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.3.RELEASE/reference/html/#the-stripprefix-gatewayfilter-factory

我们修改刚才的路由配置:

filters: # 过滤项
  - AddRequestHeader=name,itheima
  - StripPrefix=1 

此时,网关做路由的代理时,就不会把/user-service作为目标请求路径的一部分了。

也就是说,我们访问:http://localhost:10010/user-api/user/itcast,会代理到:http://localhost:8081/user/itcast

我们访问:http://localhost:10010/user-service/address/me,会代理到:http://localhost:8081/address/me

  1. 演示添加请求头
  2. 熔断器过滤器
  3. 去除前缀过滤器

3.6 全局过滤器

全局过滤器Global Filter 与局部的GatewayFilter会在运行时合并到一个过滤器链中,并且根据org.springframework.core.Ordered来排序后执行,顺序可以通过getOrder()方法或者@Order注解来指定。

3.6.1 GlobalFilter接口

来看看全局过滤器的顶级接口:

public interface GlobalFilter {

	/**
	 * Process the Web request and (optionally) delegate to the next {@code WebFilter}
	 * through the given {@link GatewayFilterChain}.
	 * @param exchange the current server exchange
	 * @param chain provides a way to delegate to the next filter
	 * @return {@code Mono} to indicate when request processing is complete
	 */
	Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

}

实现接口,就要实现其中的filter方法,在方法内部完成过滤逻辑,其中的参数包括:

  • ServerWebExchange:一个类似于Context的域对象,封装了Request、Response等服务相关的属性

  • GatewayFilterChain:过滤器链,用于放行请求到下一个过滤器

3.6.2 过滤器顺序

通过添加@Order注解,可以控制过滤器的优先级,从而决定了过滤器的执行顺序。

另外,一个过滤器的执行包括"pre""post"两个过程。在GlobalFilter.filter()方法中编写的逻辑属于pre阶段,在使用GatewayFilterChain.filter().then()的阶段,属于Post阶段。

优先级最高的过滤器,会在pre过程的第一个执行,在post过程的最后一个执行,如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kaHukSDY-1607004323827)(assets/1568470116218.png)]

我们可以在pre阶段做很多事情,诸如:

  • 登录状态判断
  • 权限校验
  • 请求限流等

3.7 自定义全局过滤器

定义过滤器只需要实现GlobalFilter即可,不过我们有多种方式来完成:

  • 方式一:定义过滤器类,实现接口

  • 方式二:通过@Configuration类结合lambda表达式

3.7.1 登录拦截案例

现在,我们通过自定义过滤器,模拟一个登录校验功能,逻辑非常简单:

  • 获取用户请求参数中的 access-token 参数
  • 判断是否为"admin"
    • 如果不是,证明未登录,拦截请求
    • 如果是,证明已经登录,放行请求

代码如下

package com.itheima.sh.gateway.filters;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @Description:
 * @Version: V1.0
 */
@Component
@Order(-1) //越小越优先
public class LoginFilter implements GlobalFilter { //, Ordered

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1 得到 request 对象
        ServerHttpRequest request = exchange.getRequest();

        // 2 从request获取请求参数 access-token
        String token = request.getQueryParams().getFirst("access-token");

        // 3 判断access-token值是否为admin
        if (!"admin".equals(token)) {  // 拦截
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        // 放行
        return chain.filter(exchange);
    }

    // 越小越优先
//    @Override
//    public int getOrder() {
//        return -1;
//    }
}

3.7.2 多过滤器演示(了解)

下面我们通过lambda表达式来定义过滤器:

package com.itheima.sh.gateway.filters;

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;

@Slf4j
@Configuration
public class FilterConfiguration {

    @Bean
    @Order(-2)
    public GlobalFilter globalFilter1(){
        return ((exchange, chain) -> {
            log.info("过滤器1的pre阶段!");
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("过滤器1的post阶段!");
            }));
        });
    }

    @Bean
    @Order(-1)
    public GlobalFilter globalFilter2(){
        return ((exchange, chain) -> {
            log.info("过滤器2的pre阶段!");
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("过滤器2的post阶段!");
            }));
        });
    }

    @Bean
    @Order(0)
    public GlobalFilter globalFilter3(){
        return ((exchange, chain) -> {
            log.info("过滤器3的pre阶段!");
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("过滤器3的post阶段!");
            }));
        });
    }
}

3.8 网关限流

网关除了请求路由、身份验证,还有一个非常重要的作用:请求限流。当系统面对高并发请求时,为了减少对业务处理服务的压力,需要在网关中对请求限流,按照一定的速率放行请求。

常见的限流算法包括:

  • 计数器算法
  • 漏桶算法
  • 令牌桶算法

算法介绍:

https://blog.csdn.net/u012441595/article/details/102483501

3.8.1 令牌桶算法原理

SpringGateway中采用的是令牌桶算法,令牌桶算法原理:

  • 准备一个令牌桶,有固定容量,一般为服务并发上限
  • 按照固定速率,生成令牌并存入令牌桶,如果桶中令牌数达到上限,就丢弃令牌。
  • 每次请求调用需要先获取令牌,只有拿到令牌,才继续执行,否则选择选择等待或者直接拒绝。

3.8.2 Gateway中限流实现

SpringCloudGateway是采用令牌桶算法,其令牌相关信息记录在redis中,因此我们需要安装redis,并引入Redis相关依赖。

1) 引入redis有关依赖:


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redis-reactiveartifactId>
dependency>

注意:这里不是普通的redis依赖,而是响应式的Redis依赖,因为SpringGateway是基于WebFlux的响应式项目。

在application.yml中配置Redis地址:

spring:
  redis:
    host: 192.168.200.129

2) 配置过滤条件key:

Gateway会在Redis中记录令牌相关信息,我们可以自己定义令牌桶的规则,例如:

  • 给不同的请求URI路径设置不同令牌桶
  • 给不同的登录用户设置不同令牌桶
  • 给不同的请求IP地址设置不同令牌桶

Redis中的一个Key和Value对就是一个令牌桶。因此Key的生成规则就是桶的定义规则。SpringCloudGateway中key的生成规则定义在KeyResolver接口中:

public interface KeyResolver {

	Mono<String> resolve(ServerWebExchange exchange);

}

这个接口中的方法返回值就是给令牌桶生成的key。API说明:

  • Mono:是一个单元素容器,用来存放令牌桶的key
  • ServerWebExchange:上下文对象,可以理解为ServletContext,可以从中获取request、response、cookie等信息

比如上面的三种令牌桶规则,生成key的方式如下:

  • 给不同的请求URI路径设置不同令牌桶,示例代码:

    return Mono.just(exchange.getRequest().getURI().getPath());// 获取请求URI
    
  • 给不同的登录用户设置不同令牌桶

    return exchange.getPrincipal().map(Principal::getName);// 获取用户
    
  • 给不同的请求IP地址设置不同令牌桶

    return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());// 获取请求者IP
    

这里我们选择最后一种,使用IP地址的令牌桶key。

我们在com.itheima.sh.ratelimit中定义一个类,配置一个KeyResolver的Bean实例:

package com.itheima.sh.ratelimit;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class IpKeyResolver implements KeyResolver {
    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }
}

3) 配置桶参数:

另外,令牌桶的参数需要通过yaml文件来配置,参数有2个

  • replenishRate:每秒钟生成令牌的速率,基本上就是每秒钟允许的最大请求数量

  • burstCapacity:令牌桶的容量,就是令牌桶中存放的最大的令牌的数量

完整配置如下:

server:
  port: 10010 #服务端口
spring:
  application:
    name: gateway-server #指定服务名
  cloud:
    gateway:
      routes:
        - id: user-service # 当前路由的唯一标识
          uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
#          uri: http://127.0.0.1:8081 # 路由的目标微服务地址
          predicates: # 断言
            - Path=/user-api/** # 按照路径匹配的规则
          filters: # 过滤项
            - AddRequestHeader=name,itheima
            - StripPrefix=1
      default-filters: # 默认过滤项
        - name: Hystrix # 指定过滤工厂名称(可以是任意过滤工厂类型)
          args: # 指定过滤的参数
            name: fallbackcmd  # hystrix的指令名
            fallbackUri: forward:/fallbackTest # 失败后的跳转路径
        - name: RequestRateLimiter #请求数限流 名字不能随便写
          args:
            key-resolver: "#{@ipKeyResolver}" # 指定一个key生成器
            redis-rate-limiter.replenishRate: 2 # 生成令牌的速率
            redis-rate-limiter.burstCapacity: 2 # 桶的容量
  redis:
    host: 192.168.200.129
hystrix:
  command:
    default:
      execution.isolation.thread.timeoutInMilliseconds: 1000 # 失败超时时长
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

这里配置了一个过滤器:RequestRateLimiter,并设置了三个参数:

  • key-resolver"#{@ipKeyResolver}"是SpEL表达式,写法是#{@bean的名称},ipKeyResolver就是我们定义的Bean名称

  • redis-rate-limiter.replenishRate:每秒钟生成令牌的速率

  • redis-rate-limiter.burstCapacity:令牌桶的容量

这样的限流配置可以达成的效果:

  • 每一个IP地址,每秒钟最多发起2次请求
  • 每秒钟超过2次请求,则返回429的异常状态码

4)测试:

我们快速在浏览器多次访问http://localhost:10010/user-api/user/itcast,就会得到一个错误:

429:代表请求次数过多,触发限流了。

3.9 跨域配置

跨域:浏览器的同源策略。网络通信的三要素:IP、PORT、协议,只要是三要素中的一个不相同,则会触发浏览器同源策略,出现跨域现象。

例如:

前端:http://192.168.1.2:8080

后端:http://192.168.1.2:8080/user

不会出现跨域


前端:http://192.168.1.2:8081

后端:http://192.168.1.2:8080/user

会出现跨域,端口不同


前端:http://192.168.100.2:8080

后端:http://192.168.1.2:8080/user

会出现跨域,IP地址不同


前端:https://192.168.1.2:8080 源

后端:http://192.168.1.2:8080/user 目标

会出现跨域,协议不同

有时候,我们需要对所有微服务跨域请求进行处理,则可以在gateway中进行跨域支持。修改application.yml,添加如下代码:

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有请求
              allowedOrigins: "*" #跨域处理 允许所有的域
              allowedMethods: # 支持的方法
                - GET
                - POST
                - PUT
                - DELETE

你可能感兴趣的:(springCloud,spring)