OpenFeign学习记录
周瑜 Lv2

Feign声明式服务调用

1.什么是feign

  • Feign是SpringCloud Netflix 组件中的一个轻量级RESTful的HTTP服务客户端,实现了负载均衡和Rest调用的开源框架,封装了Ribbon和RestTemplate,实现了WebService的面向接口编程,进一步降低了项目的耦合度。
  • Feign内置了Ribbon,用来做客户端负载均衡调用服务注册中心的服务。
  • Feign本身并不支持SpringMVC的注解,他有一套自己的注解,为了更方便的使用,Spring Cloud孵化了OpenFeign。
  • Feign是一种声明式、模板化的HTTP客户端(仅在Consumer中使用)
  • Feign支持的注解和用法请参考官方文档
  • Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用注册中心的服务

2.Feign解决了什么问题

  • Feign旨在使编写JAVA HTTP 客户端变得更容易,Feign简化了Rest Template代码,实现了Ribbon负载均衡,是代码变得更加简洁,也少了客户端调用的代码,使用Feign实现负载均衡是首选方案。只需要你创建一个接口,然后在上面添加注解即可。
  • Feign是声明式服务调用组件,其核心就是:像调用本地方法一样调用远程方法,无感知远程HTTP请求。
  • 它解决了让开发者调用远程接口就跟调用本地方法一样的体验,开发者完全感知不到这是远程方法,更感知不到这是个HTTP请求,无需关注与远程的交互细节,更无需关注分布式环境开发。
  • 它像Dubbo一样 ,Consumer直接调用Provider接口方法,而不需要通过常规的Http Client狗仔请求再解析返回数据。

3.Feign vs Open Feign

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

4.Feign入门案例

Feign的使用主要分为一下几个步骤:

  • 服务消费者添加Feign依赖;
  • 创建业务层接口,添加@FeignClient注解声明要调用的服务;
  • 业务层抽象方法使用SpringMVC注解来配置服务地址及参数
  • 启动类添加@EnableFeignClient注解激活Feign组件

