1 Star 2 Fork 1

zrclass / springcloud-demo

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
Apache-2.0

SpringCloud与SpringBoot版本关系

大版本号对应关系:

Spring Boot Spring Cloud 关系
1.2.x Angel版本(天使) 兼容Spring Boot 1.2.x
1.3.x Brixton版本(布里克斯顿) 兼容Spring Boot 1.3.x,也兼容Spring Boot 1.4.x
1.4.x Camden版本(卡姆登) 兼容Spring Boot 1.4.x,也兼容Spring Boot 1.5.x
1.5.x Dalston版本(多尔斯顿) 兼容Spring Boot 1.5.x,不兼容Spring Boot 2.0.x
1.5.x Edgware版本(埃奇韦尔) 兼容Spring Boot 1.5.x,不兼容Spring Boot 2.0.x
2.0.x Finchley版本(芬奇利) 兼容Spring Boot 2.0.x,不兼容Spring Boot 1.5.x
2.1.x Greenwich版本(格林威治) 兼容Spring Boot 2.1.x
2.2.x,2.3.x Hoxton版本 兼容SpringBoot2.2.x,2.3.x
2.4.x,2.5.x 2020.x 兼容SpringBoot2.4.x,2.5.x
2.5.x,2.6.x 2021.x 兼容SpringBoot2.5.x,2.6.x
3.0.x 2022.x 兼容SpringBoot3.0.x

Eureka

一.Eureka概述

Eureka是一个基于REST的服务,主要用于AWS(Amazon Web Services 亚马逊云计算服务)云中的定位服务,以实现中间层服务器的负载平衡和故障转移在 Spring Cloud 微服务架构中通常用作注册中心, 我们称这个服务为 Eureka Server,还有一个与之交互的客户端称之为 Eureka Client .

二.Eureka架构图

img

如上图所示,其中

Eureka Server 表示服务注册中心 (Eureka服务端)

Application Service表示服务提供方 (Eureka客户端,需要在Eureka服务端注册)

Application Client 表示服务消费方 (Eureka客户端,需要在Eureka服务端注册)

Make Remote Call 表示远程调用

服务在Eureka上注册,然后每隔30秒发送心跳来更新它们的租约。如果客户端不能多次续订租约,那么它将在大约90秒内从服务器注册表中剔除。注册信息和更新被复制到集群中的所有eureka节点。来自任何区域的客户端都可以查找注册表信息(每30秒发生一次)来定位它们的服务(可能在任何区域)并进行远程调用。Eureka Client需要每30秒给Eureka Server发一次心跳,同时更新Server上最新的注册信息到本地,如果Server多次没有收到来自客户端的心跳,那么在90秒内会被Server上剔除.

img

三.Eureka客户端与服务端(注册中心)之间的通信

1.register(注册): Eureka客户端将关于运行实例的信息注册到Eureka服务器。注册发生在第一次心跳。

2.renew(更新 / 续借):Eureka客户端需要更新最新注册信息(续借),通过每30秒发送一次心跳。更新通知是为了告诉Eureka服务器实例仍然存活。如果服务器在90秒内没有看到更新,它会将实例从注册表中删除。建议不要更改更新间隔,因为服务器使用该信息来确定客户机与服务器之间的通信是否存在广泛传播的问题。

3.Fetch Registry(抓取注册信息):Eureka客户端从服务器(注册中心)获取注册表信息并在本地缓存。之后,客户端使用这些信息来查找其他服务。通过在上一个获取周期和当前获取周期之间获取增量更新,这些信息会定期更新(每30秒更新一次)。获取的时候可能返回相同的实例。Eureka客户端自动处理重复信息。

4.Cancel(取消):Eureka客户端在关机时向Eureka注册中心发送一个取消请求。这将从服务器的实例注册表中删除实例,从而有效地将实例从流量中取出.

5.Eviction (服务剔除):当 Eureka Client 和 Eureka Server 不再有心跳时,Eureka Server 会将该服务实例从服务注册列表中删除,即服务剔除。

四.Eureka自我保护机制

如果 Eureka 服务器检测到超过预期数量的注册客户端终止了连接,并且同时正在等待被驱逐,那么它们将进入自我保护模式。这样做是为了确保灾难性网络事件不会擦除eureka注册表数据,并将其向下传播到所有客户端。

任何客户端,如果连续3次心跳更新失败,那么它将被视为非正常终止,病句将被剔除。当超过当前注册实例15%的客户端都处于这种状态,那么自我保护将被开启。

举例:比如你有10个user-service节点注册到eureka-server中,这个时候挂了两台。那么正常比例就是:(10-2)/10 = 80% 。只有80%的节点正常,20%的节点不正常(超过15%的异常率)。 这个时候就会引发自我保护机制。

当自我保护开启以后,eureka服务器将停止剔除所有实例,直到:

  1. 它看到的心跳续借的数量回到了预期的阈值之上,或者
  2. 自我保护被禁用

默认情况下,自我保护是启用的,并且,默认的阈值是要大于当前注册数量的15%

