微服务-04-SpringCloud

1. Spring Cloud简介

Spring Cloud只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装,屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。

Spring Cloud官网:https://spring.io/projects/spring-cloud/

Spring Cloud的子项目,大致可分成两类:

  1. 一类是对现有成熟框架“Spring Boot化”的封装和抽象
  2. 另一类是开发了一部分分布式系统的基础设施的实现

常见的远程调用方式有RPC和HTTP:

  1. RPC特点:基于Socket、自定义数据格式、速度快
  2. HTTP特点:基于TCP/IP、规定数据传输格式、消息封装比较臃肿、传输速度比较慢、对服务提供和调用方式没有任何技术限定,自由灵活,更符合微服务理念
  • 微服务更加强调的是独立、自治、灵活,而RPC方式的限制较多,因此微服务框架中一般都会采用基于HTTP的Rest风格服务

2. Spring Cloud Netflix Eureka

Eureka是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。

Eureka包含两个组件:Eureka Server和Eureka Client

  1. Eureka Server提供服务注册服务,各个节点启动后,会在Eureka Server中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到
  2. Eureka Client是一个java客户端,用于简化与Eureka Server的交互,客户端同时也就是一个内置的、使用轮询(round-robin)负载算法的负载均衡器

在应用启动后,将会向Eureka Server发送心跳,默认周期为30秒,如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)。

Eureka Server之间通过复制的方式完成数据的同步,Eureka还提供了客户端缓存机制,即使所有的Eureka Server都挂掉,客户端依然可以利用缓存中的信息消费其他服务的API。

Eureka中的三个核心角色:

  1. 服务注册中心:Eureka Server端,提供服务注册发现功能
  2. 服务提供者:Eureka Client端,向注册中心注册服务,对外提供Rest风格服务
  3. 服务消费者:Eureka Client端,从注册中心发现服务

2.1 创建父工程

创建一个Maven父工程来管理依赖:

<packaging>pom</packaging>
<!--Spring Boot版本-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
</parent>
<!--Spring Cloud版本-->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.SR1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

2.2 创建eureka-server子工程

eureka-server本身就是一个微服务

eureka-server会把注册来的服务信息保存在Map中

  1. 添加eureka-server的起步依赖:

    <dependencies>
        <!--eureka-server的起步依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>
    
  2. 创建启动类,并添加@EnableEurekaServer注解:

    package com.lhp;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
    
    // 启用EurekaServer
    @EnableEurekaServer
    @SpringBootApplication
    public class EurekaServerApplication {
        public static void main(String[] args) {
            SpringApplication.run(EurekaServerApplication.class, args);
        }
    }
    
  3. 在application.yml中配置:

    server:
      port: 7000
    eureka:
      client:
        # 是否将自己注册到eureka-server中
        register-with-eureka: false
        # 是否从eureka-server中获取服务信息
        fetch-registry: false
        service-url:
          # eureka-server的注册地址
          defaultZone: http://localhost:7000/eureka
      server:
        # 每隔5秒检查一次心跳,剔除没有续约的失效服务
        eviction-interval-timer-in-ms: 5000
        # 关闭自我保护机制;自我保护模式下不会剔除任何服务实例
        enable-self-preservation: false
    spring:
      application:
        # 应用名,会在eureka中作为serviceId
        name: eureka-server
    
  4. 启动,浏览器访问查看:http://localhost:7000/

2.3 创建provider子工程

服务提供者注册服务

服务在启动时,若检测到有@EnableDiscoveryClient注解和配置信息,则会向注册中心发起注册请求,携带服务元数据信息(ip、port等)

服务注册完后,服务提供者会维持一个心跳,保存服务处于存在状态,这个称之为服务续约(renew)