5.Feign实例项目

  • 先创建一个空项目

  • 然后分别创建两个eureka注册中心

    • eureka-server

      • 添加eureka依赖

        1
        2
        3
        4
        5
        <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        <version>3.0.1</version>
        </dependency>
      • 编写配置文件

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        spring:
        application:
        name: eureka-server #应用名称
        server:
        port: 8761
        eureka:
        instance:
        hostname: eureka01 #主机名
        prefer-ip-address: true
        instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip:port
        client:
        #设置服务注册中心地址,指向另一个注册中心
        service-url: #注册中心对外暴露的注册地址
        defaultZone: http://localhost:8762/eureka/
    • eureka-server02

      • 同上,配置文件修改一下端口和注册地址

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        spring:
        application:
        name: eureka-server #应用名称
        server:
        port: 8762
        eureka:
        instance:
        hostname: eureka02 #主机名
        prefer-ip-address: true
        instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip:port
        client:
        #设置服务注册中心地址,指向另一个注册中心
        service-url: #注册中心对外暴露的注册地址
        defaultZone: http://localhost:8761/eureka/
  • 在创建两个服务端并注册到注册中心去,待会供我们的消费者使用

    • service-provider

      • 添加依赖

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
        </dependency>
        <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        <version>3.0.1</version>
        </dependency>
      • 编写配置文件

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
           spring:
        application:
        name: service-provider
        server:
        port: 7070
        eureka:
        instance:
        prefer-ip-address: true
        instance-id: ${spring.cloud.client.ip-address}:${server.port}
        client:
        service-url:
        defaultZone: http://localhost:8761/eureka/,
        http://localhost:8762/eureka/ #将其分别注册进两个注册中心中
      • 编写bean

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        package com.feign.serviceprovider.bean;

        import lombok.AllArgsConstructor;
        import lombok.Data;
        import lombok.NoArgsConstructor;

        import java.io.Serializable;
        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        public class Product implements Serializable {
        private Integer id;
        private String productName;
        private Integer productNum;
        private Double productPrice;

        }
      • 编写service

        • 接口

          1
          2
          3
          4
          5
          6
          7
          8
          9
          package com.feign.serviceprovider.service;

          import com.feign.serviceprovider.bean.Product;

          import java.util.List;

          public interface ProductService {
          List<Product> getPro();
          }
        • impl

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          package com.feign.serviceprovider.service.impl;

          import com.feign.serviceprovider.bean.Product;
          import com.feign.serviceprovider.service.ProductService;
          import org.springframework.stereotype.Service;

          import java.util.Arrays;
          import java.util.List;
          @Service
          public class ProductServiceImpl implements ProductService {

          @Override
          public List<Product> getPro() {
          return Arrays.asList(new Product(1,"华为手机-7070",2,5000.00),
          new Product(2,"苹果手机",3,6900.00),
          new Product(3,"一加手机",2,4999.99)
          );
          }
          }
      • 编写控制器

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        package com.feign.serviceprovider.controller;

        import com.feign.serviceprovider.service.ProductService;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.web.bind.annotation.GetMapping;
        import org.springframework.web.bind.annotation.RestController;

        @RestController
        public class ProductController {
        @Autowired
        ProductService productService;
        @GetMapping("/getPro")
        public Object getPro(){
        return productService.getPro();
        }
        }
    • service-provider02

      • 同上除了配置文件端口和实现类输出的信息
  • 最后创建我们的消费者远程调用服务端

    • service-consumer

      • 添加依赖

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
        </dependency>
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        </dependency>
        <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
        <version>3.0.1</version>
        </dependency>
        <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        <version>3.0.1</version>
        </dependency>
      • 编写配置文件

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        spring:
        application:
        name: service-provider
        server:
        port: 9090
        eureka:
        client:
        service-url:
        defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
        register-with-eureka: false #是否将自己注册到注册中心,默认为true
        registry-fetch-interval-seconds: 10 #表示Eureka Client间隔多久去服务器拉取注册信息,默认为30秒
      • 编写bean

        • Order

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          package com.feign.serviceconsumer.bean;

          import lombok.AllArgsConstructor;
          import lombok.Data;
          import lombok.NoArgsConstructor;

          import java.io.Serializable;
          import java.util.List;
          @Data
          @AllArgsConstructor
          @NoArgsConstructor
          public class Order implements Serializable {
          private Integer id;
          private String orderNo;
          private String orderAddress;
          private Double totalPrice;
          private List<Product> productList;
          }
        • Product

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          import lombok.AllArgsConstructor;
          import lombok.Data;
          import lombok.NoArgsConstructor;

          import java.io.Serializable;

          @Data
          @NoArgsConstructor
          @AllArgsConstructor
          public class Product implements Serializable {
          private Integer id;
          private String productName;
          private Integer productNum;
          private Double productPrice;

          }
      • service层

        • Product

          • 接口

            1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            11
            12
            13
            package com.feign.serviceconsumer.service;


            import com.feign.serviceconsumer.bean.Product;
            import org.springframework.cloud.openfeign.FeignClient;
            import org.springframework.web.bind.annotation.GetMapping;

            import java.util.List;
            @FeignClient("SERVICE-PROVIDER") //绑定在注册中心名为SERVICE-PROVIDER的服务端
            public interface ProductService {
            @GetMapping("/getPro")
            List<Product> getPro();
            }
        • Order

          • 接口

            1
            2
            3
            4
            5
            6
            7
            8
            package com.feign.serviceconsumer.service;

            import com.feign.serviceconsumer.bean.Order;

            public interface OrderService {

            Order selectOrderById(Integer id);
            }
          • impl

            1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            11
            12
            13
            14
            15
            16
            17
            18
            package com.feign.serviceconsumer.service.impl;

            import com.feign.serviceconsumer.bean.Order;
            import com.feign.serviceconsumer.service.OrderService;
            import com.feign.serviceconsumer.service.ProductService;
            import org.springframework.beans.factory.annotation.Autowired;
            import org.springframework.stereotype.Service;

            @Service
            public class OrderServiceImpl implements OrderService {
            @Autowired
            ProductService productService;

            @Override
            public Order selectOrderById(Integer id) {
            return new Order(id,"order-001","中国",2222.00,productService.getPro());
            }
            }
      • controller层

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        package com.feign.serviceconsumer.controller;

        import com.feign.serviceconsumer.bean.Order;
        import com.feign.serviceconsumer.service.OrderService;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.web.bind.annotation.GetMapping;
        import org.springframework.web.bind.annotation.PathVariable;
        import org.springframework.web.bind.annotation.RestController;

        @RestController
        public class OrderController {
        @Autowired
        private OrderService orderService;

        @GetMapping("/getOrder/{id}")
        public Order getOrder(@PathVariable("id") Integer id){
        return orderService.selectOrderById(id);
        }
        }
      • springboot启动类

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        package com.feign.serviceconsumer;

        import org.springframework.boot.SpringApplication;
        import org.springframework.boot.autoconfigure.SpringBootApplication;
        import org.springframework.cloud.openfeign.EnableFeignClients;

        @SpringBootApplication
        @EnableFeignClients //告诉框架扫描所有通过注解@FeignClient定义的feign客户端
        public class ServiceConsumerApplication {

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

        }
    • 效果图:分别为第一次访问和第二次访问,feign自带负载均衡配置项,会依次访问两个服务端

      image-20210315224047507