五.Eureka集群原理

img

从图中可以看出 Eureka Server 集群相互之间通过 Replicate 来同步数据,相互之间不区分主节点和从节点,所有的节点都是平等的。在这种架构中,节点通过彼此互相注册来提高可用性,每个节点需要添加一个或多个有效的 serviceUrl 指向其他节点。

如果某台 Eureka Server 宕机,Eureka Client 的请求会自动切换到新的 Eureka Server 节点。当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会进行节点间复制,将请求复制到其它 Eureka Server 当前所知的所有节点中。

另外 Eureka Server 的同步遵循着一个非常简单的原则:只要有一条边将节点连接,就可以进行信息传播与同步。所以,如果存在多个节点,只需要将节点之间两两连接起来形成通路,那么其它注册中心都可以共享信息。每个 Eureka Server 同时也是 Eureka Client,多个 Eureka Server 之间通过 P2P 的方式完成服务注册表的同步。

Eureka Server 集群之间的状态是采用异步方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。

1.Eureka 分区

Eureka 提供了 Region 和 Zone 两个概念来进行分区,这两个概念均来自于亚马逊的 AWS:

  • region:可以理解为地理上的不同区域,比如亚洲地区,中国区或者深圳等等。没有具体大小的限制。根据项目具体的情况,可以自行合理划分 region。
  • zone:可以简单理解为 region 内的具体机房,比如说 region 划分为深圳,然后深圳有两个机房,就可以在此 region 之下划分出 zone1、zone2 两个 zone。

上图中的 us-east-1c、us-east-1d、us-east-1e 就代表了不同的 Zone。Zone 内的 Eureka Client 优先和 Zone 内的 Eureka Server 进行心跳同步,同样调用端优先在 Zone 内的 Eureka Server 获取服务列表,当 Zone 内的 Eureka Server 挂掉之后,才会从别的 Zone 中获取信息。

2.Eurka 保证 AP

Eureka Server 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而 Eureka Client 在向某个 Eureka 注册时,如果发现连接失败,则会自动切换至其它节点。只要有一台 Eureka Server 还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。

六.Eureka配置解析

1.EurekaServer配置解析

server:
  port: 7001
spring:
  application:
    name: service-eureka
  # 引入spring security,配置权限认证的用户名和密码
  security:
    user: #登录注册中心的用户名和密码
      name: admin
      password: admin
eureka:
  instance:
    # eureka服务端的实例名称
    # 单机 hostname: localhost
    hostname: 127.0.0.1
    instance-id: ${eureka.instance.hostname}:${server.port}
    prefer-ip-address: true
    # Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
    lease-renewal-interval-in-seconds: 30
    # Eureka服务端在收到最后一次心跳后等待时间上限 ,单位为秒(默认是90秒),超时剔除服务
    lease-expiration-duration-in-seconds: 90
  server:
    # 禁用自我保护,保证不可用服务被及时删除,默认启用为true
    enable-self-preservation: false
	# Server 清理无效节点的时间间隔,默认60000毫秒,即60秒。
    eviction-interval-timer-in-ms: 60000
    # 自我保护续约百分比,默认是0.85
    renewal-percent-threshold: 0.85
  client:
    # false表示不向注册中心注册自己
    register-with-eureka: false
    # false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要检索服务
    fetch-registry: false
    service-url:
      # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址
      # 单机 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
      defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@${eureka.instance.hostname}:${server.port}/eureka/
      # 相互注册
     # defaultZone: http://eureka7002.com:7002/eureka/

2.EurekaClient配置解析

server:
  port: 8001
spring:
  application:
    name: service-provider

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka
      # 集群版
      #defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
    # 是否注册到注册中心
    enabled: true
  instance:
    hostname: 127.0.0.1
    port: 7001
    user: admin
    password: admin
    instance-id: ${eureka.instance.hostname}:${server.port}
    # 访问路径可以显示ip地址
    prefer-ip-address: true

七.Eureka的工作流程

1、Eureka Server 启动成功,等待服务端注册。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息

2、Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务

3、Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常

4、当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例

5、单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳,则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端

6、当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保护模式

7、Eureka Client 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地

8、服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存

9、Eureka Client 获取到目标服务器信息,发起服务调用

10、Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除

这就是Eurka基本工作流程

八.同为注册中心Eureka与zookeeper比较

CAP理论指出,一个分布式系统不可能同时满足C(一致性)、A(可用性)和P(分区容错性)。由于分区容错性在是分布式系统中必须要保证的,因此我们只能在A和C之间进行权衡。在此Zookeeper保证的是CP, 而Eureka则是AP。

Zookeeper保证CP: 当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30 ~ 120s, 且选举期间整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。

Eureka保证AP:Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册或时如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。除此之外,Eureka还有一种自我保护机制,如果在15分钟内超过15%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:

​ 1.Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务

​ 2.Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)

​ 3.当网络稳定时,当前实例新的注册信息会被同步到其它节点中

因此, Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪

引用博客地址 :https://blog.csdn.net/weixin_42232931/article/details/102647679?spm=1001.2014.3001.5502