当服务正常关闭时会发送服务下线的REST请求给eureka-server,eureka-server接收到请求后将该服务置为下线状态

  1. 添加eureka-client的起步依赖和web的起步依赖:

    <dependencies>
        <!--lombok的依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--mybatis的起步依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
        <!--mybatis驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
    
        <!--需要添加web的起步依赖,不然client可能注册不到eureka-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--eureka-client的起步依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
    
  2. 创建启动类,并添加@EnableDiscoveryClient注解或@EnableEurekaClient注解:

    package com.lhp;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
    
    // @EnableDiscoveryClient和@EnableEurekaClient任选其一即可
    // 开启客户端的发现功能,且注册中心只能是Eureka
    // @EnableEurekaClient
    // 开启客户端的发现功能
    @EnableDiscoveryClient
    // MapperScan会扫描指定包下的所有的接口,然后将接口的代理对象交给Spring容器
    @MapperScan(basePackages = "com.lhp.dao")
    @SpringBootApplication
    public class ProviderApplication {
        public static void main(String[] args) {
            SpringApplication.run(ProviderApplication.class, args);
        }
    }
    
  3. 在application.yml中配置:

    server:
      port: 7101
    eureka:
      client:
        service-url:
          # eureka-server的注册地址
          defaultZone: http://localhost:7000/eureka
        # 获取eureka-server中的服务列表(只读备份)的间隔时间
        registry-fetch-interval-seconds: 30
      instance:
        # 指定自己的ip
        ip-address: 127.0.0.1
        # 启用ip进行注册,而不是hostname
        prefer-ip-address: true
        # 租约过期时间,默认90seconds
        lease-expiration-duration-in-seconds: 150
        # 租期续约间隔,默认30seconds;服务超过30秒没有发生心跳,eureka-server会将服务从列表移除(前提是eureka-server关闭了自我保护)
        lease-renewal-interval-in-seconds: 30
    spring:
      application:
        # 应用名,会在eureka中作为serviceId
        name: provider
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db01?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8
        username: root
        password: 123456
    mybatis:
      # 配置mybatis映射文件的位置
      mapper-locations: classpath:com/lhp/dao/*Dao.xml
    
  4. 启动,浏览器访问查看:http://localhost:7000/

2.4 创建consumer子工程

服务消费者发现服务

服务消费者启动时,会检测是否获取服务注册信息配置;如果是,则会从eureka-server服务列表获取只读的备份,缓存到本地

  1. 添加eureka-client的起步依赖和web的起步依赖:

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    
        <!--需要添加web的起步依赖,不然client可能注册不到eureka-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--eureka-client的起步依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
    
  2. 创建启动类,并添加@EnableDiscoveryClient注解或@EnableEurekaClient注解:

    package com.lhp;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    import org.springframework.context.annotation.Bean;
    import org.springframework.web.client.RestTemplate;
    
    // 开启客户端的发现功能
    @EnableDiscoveryClient
    @SpringBootApplication
    public class ConsumerApplication {
        public static void main(String[] args) {
            SpringApplication.run(ConsumerApplication.class, args);
        }
    
        // 将RestTemplate对象放入到Spring容器
        @Bean
        public RestTemplate restTemplate() {
            return new RestTemplate();
        }
    }
    
  3. 在application.yml中配置:

    server:
      port: 7201
    eureka:
      client:
        service-url:
          # eureka-server的注册地址
          defaultZone: http://localhost:7000/eureka
        # 获取eureka-server中的服务列表(只读备份)的间隔时间
        registry-fetch-interval-seconds: 30
    spring:
      application:
        # 应用名,会在eureka中作为serviceId
        name: consumer
    
  4. 启动,浏览器访问查看:http://localhost:7000/

2.5 consumer通过eureka-server访问provider

consumer在eureka-server中获取provider的ip和port:

package com.lhp.controller;

import com.lhp.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@RestController
@RequestMapping("/movie")
public class MovieController {
    // RestTemplate是从Spring3.0开始支持的一个HTTP请求工具,它提供了常见的REST请求方案的模版
    @Autowired
    private RestTemplate restTemplate;
    // DiscoveryClient可以发现当前注册中心的服务
    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("/see")
    public Object see() {
        // 获取指定serviceId的实例集合
        List<ServiceInstance> instances = discoveryClient.getInstances("provider");
        // 获取第0个实例
        ServiceInstance instance = instances.get(0);
        // 获取实例的ip和port
        String instanceUrl = "http://" + instance.getHost() + ":" + instance.getPort() + "/user/1";

        // http://192.168.219.224:7001/user/1
        System.out.println(instanceUrl);
        User user = restTemplate.getForObject(instanceUrl, User.class);
        return user;
    }
}

3. Spring Cloud Netflix Ribbon

Ribbon是客户端负载均衡器,其负载均衡策略有(IRule实现类):

  1. com.netflix.loadbalancer.RoundRobinRule:轮询
  2. com.netflix.loadbalancer.AvailabilityFilteringRule:根据可用性筛选
  3. com.netflix.loadbalancer.WeightedResponseTimeRule:根据加权响应时间筛选
  4. com.netflix.loadbalancer.ZoneAvoidanceRule(默认):根据区域和可用性筛选,使用区域(Zone)对服务器进行分类
  5. com.netflix.loadbalancer.BestAvailableRule:忽略“短路”的服务器,并选择并发数较低的服务器
  6. com.netflix.loadbalancer.RandomRule:随机
  7. com.netflix.loadbalancer.RetryRule:重试

Ribbon使用:提前准备一个服务集群(将provider复制一份充当provider2,模拟时修改端口号)

  1. Eureka的依赖中已经有了Ribbon的依赖,因此无需再引入依赖;使用Ribbon时,只需在RestTemplate的@Bean方法上添加@LoadBalanced注解

    package com.lhp;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    import org.springframework.cloud.client.loadbalancer.LoadBalanced;
    import org.springframework.context.annotation.Bean;
    import org.springframework.web.client.RestTemplate;
    
    // 开启客户端的发现功能
    @EnableDiscoveryClient
    @SpringBootApplication
    public class ConsumerApplication {
        public static void main(String[] args) {
            SpringApplication.run(ConsumerApplication.class, args);
        }
    
        // 开启负载均衡
        @LoadBalanced
        // 将RestTemplate对象放入到Spring容器
        @Bean
        public RestTemplate restTemplate() {
            return new RestTemplate();
        }
    }
    
  2. consumer通过serviceId访问provider

    package com.lhp.controller;
    
    import com.lhp.pojo.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cloud.client.ServiceInstance;
    import org.springframework.cloud.client.discovery.DiscoveryClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.client.RestTemplate;
    
    import java.util.List;
    
    @RestController
    @RequestMapping("/movie")
    public class MovieController {
        // RestTemplate是从Spring3.0开始支持的一个HTTP请求工具,它提供了常见的REST请求方案的模版
        @Autowired
        private RestTemplate restTemplate;
    
        @GetMapping("/see")
        public Object see() {
            // 通过serviceId访问provider
            String serviceId = "provider";
            String instanceUrl = "http://" + serviceId + "/user/1";
            User user = restTemplate.getForObject(instanceUrl, User.class);
            return user;
        }
    }
    
  3. 修改对serviceId的负载均衡策略:

    server:
      port: 7201
    eureka:
      client:
        service-url:
          # eureka-server的注册地址
          defaultZone: http://localhost:7000/eureka
        # 获取eureka-server中的服务列表(只读备份)的间隔时间
        registry-fetch-interval-seconds: 30
    spring:
      application:
        # 应用名,会在eureka中作为serviceId
        name: consumer
    # 修改对serviceId的负载均衡策略,默认是轮询
    # 格式:serviceId或default.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.类
    # 若为default,则对所有服务生效
    provider:
      ribbon:
        # 随机策略
        NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    

负载均衡的过程:

跟踪LoadBalancerInterceptor类的源码:LoadBalancerInterceptor会对RestTemplate的请求进行拦截

ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution)
// 获取服务名
String serviceName = originalUri.getHost()

<T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
// 获取负载均衡器,此时负载均衡器中有serviceId的服务列表
ILoadBalancer loadBalancer = getLoadBalancer(serviceId)
// 根据负载均衡器选择出要使用的服务节点
Server server = getServer(loadBalancer, hint)

4. Spring Cloud Netflix Hystrix

服务雪崩效应是一种因“服务提供者”不可用而导致“服务消费者”不可用,并将不可用逐渐放大的过程;在微服务中,一个请求可能需要多个微服务才能实现,会形成复杂的调用链路,若链路中的某个基础服务故障,则会导致级联故障,进而造成整个系统不可用

熔断机制是应对服务雪崩效应的一种微服务链路保护机制,熔断器有3个状态:

  1. Closed:所有请求正常访问;当失败的请求量达到阈值(默认20),且失败的请求百分比达到阈值(默认50%)时,熔断器从Closed切换到Open
  2. Open:请求会直接失败而不会发送给服务;Open保持一段时间后(默认5秒)会自动切换到Half Open
  3. Half Open:判断下一次请求的返回情况,如果请求成功,则熔断器切换到Closed,否则切换到Open

Hystrix解决服务雪崩效应的主要方式:

  1. 服务降级:当某个服务熔断之后将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值,这样虽然服务水平下降,但好歹可用,比直接挂掉要强
  2. 线程隔离:为每个服务分配一个小的线程池,如果一个服务的线程发生阻塞,调用该服务的过程中不会影响到其它服务

Hystrix使用:

  1. 在consumer中添加hystrix的起步依赖

    <!--hystrix的起步依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    
  2. 在consumer的启动类上添加@EnableCircuitBreaker注解

  3. 在consumer的Controller中添加服务降级处理方法

    // 服务降级处理方法,方法名随意,返回值和形参需要和@HystrixCommand修饰的方法一致
    public Object fallback() {
        User user = new User();
        user.setUsername("服务降级,默认处理!");
        return user;
    }
    
  4. 局部熔断:在consumer中可能发生问题的方法上添加@HystrixCommand(fallbackMethod = "降级处理方法名")注解

    // 如果方法发生问题,则调用降级处理方法;仅对当前方法生效
    @HystrixCommand(fallbackMethod = "fallback")
    @GetMapping("/see")
    public Object see() {
        // 通过serviceId访问provider
        String serviceId = "provider";
        String instanceUrl = "http://" + serviceId + "/user/1";
        User user = restTemplate.getForObject(instanceUrl, User.class);
        return user;
    }
    
  5. 全局熔断:在consumer中可能发生问题的方法上添加@HystrixCommand注解,并在类上添加@DefaultProperties(defaultFallback = "全局降级处理方法名")

    package com.lhp.controller;
    
    import com.lhp.pojo.User;
    import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
    import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.client.RestTemplate;
    
    // 全局服务降级处理,对类的所有方法生效
    @DefaultProperties(defaultFallback = "defaultFallback")
    @RestController
    @RequestMapping("/movie")
    public class MovieController {
        // RestTemplate是从Spring3.0开始支持的一个HTTP请求工具,它提供了常见的REST请求方案的模版
        @Autowired
        private RestTemplate restTemplate;
    
        // 如果方法发生问题,则调用降级处理方法;仅对当前方法生效
        // @HystrixCommand(fallbackMethod = "fallback")
        @HystrixCommand
        @GetMapping("/see")
        public Object see() {
            // 通过serviceId访问provider
            String serviceId = "provider";
            String instanceUrl = "http://" + serviceId + "/user/1";
            User user = restTemplate.getForObject(instanceUrl, User.class);
            return user;
        }
    
        // 全局服务降级处理方法:不能有形参,返回值应与类中每个方法的返回值兼容
        public Object defaultFallback() {
            User user = new User();
            user.setUsername("服务降级,全局默认处理!");
            return user;
        }
    
        // 服务降级处理方法,方法名随意,返回值和形参需要和@HystrixCommand修饰的方法一致
        public Object fallback() {
            User user = new User();
            user.setUsername("服务降级,默认处理!");
            return user;
        }
    }
    
  6. 在consumer的application.yml中追加其他熔断策略的配置

    # 配置熔断策略
    hystrix:
      command:
        default:
          circuitBreaker:
            # 是否强制打开熔断器
            forceOpen: false
            # 失败的请求量阈值
            requestVolumeThreshold: 10
            # 失败的请求百分比阈值
            errorThresholdPercentage: 50
            # Open到Half Open的时间
            sleepWindowInMilliseconds: 10000
          execution:
            isolation:
              thread:
                # 熔断超时时间
                timeoutInMilliseconds: 2000
    

5. Spring Cloud OpenFeign

OpenFeign是一个声明性的REST客户端

OpenFeign已经集成了Ribbon、Hystrix、slf4j

使用OpenFeign替代RestTemplate发送Rest请求:

  1. 在consumer中添加openfeign的起步依赖:

    <!--openfeign的起步依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
  2. 在consumer的启动类上添加@EnableFeignClients注解

  3. 在consumer中创建POJOFeign接口

    package com.lhp.feign;
    
    import com.lhp.pojo.User;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    /**
     * Feign会通过动态代理生成实现类
     * FeignClient(name = "serviceId")注解声明该接口是一个Feign的客户端
     */
    @FeignClient(name = "provider")
    @RequestMapping("/user")
    public interface UserFeign {
        /**
         * Feign会根据注解生成URL地址
         * 方法名随意
         * 返回值和形参应和provider中的保持一致
         */
        @GetMapping("/{id}")
        User findById(@PathVariable(name = "id") Integer id);
    }
    
  4. 在consumer的Controller中注入并使用POJOFeign:

    package com.lhp.controller;
    
    import com.lhp.feign.UserFeign;
    import com.lhp.pojo.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/feign")
    public class ConsumerController {
        @Autowired
        private UserFeign userFeign;
    
        // 使用Feign调用provider的方法
        @RequestMapping("/{id}")
        public User findById(@PathVariable(value = "id") Integer id) {
            return userFeign.findById(id);
        }
    }
    
  5. 浏览器访问测试:http://localhost:7201/feign/1