image-20210315224119387

6.feign负载均衡的配置

  • feign负载均衡默认是轮询的,目前feign3.0.1好像配置没有,里面没有内置ribbonjar包

    1
    2
    3
    SERVICE-PROVIDER:
    ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
  1. com.netflix.loadbalancer.RandomRule #配置规则 随机,几个提供者间随机访问
  2. com.netflix.loadbalancer.RoundRobinRule #配置规则 轮询,轮流访问
  3. com.netflix.loadbalancer.RetryRule #配置规则 重试,在一段时间内通过RoundRobinRule选择服务实例,一段时间内没有选择出服务则线程终止
  4. com.netflix.loadbalancer.WeightedResponseTimeRule #配置规则 响应时间权重,根据平均响应时间来计算权重
  5. com.netflix.loadbalancer.BestAvailableRule #配置规则 最空闲连接策略

7.feign传参

  • GET 使用@PathVariable注解或@RequestParam注解接收请求参数

    • 实例

      • 在服务提供者service-provider的接口添加一个根据id查询的方法

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        package com.feign.serviceprovider.service;

        import com.feign.serviceprovider.bean.Product;

        import java.util.List;

        public interface ProductService {
        List<Product> getPro();
        Product getProById(Integer id);
        }
      • 在impl实现这个接口

        1
        2
        3
        4
        @Override
        public Product getProById(Integer id) {
        return new Product(id,"冰箱",2,3000.00);
        }
      • 在控制器层映射这个方法

        1
        2
        3
        4
        @GetMapping("/getProById/{id}")
        public Object getProById(@PathVariable("id") Integer id){
        return productService.getProById(id);
        }
      • 在消费者端的接口类调用这个方法

        1
        2
        3
        4
        5
        6
        7
        8
        import java.util.List;
        @FeignClient("SERVICE-PROVIDER")
        public interface ProductService {
        @GetMapping("/getPro")
        List<Product> getPro();
        @GetMapping("/getProById/{id}")
        Product getProById(@PathVariable("id") Integer id);
        }
      • 修改Order实现类,返回调用服务提供者的根据id查询方法

        1
        2
        3
        4
        5
             @Override
        public Order selectOrderById(Integer id) {
        return new Order(id,"order-001","中国",2222.00,
        Arrays.asList(productService.getProById(5)));
        }
      • 成功调用服务提供者的根据id查询方法,效果图

        • image-20210316105634966
  • post请求 使用@RequestBody注解接受请求参数

    • 实例

      • 在服务提供者service-provider的接口添加两个方法

        1
        2
        3
        Product getProByIdPost(Integer id);

        Map<Object,Object> createPro(Product product);
      • 在impl实现这个接口

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        @Override
        public Product getProByIdPost(Integer id) {
        return new Product(id,"洗衣机",5,3999.00);
        }

        @Override
        public Map<Object, Object> createPro(Product product) {
        System.out.println(product);
        Map<Object,Object> map = new HashMap<>();
        if(product == null){
        map.put("code",400);
        map.put("msg","添加失败");
        return map;
        }
        map.put("code",200);
        map.put("msg","添加成功");
        return map;
        }
      • 在控制器层映射这个方法

        1
        2
        3
        4
        5
        6
        7
        8
        9
              @PostMapping("/getProByIdPost")
        public Object getProByIdPost(@RequestBody Integer id){
        return productService.getProByIdPost(id);
        }
        @PostMapping("save")
        //注意这里要加上@RequestBody注解,不然实现类那里获取不到product的值
        public Map<Object,Object> save(@RequestBody Product product){
        return productService.createPro(product);
        }
      • 在消费者端的接口类调用这个方法

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        @FeignClient("SERVICE-PROVIDER")
        public interface ProductService {
        @GetMapping("/getPro")
        List<Product> getPro();
        @GetMapping("/getProById/{id}")
        Product getProById(@PathVariable("id") Integer id);
        @PostMapping("/getProByIdPost")
        Product getProByIdPost(Integer id);
        @PostMapping("save")
        Map<Object,Object> createPro(Product product);
        }
      • 控制器层映射这两个方法

        1
        2
        3
        4
        5
        6
        7
        8
        9
        @PostMapping(value = "/getProPost",produces={"application/json;charset=UTF-8"})
        public Product getProPost(Integer id){
        return productService.getProByIdPost(id);
        }
        @PostMapping("/save")
        public Map<Object,Object> save(Product product){
        System.out.println(product);
        return productService.createPro(product);
        }
      • 成功调用服务提供者的方法,效果图:

        image-20210316132939295

      image-20210316133112713