OpenFeign

前言-微服务架构设计风格

微服务设于基于RESTful架构,使用RESTful可以将愈发复杂单体应用通过HTTP请求、JSON传输数据拆分为不同的业务模块,达到服务独立部署、快速启动、模块协同开发、低耦合、代码复用、职责单一的目的,使团队间相对隔离的敏捷式开发。微服务的盛行首要解决的便是不同服务间调用的问题。

现有解决方案

  • Ribbon+RestTemplate
  • Feign
  • OpenFeign

1.Ribbon+RestTemplate是基于HTTP+TCP实现服务间通信,其原理是通过RestTemplate构造HTTP请求来完成,相当于单体应用通过HTTP调用第三方接口,是一种远程调用的实现方式。

2.Feign基于Ribbon+RestTemplate,其内部是通过JDK动态代理的方式,将对外调用的接口抽象成接口,使用远程API的Method方法实例,进行MethodHandler方法处理器分发,根据参数构造Request实例,一般是Controller实现自定义的接口,供外部调用。Feign内部实现了负载均衡,根据负载量、请求量分发至不同的服务,但目前Feign已不再维护。

3.OpenFeign是springcloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

一.OpenFeign与Feign的关系

feign不是一个中间件,feignspring cloud组件中的一个轻量级restfulhttp服务客户端。其作用是简化接口的调用,将http调用转为rpc调用,让调用远程接口像同进程应用内的接口调用一样简单。

dubborpc远程调用一样,通过动态代理实现接口的调用。feign通过封装包装请求体、发送http请求、获取接口响应结果、序列化响应结果等接口调用动作来简化接口的调用。

openfeignspring cloudfeign的基础上支持了spring mvc的注解,如@RequesMapping@GetMapping@PostMapping等。openfeign还实现与Ribbon的整合。

服务提供者只需要提供API接口,而不需要像dubbo那样需要强制使用implements实现接口,即使用fegin不要求服务提供者在Controller使用implements关键字实现接口。

二.Feign底层实现原理

openfeign通过包扫描将所有被@FeignClient注解注释的接口扫描出来,并为每个接口注册一个FeignClientFactoryBean实例。FeignClientFactoryBean是一个FactoryBean,当Spring调用FeignClientFactoryBeangetObject方法时,openfeign返回一个Feign生成的动态代理对象,拦截接口的方法执行。

feign会为代理的接口的每个方法Method都生成一个MethodHandler

​ 当为接口上的@FeignClient注解的url属性配置服务提供者的url时,其实就是不与Ribbon整合,此时由SynchronousMethodHandler实现接口方法远程同步调用,使用默认的Client实现类Default实例发起http请求。

​ 当接口上的@FeignClient注解的url属性不配置时,且会走负载均衡逻辑,也就是需要与Ribbon整合使用。这时候不再是使用默认的ClientDefault)调用接口,而是使用LoadBalancerFeignClient调用接口,由LoadBalancerFeignClient实现与Ribbon的整合。

三.Ribbon介绍及其与Feign的关系

RibbonNetflix发布的开源项目,提供在服务消费端实现负载均衡调用服务提供者,从注册中心读取所有可用的服务提供者,在客户端每次调用接口时采用如轮询负载均衡算法选出一个服务提供者调用,因此,Ribbon是一个客户端负载均衡器。

Ribbon提供多种负载均衡算法的实现、提供重试支持。Feign也提供重试支持,在SynchronousMethodHandlerinvoke方法中实现,但Feign的重试比较简单,只是向同一个服务节点发送请求,而Ribbon的失败重试是支持重新选择一个服务节点调用的,在服务提供者部署多个节点的情况下,显然Feign的重试机制意义不大。

四.Riboon原理及其与EurekaClient的关系

Ribbon远程调用的底层原理

RibbonFegin整合的桥梁是FeignLoadBalancer

  • 1、Ribbon会注册一个ILoadBalancer(默认使用实现类ZoneAwareLoadBalancer)负载均衡器,Feign通过LoadBalancerFeignClient调用FeignLoadBalancerexecuteWithLoadBalancer方法来使用RibbonILoadBalancer负载均衡器选择一个提供者节点发送http请求,实际发送请求还是OpenFeignFeignLoadBalancer发起的,Ribbon从始至终都只负责负载均衡选出一个服务节点。
  • 2、spring-cloud-netflix-ribbon的自动配置类会注册一个RibbonLoadBalancerClient,此RibbonLoadBalancerClient正是Ribbonspring cloud的负载均衡接口提供的实现类,用于实现@LoadBalancer注解语意。

Ribbon并非直接通过DiscoveryClient从注册中心获取服务的可用提供者,而是通过ServerList从注册中心获取服务提供者,ServerListDiscoveryClient不一样,ServerList不是Spring Cloud定义的接口,而是Ribbon定义的接口。以spring-cloud-kubernetes-ribbon为例,spring-cloud-kubernetes-ribbonRibbon提供ServerList的实现KubernetesServerListRibbon负责定时调用ServerListgetUpdatedListOfServers方法更新可用服务提供者。