5.1 OpenFeign配置Ribbon

在consumer的application.yml中配置ribbon:

provider:
  ribbon:
    # 负载均衡策略
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
    # 连接超时时间
    ConnectTimeout: 10000
    # 请求处理超时时间
    ReadTimeout: 2000
    # 最大重试次数
    MaxAutoRetries: 1
    # 重试集群中下一个服务实例的最大次数
    MaxAutoRetriesNextServer: 0
    # 所有操作都重试
    OkToRetryOnAllOperations: false

5.2 OpenFeign配置Hystrix

  1. 在consumer的application.yml中启用feign的hystrix:

    feign:
      hystrix:
        # 启用feign的hystrix
        enabled: true
    
  2. 实现POJOFeign接口

    package com.lhp.feign.impl;
    
    import com.lhp.feign.UserFeign;
    import com.lhp.pojo.User;
    import org.springframework.stereotype.Component;
    
    /**
     * 重写的每一个方法,即这个方法的降级处理方法
     */
    @Component
    public class UserFeignImpl implements UserFeign {
        @Override
        public User findById(Integer id) {
            User user = new User();
            user.setUsername("feign的hystrix:降级处理方法");
            return user;
        }
    }
    
  3. 在consumer中POJOFeign接口的@FeignClient注解中指定降级处理类,并去掉接口上的@RequestMapping注解

    package com.lhp.feign;
    
    import com.lhp.feign.impl.UserFeignImpl;
    import com.lhp.pojo.User;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    /**
     * Feign会通过动态代理生成实现类
     * FeignClient(name = "serviceId")注解声明该接口是一个Feign的客户端
     * fallback指定降级处理类
     */
    @FeignClient(name = "provider", fallback = UserFeignImpl.class, path = "/user")
    // 若用服务降级,则不能在接口上使用@RequestMapping注解;要么在@FeignClient中配置path属性,要么在方法上拼接全路径
    // @RequestMapping("/user")
    public interface UserFeign {
        /**
         * Feign会根据注解生成URL地址
         * 方法名随意
         * 返回值和形参应和provider中的保持一致
         */
        @GetMapping("/{id}")
        User findById(@PathVariable(name = "id") Integer id);
    }
    