8.Feign性能优化

  • Gzip压缩

    • 介绍:gzip是一种数据格式,采用deflate算法压缩数据;gzip是一种流行的文件压缩算法,应用十分广泛,尤其是在Linux平台。

    • gzip能力:当gzip压缩一个纯文本文件时,效果是非常明显的,大约可以减少70%以上的文件大小。

    • gzip作用:网络数据经过压缩后实际上降低了网络传输的字节数,最明显的好处就是可以加快网页加载的速度。

    • 实例:百度用的就是gzip压缩

      image-20210316134006646

    • Accept-Encoding向服务端表示客户端支持的压缩格式,如果不发送该消息头,服务端默认是不会压缩的

    • 服务端在收到请求后,如果发现请求头中含有Accept-Encoding字段,并且支持该类型压缩,就会对响应报文压缩之后返回给客户端,并且携带Content-Encoding:gzip消息头,表示响应报文是根据该格式进行压缩的。

    • 客户端接收到请求后,先判断是否有Content-Encoding消息头,如果有,按照格式解压报文。否则按正常报文处理

      image-20210316134157354

    • 全局配置 –目前无效,原因不详 –添加这个mime-types: “application/xhtml+xml”就好了

      1
      2
      3
      4
      5
      6
      7
      8
        server:
      port: 9090
      compression:
      enabled: true #是否开启压缩
      #mime-types:配置压缩支持的类型。默认支持"text/html",
      "text/xml", "text/plain", "text/css", "text/javascript",
      "application/javascript", "application/json", "application/xml"
      # mime-types:
    • 局部配置

      • 百度一下
  • HTTP连接池

    • 将Feign的Http客户端工具修改为HttpClient

      • 导入依赖

        1
        2
        3
        4
        5
        <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-httpclient</artifactId>
        <version>11.1</version>
        </dependency>
    • 使用了HttpClient客户端之后,我们还可以通过Get请求传递对象参数。

      • 服务提供者新增方法

        • 接口

          1
          Product savePro(Product product);
        • impl

          1
          2
          3
          4
          5
          @Override
          public Product savePro(Product product) {
          System.out.println(product);
          return product;
          }
        • 控制器

          1
          2
          3
          4
          @GetMapping("savePro")
          public Product savePro(@RequestBody Product product){
          return product;
          }
      • 消费者远程调用

        • 接口

          1
          2
          @GetMapping("/savePro")
          Product savePro(Product product);
        • 控制器

          1
          2
          3
          4
          @GetMapping("/savePro")
          public Product savePro(Product product){
          return product;
          }
      • 效果图:

        image-20210316150339188

  • 状态查看 –过时

  • 请求超时 –目前Feign失效,默认1秒超时都不报错

    • 全局

      1
      2
      3
      ribbon:
      ConnectTimeout: 2000
      ReadTimeout: 2000
    • 局部

      image-20210316155310299

  • Post title:OpenFeign学习记录
  • Post author:周瑜
  • Create time:2021-03-16 16:48:18
  • Post link:https://xinblog.github.io/2021/03/16/OpenFeign-md/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
 Comments