怎么去获取可用的服务提供者节点由你自己去实现ServerList接口,并将实现的ServerList注册到Spring容器。如果不提供ServerList,那么使用的将是Ribbon提供的默认实现类ConfigurationBasedServerListConfigurationBasedServerList并不会从注册中心读取获取服务节点,而是从配置文件中读取。

如果我们使用的注册中心是Eureka,当我们在项目中添加spring-cloud-starter-netflix-eureka-client时,其实就已经往项目中导入了一个ribbon-eurekajar,由该jar包提供RibbonEureka整合所需的ServerListDiscoveryEnabledNIWSServerList

img

Ribbon默认提供了ConfigurationBasedServerList实现是ServerList,默认从配置文件中获取服务节点。 但是eureka中使用DiscoveryEnabledNIWSServerList实现了ServerList接口,通过EurekaClient来拉取目标服务信息。

@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
    return obtainServersViaDiscovery();
}

private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
    List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();

    if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
        logger.warn("EurekaClient has not been initialized yet, returning an empty list");
        return new ArrayList<DiscoveryEnabledServer>();
    }

    EurekaClient eurekaClient = eurekaClientProvider.get();
    if (vipAddresses!=null){
        for (String vipAddress : vipAddresses.split(",")) {
            // if targetRegion is null, it will be interpreted as the same region of client
            List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion); // 拉取
            ...
    }
    return serverList;
}

五.Ribbon的失败重试机制

Ribbon提供RetryHandler接口,并且默认使用DefaultLoadBalancerRetryHandlerLoadBalancerCommandsubmit方法中(在FeignLoadBalancerexecuteWithLoadBalancer方法中调用),如果配置重试次数大于0,则会调用RxJavaAPI支持重试。

public Observable<T> submit(final ServerOperation<T> operation) {
        // .......
        final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();
        final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();
        // Use the load balancer
        Observable<T> o = (server == null ? selectServer() : Observable.just(server))
                .concatMap(new Func1<Server, Observable<T>>() {
                    @Override
                    public Observable<T> call(Server server) {
                        //.......
                        // 调用相同节点的重试次数
                        if (maxRetrysSame > 0)
                            o = o.retry(retryPolicy(maxRetrysSame, true));
                        return o;
                    }
                });
        // 调用不同节点的重试次数
        if (maxRetrysNext > 0 && server == null)
            o = o.retry(retryPolicy(maxRetrysNext, false));
        return o.onErrorResumeNext(...);
    }

默认maxRetrysSame(调用相同节点的重试次数)为0,默认maxRetrysNext(调用不同节点的重试次数)为1retryPolicy方法返回的是一个判断是否重试的决策者,由该决策者决定是否需要重试(抛出的异常是否允许重试,是否达到最大重试次数)。

private Func2<Integer, Throwable, Boolean> retryPolicy(final int maxRetrys, final boolean same) {
        return new Func2<Integer, Throwable, Boolean>() {
            @Override
            public Boolean call(Integer tryCount, Throwable e) {
                if (e instanceof AbortExecutionException) {
                    return false;
                }
                // 大于最大重试次数
                if (tryCount > maxRetrys) {
                    return false;
                }
                if (e.getCause() != null && e instanceof RuntimeException) {
                    e = e.getCause();
                }
                // 调用RetryHandler判断是否重试
                return retryHandler.isRetriableException(e, same);
            }
        };
    }

六.OpenFeign架构及配置详解

OpenFeign的架构图

在这里插入图片描述

Feign本身提供了很多扩展点,例如:

  • 日志级别logLevel
  • 契约contract
  • 客户端client
  • 超时设置options
  • 编码器encoder
  • 解码器decoder
  • 拦截器requestInterceptor

这些扩展点,我们在使用原生Feign时,可以通过Feign.Builder指定,最后再通过target生成动态代理类,完成Bean注册

Feign.Builder

1. 通过配置文件配置

application.properties格式:

feign.client.config.{服务名}.{配置名} = {配置值} 

我们配置一些你可能用的上的扩展项,比如:日志级别配置契约配置超时配置编解码配置拦截器配置,如下:

# 日志级别配置
feign.client.config.default.loggerLevel = BASIC
# 契约配置
feign.client.config.default.contract = feign.Contract.Default
# 连接超时配置
feign.client.config.default.connectTimeout = 5000
# 读取超时配置
feign.client.config.default.readTimeout = 30000
# 编码器配置
feign.client.config.default.encoder = feign.jackson.JacksonEncoder
# 解码器配置
feign.client.config.default.decoder = feign.jackson.JacksonDecoder
# 拦截器配置, 是数组, 需要自定义RequestInterceptor
feign.client.config.default.requestInterceptors[0]=com.tiangang.demo.c.interceptor.MyFeignRequestInterceptor

有效范围说明

  • 全局生效:配置 {服务名}default ,如上面例子中所示
  • 局部生效:配置 {服务名}具体服务名 例如,下面的配置仅对调用service-provider服务有效。
  feign.client.config.service-provider.loggerLevel = BASIC