5.3 配置请求/响应压缩

OpenFeign可以对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗

在consumer的application.yml中启用feign的请求压缩:

feign:
  compression:
    # 请求压缩
    request:
      enabled: true
      # 对指定类型的数据进行压缩
      mime-types: text/html,application/xml,application/json
      # 触发压缩的下限
      min-request-size: 2048
    # 响应压缩
    response:
      enabled: true

5.4 配置日志

OpenFeign配置日志:

  1. 在consumer的application.yml中配置普通日志级别

    # com.lhp包下的日志级别都为debug
    logging:
      level:
        com.lhp: debug
    
  2. 在一个配置类中注入Logger.Level

    package com.lhp.feign.config;
    
    import feign.Logger;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class FeignLogger {
        @Bean
        public Logger.Level feignLoggerLevel() {
            /**
             * NONE:不记录任何日志,默认值
             * BASIC:仅记录请求的方法、URL、状态码、执行时间
             * HEADERS:在BASIC基础上记录了请求和响应的头信息
             * FULL:记录所有请求和响应的明细
             */
            return Logger.Level.FULL;
        }
    }
    
  3. 在consumer中POJOFeign接口的@FeignClient注解中指定配置类

    @FeignClient(name = "provider", fallback = UserFeignImpl.class, path = "/user", configuration = FeignLogger.class)
    

