06.springcloud服务通信组件之OpenFeign

Posted 潮汐先生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了06.springcloud服务通信组件之OpenFeign相关的知识,希望对你有一定的参考价值。

前言

在上一节中05.负载均衡之Ribbon中我们学习了使用Ribbon+RestTemplate实现微服务间负载均衡通信,但是最终还是存在以下问题

  • 路径写死后期不易维护
  • 不能自动转换响应结果为所需对象

针对上面两个问题我们今天来看下springcloud提供的另外一个组件–OpenFeign

OpenFeign

简介

OpenFeign是一个声明式的伪Http客户端,它使得写Http客户端变得更简单。使用OpenFeign,只需要创建一个接口并注解。它具有可插拔的注解特性(可以使用springmvc的注解),可使用OpenFeign注解和JAX-RS注解。OpenFeign支持可插拔的编码器和解码器。OpenFeign默认集成了Ribbon,默认实现了负载均衡的效果并且springcloud为OpenFeign添加了springmvc注解的支持。

简单认为:OpenFeign是springcloud基于Netflix的组件feign开发而来,主要也是一个实现微服务间负载均衡通信的组件(springcloud推荐)。作用就是一个HttpClient客户端对象,不过我们称之为伪HttpClient客户端对象。OpenFeign底层继承了RestTemplate和Ribbon,也就是说实现通信和负载均衡的还是RestTemplate和Ribbon,只不过OpenFeign将两者合二为一做了简化,使开发人员实现起来更加方便了

OpenFeign简单使用

准备工作

我们这里重新构建三个服务:一个商品分类服务–Category(8890),两个商品服务–Product(8891,8892);我们使用商品分类服务调用商品服务来延时OpenFeign的使用

CATEGORY

1.新建Module

在这里插入图片描述

2.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud_parent</artifactId>
        <groupId>com.christy</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>04.springcloud_openfeign_category</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- springboot依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 引入consul client依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!-- 引入健康检查依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- 作为服务调用方需要引入OpenFeign依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

</project>
3.application.properties
server.port=8890
spring.application.name=CATEGORY

# 注册到consul server
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
4.CategoryApplication.java
/**
 * @Author Christy
 * @Date 2021/6/4 10:13
 * @EnableDiscoveryClient 开启服务发现与注册
 * @EnableFeignClients 开启OpenFeign客户端调用
 **/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class CategoryApplication {
    public static void main(String[] args) {
        SpringApplication.run(CategoryApplication.class, args);
    }
}
6.ProductClient.java

我们上面说了使用OpenFeign,只需要创建一个接口并注解,首先我们先来创建一个接口

/**
 * @Author Christy
 * @Date 2021/6/4 10:22
 * 商品服务的服务名为“PRODUCT”,会提供一个获取商品列表的list方法(注意:这个方法只做演示,不提供业务逻辑)
 **/
@FeignClient(value = "PRODUCT")
public interface ProductClient {
    
    /**
     * 这里的方法返回值,路径和请求方式必须和PRODUCT里面保持一致,方法名不做要求
     * @author Christy
     * @date 2021/6/4 10:27
     * @param 
     * @return java.lang.String
     */
    @GetMapping("/product/list")
    String list();
}
5.CategoryController.java
/**
 * @Author Christy
 * @Date 2021/6/4 10:16
 **/
@RestController
@RequestMapping("category")
public class CategoryController {
    private static final Logger log = LoggerFactory.getLogger(CategoryController.class);

    @Autowired
    ProductClient productClient;

    @GetMapping("/product/list")
    public String getProductList(){
        log.info("category service start……");

        String result = productClient.list();

        log.info("category service end," + result);

        return "category: " + result;
    }
}

PRODUCT

1.新建Module

在这里插入图片描述

2.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud_parent</artifactId>
        <groupId>com.christy</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>04.springcloud_openfeign_product</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- springboot依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 引入consul client依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!-- 引入健康检查依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>
</project>
3.application.properties
server.port=8891
spring.application.name=PRODUCT

# 注册到consul server
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
4.ProductApplication.java
/**
 * @Author Christy
 * @Date 2021/6/4 10:36
 **/