验证是否生效

我使用service-consumer发起调用,可以在启动service-consumer 启动服务 时,构建 动态代理前 打断点查看Feign.Builder

即在FeignClientFactoryBean.loadBalance方法的调target之前打断点:

FeignClientFactoryBean.loadBalance打断点查看Feign.Builder

配置后的Feign.Builder,确认已经按application.properties配置:

Feign.Builder查看调试属性-已配置

2. 通过Java Bean配置

通过Java代码配置的话需要定义一个配置类,例如我命名为:FeignConfig,里面定义需要配置的@Bean,与上面配置文件的配置项保持一致!为了做区分,这里将编解码器改为Gson。

public class FeignConfig {
    // 日志级别配置
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.BASIC;
    }
    // 契约配置
    @Bean
    public Contract feignContract() {
        return new Contract.Default();
    }
    // 超时配置
    @Bean
    public Request.Options options() {
        return new Request.Options(5000, 30000);
    }
    // 编解码器配置Jackson
    /*@Bean
    public Encoder encoder() {
        return new JacksonEncoder();
    }
    @Bean
    public Decoder decoder() {
        return new JacksonDecoder();
    }*/
    
    // 编解码器配置Gson
    @Bean
    public Encoder encoder() {
        return new GsonEncoder();
    }
    @Bean
    public Decoder decoder() {
        return new GsonDecoder();
    }
    // 拦截器配置
    @Bean
    public MyFeignRequestInterceptor myFeignRequestInterceptor() {
        return new MyFeignRequestInterceptor();
    }
}

有效范围说明

  • 全局生效(扫描到的所有服务)

    两种方式:

    • 1.在FeignConfig上加@Configuration注解(需要保证能扫描到)
    • 2.在启动类的@EnableFeignClients注解中配置defaultConfiguration
@EnableFeignClients(defaultConfiguration = FeignConfig.class) 
  • 局部生效(指定服务):在接口API的@FeignClient注解中配置
@FeignClient(value = "service-provider", configuration = FeignConfig.class)

七.OpenFeign配置注意事项

1.用对Http Client

1.1 feign中http client

如果不做特殊配置,OpenFeign默认使用jdk自带的HttpURLConnection,我们知道HttpURLConnection没有连接池、性能和效率比较低,如果采用默认,很可能会遇到性能问题导致系统故障。

可以采用Apache HttpClient,properties文件中增加下面配置:

feign.httpclient.enabled=true

pom文件中增加依赖:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>9.3.1</version>
</dependency>

也可以采用OkHttpClient,properties文件中增加下面配置:

feign.okhttp.enabled=true

pom文件中增加依赖:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    <version>10.2.0</version>
</dependency>
1.2 ribbon中的Http Client

通过OpenFeign作为含有注册中心的客户端时,默认使用Ribbon做负载均衡,Ribbon默认也是用jdk自带的HttpURLConnection,需要给Ribbon也设置一个Http client,比如使用okhttp,在properties文件中增加下面配置:

ribbon.okhttp.enabled=true

2:全局超时时间

OpenFeign可以设置超时时间,简单粗暴,设置一个全局的超时时间,如下:

feign.client.config.default.connectTimeout=2000
feign.client.config.default.readTimeout=60000

如果不配置超时时间,默认是连接超时10s,读超时60s,在源码feign.Request的内部类Options中定义。

这个接口设置了最大的readTimeout是60s,这个时间必须大于调用的所有外部接口的readTimeout,否则处理时间大于readTimeout的接口就会调用失败。

如下图,在一个系统中使用OpenFeign调用外部三个服务,每个服务提供两个接口,其中serviceC的一个接口需要60才能返回,那上面的readTimeout必须设置成60s。

img

但是如果serviceA出故障了,表现是接口1超过60s才能返回,这样OpenFeign只能等到读超时,如果调用这个接口的并发量很高,会大量占用连接资源直到资源耗尽系统奔溃。要防止这样的故障发生,就必须保证接口1能fail-fast。最好的做法就是给serviceC单独设置超时时间。

3.单服务设置超时时间

从上一节的讲解我们看到,需要对serviceC单独设置一个超时时间,代码如下:

feign.client.config.serviceC.connectTimeout=2000
feign.client.config.serviceC.readTimeout=60000

这个时间会覆盖第一节中默认的超时时间。但是问题又来了,serviceC中又掉了serviceD,因为serviceD的故障导致接口6发生了读超时的情况,为了不让系统奔溃,不得不对serviceC的接口5单独设置超时时间。如下图:

img

4.熔断超时时间

怎样给单个接口设置超时时间,查看网上资料,必须开启熔断,配置如下:

feign.hystrix.enabled=true

开启熔断后,就可以给单个接口配置超时了。如果调用serviceC的接口5的声明如下:

@FeignClient(value = "serviceC"configuration = FeignMultipartSupportConfig.class)
public interface ServiceCClient {
    @GetMapping("/interface5")
    String interface5(String param);
}