6. Spring Cloud Gateway

Spring Cloud Gateway旨在提供一种简单而有效的方式来路由API

网关可以根据断言的规则对请求进行转发(路由)、过滤

6.1 Gateway基本使用

环境准备:Gateway本身就是一个微服务,先创建一个子工程Eureka Client端

  1. 创建gateway子工程,添加gateway的起步依赖(不要加入web的起步依赖,冲突):

    <dependencies>
        <!--gateway起步依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--eureka-client起步依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
    
  2. 创建启动类

    package com.lhp;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    // 开启客户端的发现功能
    @EnableDiscoveryClient
    @SpringBootApplication
    public class GatewayApplication {
        public static void main(String[] args) {
            SpringApplication.run(GatewayApplication.class, args);
        }
    }
    
  3. 在application.yml中配置路由规则:

    server:
      port: 7301
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:7000/eureka
    spring:
      application:
        name: gateway
      cloud:
        gateway:
          # 配置路由规则
          routes:
            - id: user-service-route # 路由id
              # 转发到哪个uri
              # uri: http://localhost:7101
              # lb协议表示从eureka-server获取服务请求地址,并且会通过Ribbon进行负载均衡
              uri: lb://provider
              # 断言:路由拦截的地址
              predicates:
                # 如:http://localhost:7301/user/1--转发到-->http://localhost:7101/user/1
                - Path=/user/** # 将以/user/开头的请求都转发到uri
              # 配置局部过滤器
              filters:
                # Param需要自定义ParamGatewayFilterFactory类
                #- Param=age, 21
    
                # 以下两个规则相互抵消了
                # PrefixPath给请求添加前缀,自带的
                # 如:http://localhost:7301/1--转发到-->http://localhost:7101/user/1
                - PrefixPath=/user
                # StripPrefix给请求去除前缀,自带的
                # 如去除第1个前缀:http://localhost:7301/user/1--转发到-->http://localhost:7101/1
                - StripPrefix=1
          # 配置全局默认过滤器
          default-filters:
            - AddResponseHeader=MyName, lhp # 添加响应头:字段名, 值
    

6.2 过滤器

过滤器分类:

  1. 全局过滤器:作用在所有路由上
  2. 局部过滤器:只作用在具体路由上
  3. 默认过滤器
  4. 自定义过滤器

常见默认过滤器:

  1. AddRequestHeader:添加请求头
  2. AddRequestParameter:添加请求参数
  3. AddResponseHeader:添加响应头
  4. StripPrefix:去除前缀

自定义全局过滤器:创建GlobalFilter实现类,并交给Spring容器

package com.lhp.filter;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class LoginGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取请求参数
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        // 如果token为空,则没登录
        if (StringUtils.isEmpty(token)) {
            // 设置状态码为403
            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            // 结束请求
            return exchange.getResponse().setComplete();
        }
        // 放行
        return chain.filter(exchange);

        // 带token访问测试:http://localhost:7301/user/1?token=1
        // 不带token访问测试:http://localhost:7301/user/1
    }

    // 定义过滤器执行顺序;返回值越小,优先级越高
    @Override
    public int getOrder() {
        return 0;
    }
}

自定义局部过滤器:创建AbstractNameValueGatewayFilterFactory子类,并交给Spring容器

package com.lhp.filter;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

// XxxGatewayFilterFactory:Xxx为yaml配置文件中filters的参数
// 需要在yaml配置文件中的filters添加:- Param=name, value
@Component
public class ParamGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
    @Override
    public GatewayFilter apply(NameValueConfig config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                // 获取yaml配置文件中filters的- Param=name, value
                System.out.println("nameInConfig=" + config.getName() + ", " + "valueInConfig=" + config.getValue());
                // 放行
                return chain.filter(exchange);
            }
        };
    }
}

7. Spring Cloud Config

创建一个远程仓库,在其中创建需要被统一配置管理的配置文件

配置文件命名规约:${application}-${profile}.yml/yaml/properties
${application}为应用名称
${profile}用于区分开发环境dev、测试环境test、生产环境pro等

例如:将provider的application.yml重命名为provider-dev.yml上传到远程仓库

创建配置中心config-server子工程:

  1. 添加config-server的依赖:

    <dependencies>
        <!--eureka-client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--config-server-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
    </dependencies>
    
  2. 在启动类上添加@EnableConfigServer注解:

    package com.lhp;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    import org.springframework.cloud.config.server.EnableConfigServer;
    
    // 开启配置服务功能
    @EnableConfigServer
    // 开启客户端的发现功能
    @EnableDiscoveryClient
    @SpringBootApplication
    public class ConfigServerApplication {
        public static void main(String[] args) {
            SpringApplication.run(ConfigServerApplication.class, args);
        }
    }
    
  3. 在application.yml中配置:

    server:
      port: 7401
    eureka:
      client:
        service-url:
          # eureka-server的注册地址
          defaultZone: http://localhost:7000/eureka
    spring:
      application:
        name: config-server
      cloud:
        config:
          server:
            git:
              # 远程仓库的地址
              uri: https://gitee.com/liu-haopeng/config.git
    # com包下的日志级别都为debug
    logging:
      level:
        com: debug
    
  4. 启动,浏览器访问查看文件:http://localhost:7401/provider-dev.yml

让provider从配置中心获取配置:

  1. 添加config的起步依赖:

    <!--config的起步依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
    
  2. 删除原来的application.yml,创建bootstrap.yml:

    # bootstrap.yml是SpringBoot的默认配置文件,其加载时间早于application.yml
    # bootstrap.yml一般用于系统级配置,而application.yml一般用于应用级配置
    eureka:
      client:
        service-url:
          # eureka-server的注册地址
          defaultZone: http://localhost:7000/eureka
    spring:
      cloud:
        config:
          name: provider # 远程仓库中配置文件名的{application}
          profile: dev # 远程仓库中配置文件名的{profile}
          label: master # 远程仓库的分支
          discovery:
            # 启用配置中心
            enabled: true
            # 配置中心的serviceId
            service-id: config-server
    
  3. 依次启动eureka-server、config-server、provider,看provider是否报错

以上存在的问题:若修改远程仓库中的配置,config-server立即生效,而provider只有重启后才会生效

8. Spring Cloud Bus

Spring Cloud Bus可以解决Spring Cloud Config修改远程仓库中的配置后,服务只有重启后才会生效的问题

Spring Cloud Bus默认基于RabbitMQ,因此使用Spring Cloud Bus前需要先启动RabbitMQ

消息总线分发消息的过程:

  1. 访问配置中心的消息总线
  2. 消息总线接收到请求后向消息队列发送消息
  3. provider微服务监听到消息队列中的消息后会重新从配置中心获取最新配置信息

修改config-server子工程:

  1. 添加bus和stream-binder-rabbit的依赖:

    <!--bus-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-bus</artifactId>
    </dependency>
    <!--stream-binder-rabbit-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
    </dependency>
    
  2. 在application.yml中配置RabbitMQ并开放bus-refresh接口:

    server:
      port: 7401
    eureka:
      client:
        service-url:
          # eureka-server的注册地址
          defaultZone: http://localhost:7000/eureka
    spring:
      application:
        name: config-server
      cloud:
        config:
          server:
            git:
              # 远程仓库的地址
              uri: https://gitee.com/liu-haopeng/config.git
      # 配置RabbitMQ
      rabbitmq:
        host: localhost
        port: 5672
        virtual-host: /
        username: guest
        password: guest
    management:
      endpoints:
        web:
          exposure:
            # 开放bus-refresh接口,以便配合bus实现配置动态刷新
            include: bus-refresh
    # com包下的日志级别都为debug
    logging:
      level:
        com: debug
    

修改provider子工程:

  1. 添加bus和stream-binder-rabbit的依赖:

    <!--bus-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-bus</artifactId>
    </dependency>
    <!--stream-binder-rabbit-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
    </dependency>
    
  2. 在bootstrap.yml中配置RabbitMQ:

    # bootstrap.yml是SpringBoot的默认配置文件,其加载时间早于application.yml
    # bootstrap.yml一般用于系统级配置,而application.yml一般用于应用级配置
    eureka:
      client:
        service-url:
          # eureka-server的注册地址
          defaultZone: http://localhost:7000/eureka
    spring:
      cloud:
        config:
          name: provider # 远程仓库中配置文件名的{application}
          profile: dev # 远程仓库中配置文件名的{profile}
          label: master # 远程仓库的分支
          discovery:
            # 启用配置中心
            enabled: true
            # 配置中心的serviceId
            service-id: config-server
      # 配置RabbitMQ
      rabbitmq:
        host: localhost
        port: 5672
        virtual-host: /
        username: guest
        password: guest
    
  3. @RefreshScope注解可以启用刷新配置文件的信息:创建一个单独的配置类存放yml中的数据,然后在Controller中注入这个配置类,不然直接在Controller上添加@RefreshScope注解会导致获取的配置为空

    package com.lhp.config;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cloud.context.config.annotation.RefreshScope;
    import org.springframework.stereotype.Component;
    
    // 启用刷新配置文件的信息
    @RefreshScope
    @Component
    public class ConfigData {
        @Value("${age}")
        private int age;
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    }
    
  4. 修改远程仓库中的配置后,以POST方式访问配置中心的消息总线http://localhost:7401/actuator/bus-refresh,actuator固定,bus-refresh为配置中心yml中的management.endpoints.web.exposure.include

以上组件的Spring Cloud总架构图:

9. Spring Cloud Alibaba Nacos

Spring Cloud Alibaba的组件版本关系:https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E

Nacos官网:https://nacos.io/zh-cn/

Nacos可以代替Eureka做注册中心,也可以代替Config做配置中心

Nacos支持CP和AP模式的切换:

  1. 一致性C:在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
  2. 可用性A:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
  3. 分区容忍性P:分区相当于对通信的时限要求,系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
  • AP模式下只支持注册临时实例,CP模式下支持注册持久化实例

Nacos分为客户端和服务端,客户端可以是个微服务,服务端需要单独下载安装:

  1. Nacos服务端下载(如:nacos-server-1.2.1.zip):https://github.com/alibaba/nacos/releases
  2. Nacos安装启动:解压安装包,Windows下启动bin目录里的startup.cmd;Linux下执行:sh startup.sh -m standalone
  3. 启动后浏览器访问,默认账号和密码都是nacos:http://localhost:8848/nacos

9.1 创建父工程

创建一个Maven父工程来管理依赖:

<packaging>pom</packaging>
<!--Spring Boot版本-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.4.RELEASE</version>
</parent>
<dependencyManagement>
    <dependencies>
        <!--Spring Cloud版本-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.SR1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--Spring Cloud Alibaba版本-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.1.1.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

9.2 Nacos作为注册中心

  1. 创建nacos-client子工程,导入依赖

    <dependencies>
        <!--nacos作为注册中心客户端的起步依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--web起步依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    
  2. 创建启动类,并添加@EnableDiscoveryClient注解

    package com.lhp;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    @SpringBootApplication
    @EnableDiscoveryClient
    public class NacosClientApplication {
        public static void main(String[] args) {
            SpringApplication.run(NacosClientApplication.class, args);
        }
    }
    
  3. 在application.yml中配置:

    server:
      port: 9001
    spring:
      application:
        name: nacos-client
    
  4. 在bootstrap.yml中配置:

    spring:
      cloud:
        nacos:
          # 服务端地址
          server-addr: localhost:8848
          discovery:
            # 服务端的注册地址
            server-addr: ${spring.cloud.nacos.server-addr}
      application:
        name: nacos-client
    
  5. 启动服务端和客户端微服务,浏览器访问服务端查看:http://localhost:8848/nacos

9.2 Nacos作为配置中心

  1. 导入依赖:

    <!--nacos作为配置中心的起步依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    
  2. 在application.yml中配置:

    server:
      port: 9001
    spring:
      application:
        name: nacos-client
    
  3. 在bootstrap.yml中配置spring.cloud.nacos.config:

    spring:
      cloud:
        nacos:
          # 服务端地址
          server-addr: localhost:8848
          discovery:
            # 服务端的注册地址
            server-addr: ${spring.cloud.nacos.server-addr}
          config:
            # 配置中心的地址
            server-addr: ${spring.cloud.nacos.server-addr}
            # 配置文件的配置格式,默认为properties
            file-extension: yaml
            # 命名空间ID
            namespace: public
            # Group
            group: DEFAULT_GROUP
      application:
        name: nacos-client
    
  4. 进入Nacos服务端的配置管理-->配置列表-->点击+号新建配置

    Data ID格式:${prefix}-${spring.profile.active}.${file-extension}
    prefix默认为:spring.application.name,也可以通过spring.cloud.nacos.config.prefix来配置
    spring.profile.active为空时的格式:${prefix}.${file-extension}
    
    例如:nacos-client.yaml
    
    配置格式:选择配置文件的格式
    配置内容:application.yml的内容
    

Nacos有不同的管理级别,可以进行多环境管理:

  1. 配置集Data Id:一个配置文件通常就是一个配置集,配置文件中的每一项是配置项
  2. 配置分组group:配置分组是对配置集进行分组
  3. 命名空间namespace:可用于进行不同环境的配置隔离
  • 范围大小:Data Id \subset group \subset namespace
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容