@SpringBootApplication
@EnableDiscoveryClient
public class ProductApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductApplication.class, args);
    }
}
5.ProductController.java
/**
 * @Author Christy
 * @Date 2021/6/4 10:38
 **/
@RestController
@RequestMapping("product")
public class ProductController {
    private static final Logger log = LoggerFactory.getLogger(ProductController.class);
    
    @Value("${server.port}")
    private int port;

    @RequestMapping("list")
    public String list(){
        log.info("product service start ……");
        log.info("product service ok, current service port:" + port);
        return "product service ok, current service port:" + port;
    }
}

启动consul

win+R调出命令行窗口,输入命令consul agent -dev启动consul,浏览器访问http://localhost:8500

在这里插入图片描述

启动服务

consul启动成功后依次启动我们的CATEGORYPRODUCT服务,其中PRODUCT服务需要按照文章02.服务注册中心之Eureka中搭建Eureka集群的方法分别启动8891和8892

在这里插入图片描述

在这里插入图片描述

测试

服务启动完毕后,我们在浏览器访问CATEGORY服务中的方法http://localhost:8890/category/product/list

在这里插入图片描述

我们可以看到服务调用成功了,而且实现了我们说的负载均衡

服务间的参数传递和响应处理

开始这个话题之前我们首先要明确的是参数传递的方式有哪几种?大体来讲可以分为三种

  • 零散类型的传参:这里又分为两种,path?key=valuepath/{id}/{name}
  • 对象类型的传参
  • 数组和集合类型的传参

零散类型传参

单个参数
1.ProductController

使用OpenFeign使用零散类型参数,如果是一个参数的话不需要额外的操作,直接定义然后请求就可以了,比如我们在PRODUCT定义单个参数的方法

@RequestMapping("one")
public String getOne(String name){
  log.info("product service select one start ……");
  log.info("product service select one ok, current service port:" + port + ", accept param name:" + name);
  return "product service select one ok, current service port:" + port + ", accept param name:" + name;
}
2.ProductClient

对于调用方CATEGORY来说,我们首先要在ProductClient定义出这个接口

@GetMapping("/product/one")
String getOne(String name);
3.CategoryController

然后我们在CategoryController中调用该方法

@GetMapping("/product/one")
public String getProductOne(){
  log.info("category service get one product start……");
  String result = productClient.getOne("001");
  log.info("category service get one product end," + result);

  return "category: " + result;
}
4.测试

之后我们分别启动两个服务(能正常启动,不报错),直接浏览器输入http://localhost:8890/category/product/one

在这里插入图片描述

我们可以看到参数并没有接收到,这是为什么?想搞清楚这个问题我们先来看下多个参数传递

多个参数
1.ProductController

我们在ProductController中定义如下方法

@RequestMapping("/byParams")
public String getProductByParams(String name, double price){
  log.info("product service select product by params start ……");
  log.info("product service product by params ok, current service port:" + port + ", accept param name=" + name + ",price" +
           "=" + price );
  return "product service product by params ok, current service port:" + port + ", accept param name=" + name + ",price" +
    "=" + price;
}
2.ProductClient

然后我们在ProductClient中调用

@GetMapping("/product/byParams")
String getProductByParams(String name, double price);
3.CategoryController

最后我们在CategoryController中执行该方法

@GetMapping("/product/byParams")
public String getProductByParams(){
  log.info("category service get product by params start……");
  String result = productClient.getProductByParams("杜蕾斯",99.99);
  log.info("category service get product by params end," + result);

  return "category: " + result;
}
4.测试

准备完毕后我们分别启动两个服务,我们发现PRODUCT服务能正常启动,但是CATEGORY报错了

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.christy.feignclient.ProductClient': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalStateException: Method has too many Body parameters: public abstract java.lang.String com.christy.feignclient.ProductClient.getProductByParams(java.lang.String,double)

问题分析

我们来分析一下这是什么原因导致的:

我们开头说了OpenFeign是一个伪HttpClient客户端,底层间的服务调用还是通过RestTemplate来进行的,我们知道零散类型的参数传递有两种方式path?key=valuepath/{id}。一个参数传递的时候默认和第一种方式传递,而多个参数传递的时候我们就必须显式的告诉底层我们希望用哪一种方式传递参数。对于问号传参沃恩需要用注解**@RequestParam修饰参数,对于路径传参必须使用@PathVariable**修饰参数。而且必须要指定形参。