根据上面interface5接口的声明,在properties文件中增加如下配置:

hystrix.command.ServiceCClient#interface5(param).execution.isolation.thread.timeoutInMilliseconds=60000

网上资料说的并不准确,这个超时时间并没有起作用。为什么不生效呢?

4.1 使用feign超时

最终使用的超时时间来自于Options类。如果我们配置了feign的超时时间,会选择使用feign超时时间,下面代码在FeignClientFactoryBean类的configureUsingProperties方法:

if (config.getConnectTimeout() != null && config.getReadTimeout() != null) {
 builder.options(new Request.Options(config.getConnectTimeout(), config.getReadTimeout()));
}
4.2 使用ribbon超时

如果没有配置feign,但是配置了ribbon的超时时间,会使用ribbon的超时时间。我们看下这段源代码,FeignLoadBalancer里面的execute方法,

public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
  throws IOException {
 Request.Options options;
 if (configOverride != null) {
  RibbonProperties override = RibbonProperties.from(configOverride);
  options = new Request.Options(
    override.connectTimeout(this.connectTimeout),
    override.readTimeout(this.readTimeout));
 }
 else {
  options = new Request.Options(this.connectTimeout, this.readTimeout);
 }
 //这个request里面的client就是OkHttpClient
 Response response = request.client().execute(request.toRequest(), options);
 return new RibbonResponse(request.getUri(), response);
}
4.3 使用自定义Options

对于单个接口怎么配置超时时间,我这里给出一个方案,如果你有其他方案,欢迎探讨。我的方案是使用RestTemplate来调这个接口,单独配置超时时间,配置代码如下,这里使用OkHttpClient:

public class RestTemplateConfiguration {
 
    @Bean
    public OkHttp3ClientHttpRequestFactory okHttp3RequestFactory(){
        OkHttp3ClientHttpRequestFactory requestFactory = new OkHttp3ClientHttpRequestFactory();
        requestFactory.setConnectTimeout(2000);
        requestFactory.setReadTimeout(60000);
        return requestFactory;
    }
 
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(OkHttp3ClientHttpRequestFactory okHttp3RequestFactory){
        return new RestTemplate(okHttp3RequestFactory);
    }
}

为了使用ribbon负载均衡,上面加了@LoadBalanced

如果使用RestTemplate,就会使用OkHttp3ClientHttpRequestFactory中配置的时间。

5.ribbon超时时间

作为负载均衡,ribbon超时时间也是可以配置的,可以在properties增加下面配置:

ribbon.ConnectTimeout=2000
ribbon.ReadTimeout=11000

复制

有文章讲ribbon配置的超时时间必须要满足接口响应时间,其实不然,配置feign的超时时间就足够了,因为它可以覆盖掉ribbon的超时时间。

6.重试默认不开启

OpenFeign默认是不支持重试的,可以在源代码FeignClientsConfiguration中feignRetryer中看出。

@Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
 return Retryer.NEVER_RETRY;
}

复制

要开启重试,我们可以自定义Retryer,比如下面这行代码:

Retryer retryer = new Retryer.Default(100, 1000, 2);

复制

表示每间隔100ms,最大间隔1000ms重试一次,最大重试次数是1,因为第三个参数包含了第一次请求。

7.Ribbon重试

7.1 拉取服务列表

Ribbon默认从服务端拉取列表的时间间隔是30s,这个对优雅发布很不友好,一般我们会把这个时间改短,如下改成3s:

serviceC.ribbon.ServerListRefreshInterval=3
7.2 重试

Ribbon重试有不少需要注意的地方,这里分享4个。

1.同一实例最大重试次数,不包括首次调用,配置如下:

serviceC.ribbon.MaxAutoRetries=1

复制

这个次数不包括首次调用,配置了1,重试策略会先尝试在失败的实例上重试一次,如果失败,请求下一个实例。

2.同一个服务其他实例的最大重试次数,这里不包括第一次调用的实例。默认值为1:

serviceC.ribbon.MaxAutoRetriesNextServer=1

复制

3.是否对所有操作都重试,如果改为true,则对所有操作请求都进行重试,包括post,建议采用默认配置false。

serviceC.ribbon.OkToRetryOnAllOperations=false

复制

4.对指定的http状态码进行重试

serviceC.retryableStatusCodes=404,408,502,500

复制

8.hystrix超时

如下图:

img

hystrix默认不开启,但是如果开启了hystrix,因为hystrix是在Ribbon外面,所以超时时间需要符合下面规则:hystrix超时 >= (MaxAutoRetries + 1) * (ribbon ConnectTimeout + ribbon ReadTimeout)

如果Ribbon不重试,MaxAutoRetries=0

根据上面公式,假如我们配置熔断超时时间如下:

hystrix.command.ServiceCClient#interface5(param).execution.isolation.thread.timeoutInMilliseconds=15000
ribbon.ReadTimeout=8000

复制

这个配置是不会重试一次的。serviceA调用serviceB时,hystrix会等待Ribbon返回的结果,如果Ribbon配置了重试,hystrix会一直等待直到超时。上面的配置,因为第一次请求已经耗去了8s,剩下时间7s不够请求一次了,所以是不会进行重试的。

参考博客:https://cloud.tencent.com/developer/article/1658789

https://cloud.tencent.com/developer/article/1866274

GateWay

一. 网关的作用及背景

1.API网关的作用

  • 请求路由

在我们的系统中由于同一个接口新老两套系统都在使用,我们需要根据请求上下文将请求路由到对应的接口。

  • 统一鉴权

对于鉴权操作不涉及到业务逻辑,那么可以在网关层进行处理,不用下层到业务逻辑。

  • 统一监控

由于网关是外部服务的入口,所以我们可以在这里监控我们想要的数据,比如入参出参,链路时间。

  • 流量控制,熔断降级

对于流量控制,熔断降级非业务逻辑可以统一放到网关层。

2.GateWay产生的背景

Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用的Zuul网关; 但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关替代Zuul, 那就是SpringCloud Gateway一句话:gateway是原zuul1.x版的替代 。

SpringCloud Gateway 是 Spring Cloud 的一个全新项目,基于 Spring 5.0+Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。 为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。  zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能 

二.GateWay配置介绍

img

1.GateWay三大概念

  • Route(路由):路由是构建网关的基本模块,它由 ID、目标 URI、一系列的断言和过滤器组成,如果断言为 true 则匹配该路由
  • Predicate(断言):参考的是 Java8 中的 java.util.function.Predicate。开发人员可以匹配 HTTP 请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
  • Filter(过滤):指的是 Spring 框架中 GatewayFilter 的实例,使用过滤器,可以在请求被路由之前或之后对请求进行修改。
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: service-provider # 路由标示,必须唯一
          uri: lb://SERVICE-PROVIDER # 路由的目标地址;动态路由使用,lb://微服务名
          predicates:  # 路由断言,判断请求是否符合规则
            - Path=/provider/**  # 路径断言,判断路径是否是以/provider开头,如果是则符合
        - id: service-consumer
          uri: lb://SERVICE-CONSUMER
          predicates:
            - Path=/consumer/**
1.1 Route(路由)

gateway 中可以配置多个 Route。一个 Route 由路由 id,转发的 uri,多个 Predicates 以及多个 Filters 构成。处理请求时会按优先级排序,找到第一个满足所有 Predicates 的 Route

1.2 Predicate(断言)

Gateway中predicates配置除了Path断言工厂,还有十几个:

名称 说明 示例
After 是某个时间点后的请求 – After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before 是某个时间点之前的请求 – Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between 是某两个时间点之前的请求 – Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
Cookie 请求必须包含某些cookie – Cookie=chocolate, ch.p
Header 请求必须包含某些header – Header=X-Request-Id, \d+
Host 请求必须是访问某个host(域名) – Host=.somehost.org,.anotherhost.org
Method 请求方式必须是指定方式 – Method=GET,POST
Path 请求路径必须符合指定规则 – Path=/red/{segment},/blue/**
Query 请求参数必须包含指定参数 – Query=name, Jack或者- Query=name
RemoteAddr 请求者的ip必须是指定范围 – RemoteAddr=192.168.1.1/24
Weight 权重处理
1.3 Filter(过滤)

gateway提供了31种不同的内置路由过滤器工厂。常用的如:

名称 说明
AddRequestHeader 给当前请求添加一个请求头
RemoveRequestHeader 移除请求中的一个请求头
AddResponseHeader 给响应结果中添加一个响应头
RemoveResponseHeader 从响应结果中移除有一个响应头
RequestRateLimiter 限制请求的流量

1.2.1 添加过滤器

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: service-provider # 路由标示,必须唯一
          uri: lb://SERVICE-PROVIDER # 路由的目标地址;动态路由使用,lb://微服务名
          predicates:  # 路由断言,判断请求是否符合规则
            - Path=/provider/**  # 路径断言,判断路径是否是以/provider开头,如果是则符合
        - id: service-consumer
          uri: lb://SERVICE-CONSUMER
          predicates:
            - Path=/consumer/**
          filters: # 针对某个服务添加过滤器
            - AddRequestHeader=name,zhangsan 
      default-filters: # 全局过滤器给请求添加请求头信息
        - AddRequestHeader=name,zhangsan 
        - AddRequestHeader=age,10

1.2.2 自定义全局过滤器(鉴权)

通过实现GlobalFilter实现自定义全局过滤器

@Component
public class LoginFilter implements Ordered, GlobalFilter {

    private static final Logger logger = Logger.getLogger(LoginFilter.class.getName());

    private static final String TOKEN = "token";
    /**
     * 不拦截的urL集合
     */
    @Value("#{'${ignore.login.urls}'.split(',')}")
    public List<String> ignoreUrl;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String requestUrl = exchange.getRequest().getPath().toString();
        // 忽略url,不需要拦击的url,包括登录,验证码,license获取,版本获取
        // isIgnore=true 不拦截,isIgnore=false 拦截,判断请求url是否在不需要拦截url列表,如果在,则直接放行
        boolean isIgnore = CollectionUtil.contains(ignoreUrl, requestUrl);
        if (!isIgnore) {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();
            String token = request.getHeaders().getFirst(TOKEN);
            logger.info("token=" + token);
            if (StrUtil.isBlank(token)) {
                // 从header中获取token,判断 request中是否有token
                token = request.getQueryParams().getFirst(TOKEN);
                logger.info("token=" + token);
                if (StrUtil.isBlank(token)) {
                    final DataBuffer buffer = this.getResponseMessage(response, -1, "认证失败");
                    return response.writeWith(Mono.just(buffer));
                }
            }
            // 解析token,获取当前用户id
            // Long userId = RedisTokenUtil.fromTokenGetUserId(token);
            Long userId = 0l;
            if (userId == null) {
                logger.info("header中没有token或token错误");
                final DataBuffer buffer = this.getResponseMessage(response, -1, "认证失败");
                return response.writeWith(Mono.just(buffer));
            }
            logger.info("header中有token,放行,检查redis");
            // boolean hasUserId = RedisTokenUtil.checkLoginUserId(userId);
            boolean hasUserId = true;
            if (BooleanUtils.isFalse(hasUserId)) {
                logger.info("redis中不存在,用户登录已过期");
                final DataBuffer buffer = this.getResponseMessage(response, 50000, "尚未登录或登录已过期,请重新登录");
                return response.writeWith(Mono.just(buffer));
            }
            // 判断根据userId存入的token是否和当前不一致,不一致证明用户在其他地方登录token变更
            // boolean checkToken = RedisTokenUtil.checkLoginUserToken(userId, token);
            boolean checkToken = true;
            if (BooleanUtils.isFalse(checkToken)) {
                logger.info("redis中不存在,用户已登录");
                final DataBuffer buffer = this.getResponseMessage(response, 50000, "用户在别处登录,强制退出!");
                return response.writeWith(Mono.just(buffer));
            }
            logger.info("和redis中匹配,放行,执行接口");
            logger.info("更新redis中的过期时间");
        }

        return chain.filter(exchange);
    }

    /**
     * 过滤器执行优先级
     *
     * @return 数值越小,优先级越高
     */
    @Override
    public int getOrder() {
        return 1;
    }

    /**
     * 获取返回信息
     *
     * @return DataBuffer
     */
    private DataBuffer getResponseMessage(ServerHttpResponse response, int code, String message) {
        JSONObject messageObj = new JSONObject();
        //状态码
        messageObj.put("code", code);
        //状态信息
        messageObj.put("message", message);
        //当前时间
        messageObj.put("ctime", System.currentTimeMillis());
        byte[] bits = messageObj.toString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().add("Content-Type", "text/json;charset=UTF-8");
        return buffer;
    }
}