问题修复
1.ProductController

首先们在ProductController中定义下面两个方法

@RequestMapping("/byRequestParam")
    public String getProductByRequestParam(@RequestParam(value = "name") String name, @RequestParam(value = "price") double price){
        log.info("product service select product by request params start ……");

        log.info("product service product by request params ok, current service port:" + port + ", accept param name=" + name +
                ",price" +
                "=" + price );
        return "product service product by request params ok, current service port:" + port + ", accept param name=" + name + ",price" +
                "=" + price;
    }

    @RequestMapping("/byPathVariable/{name}/{price}")
    public String getProductByPathVariable(@PathVariable(value = "name") String name, @PathVariable(value = "price") double price){
        log.info("product service select product by path variable start ……");

        log.info("product service product by path variable ok, current service port:" + port + ", accept param name=" + name +
                ",price" +
                "=" + price );
        return "product service product by path variable ok, current service port:" + port + ", accept param name=" + name + ",price" +
                "=" + price;
    }
2.ProductClient
@GetMapping("/product/byRequestParam")
String getProductByRequestParam(@RequestParam(value = "name") String name, @RequestParam(value = "price") double price);

@GetMapping("/product/byPathVariable/{name}/{price}")
String getProductByPathVariable(@PathVariable(value = "name") String name, @PathVariable(value = "price") double price);
3.CategoryController
/**
 * 问号传参方式
 * @author Christy
 * @date 2021/6/4 12:42
 * @param
 * @return java.lang.String
 */
@GetMapping("/product/byRequestParam")
public String getProductByRequestParam(){
  log.info("category service get product by request param start……");
  String result = productClient.getProductByRequestParam("杜蕾斯",99.99);
  log.info("category service get product by request param end," + result);

  return "category: " + result;
}

/**
  * 路径传参方式
  * @author Christy
  * @date 2021/6/4 12:42
  * @param
  * @return java.lang.String
  */
@GetMapping("/product/byPathVariable")
public String getProductByPathVariable(){
  log.info("category service get product by path variable start……");
  String result = productClient.getProductByPathVariable("杜蕾斯",99.99);
  log.info("category service get product by path variable end," + result);

  return "category: " + result;
}
4.测试

启动五之前记得将上面没有加注解的多个参数传递的方法删除或者注释掉,最后我们来分别启动两个服务

在这里插入图片描述

我们可以看到两个服务都启动成功了,我们再来访问一下这两个方法,浏览器分别输入http://localhost:8890/category/product/byRequestParamhttp://localhost:8890/category/product/byPathVariable

在这里插入图片描述

在这里插入图片描述

我们可以看到两个方法都访问成功了,这也说明我们上面的问题分析也是对的。那么对于单个参数的传递虽然启动的时候不报错,但是实际书写的时候我们也要指定传递的方式和明确形参的名称,否则参数是传递不过去的。

对象类型传参

由于微服务中使用OpenFeign做服务间调用主要是以Rest的方式,所以对于对象类型的传参,主要是传递json的形式。我们知道传递json化的对象数据要使用注解**@RequestBody**来修饰传递的参数

由于涉及到对象的传递,我们现在两个服务里面新建一个实体类Product.java

实际开发中,我们的实体类肯定是放到common服务中心,其他的服务想要使用实体类直接引入就行了。这里由于是演示demo,没有考虑具体的业务逻辑,所以没有设计具体的公共模块,这里我们在CATEGORYPRODUCT里面分别新建一个实体类Product.java

1.Product.java
package com.christy.entity;

import java.util.Date;

/**
 * @Author Christy
 * 

以上是关于06.springcloud服务通信组件之OpenFeign的主要内容,如果未能解决你的问题,请参考以下文章

angular 组件间传值与通信的方法

android 四大组件之Service 通过信使进行远程通信

Vue之父子组件的通信

Android四大组件之Service

Android四大组件之服务的两种启动方式详解

React 组件通信之发布订阅模式