1.2.3 gateway中过滤器的执行顺序

  • 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
  • GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
  • 路由过滤器和defaultFilter的order由gateway指定,默认是按照声明顺序从1递增。
  • 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。

三.GateWay工作原理

1.GateWay架构原理

1.拉取注册中心的服务列表

2.获取请求地址,根据routes路由规则判断匹配所拉取的服务列表

3.路由规则匹配上了某个服务列表中的服务,负载均衡转发个对应的服务

在这里插入图片描述

2.GateWay的内部流程

image-20230724125717842

1)服务启动时:

1.服务启动时,加载配置文件中的路由配置

2.将路由配置转化为RouteDefinition (封装gateway路由属性信息的bean,即Route的bean定义)

3.RouteLocator读取RouteDefinition并将RouteDefinition 转换成Route对象( Route对象中定义并实例化了路由断言、过滤器、路由地址及路由优先级等信息 ),并提供了获取Route对象的方法

2)客户端请求到达时:

1.Netty Server监听到客户端请求,通过所有路由的路由断言,看是否匹配上路由

2.匹配上路由,封装具体路由的Handler(将路由的一些信息封装到ServerWebExchange对象中),并将对应的过滤器链加入到Handler中

3.拿到具体路由的执行Handler后,执行过滤器链,并通过路由信息转发请求到对应的服务

RoutePredicateHandlerMapping的执行顺序

通过路由定位器获取全部路由(RouteLocator)
通过路由的谓语(Predicate)过滤掉不可用的路由信息
查找到路由信息后将路由信息设置当上下文环境中(GATEWAY_ROUTE_ATTR)
返回gatway自定的webhandler(FilteringWebHandler)

img

Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

简介

SpringCloud 学习代码及文档总结,涵盖eureka,openfeign,ribbon,hystrix,gateway等组件的学习和用法及原理的总结 展开 收起
Java
Apache-2.0
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Java
1
https://gitee.com/zrclass/springcloud-demo.git
git@gitee.com:zrclass/springcloud-demo.git
zrclass
springcloud-demo
springcloud-demo
master

搜索帮助

344bd9b3 5694891 D2dac590 5694891