1. Feign简介

1.1 简介

Feign是Netflix公司开发的一个声明式的REST调用客户端; Ribbon负载均衡、 Hystrⅸ服务熔断是我们Spring Cloud中进行微服务开发非常基础的组件,在使用的过程中我们也发现它们一般都是同时出现的,而且配置也都非常相似,每次开发都有很多相同的代码,因此Spring Cloud基于Netflix Feign整合了Ribbon和Hystrix两个组件,让我们的开发工作变得更加简单, 就像Spring boot是对Spring+ SpringMVC的简化, Spring Cloud Feign对Ribbon负载均衡、 Hystrⅸ服务熔断进行简化,在其基础上进行了进一步的封装,不仅在配置上大大简化了开发工作,同时还提供了一种声明式的Web服务客户端定义方式。使用方式类似Dubbo的使用方式。

1.2 Feign和Ribbon的联系

Ribbon是一个基于 HTTP 和 TCP 客户端 的负载均衡的工具。它可以 在客户端配置

RibbonServerList(服务端列表),使用 HttpClient 或 RestTemplate 模拟http请求,步骤相当繁琐。

Feign 是在 Ribbon的基础上进行了一次改进,是一个使用起来更加方便的 HTTP 客户端。采用接口的 方式, 只需要创建一个接口,然后在上面添加注解即可 ,将需要调用的其他服务的方法定义成抽象方 法即可, 不需要自己构建http请求。然后就像是调用自身工程的方法调用,而感觉不到是调用远程方 法,使得编写客户端变得非常容易。

1.3 负载均衡

Feign中本身已经集成了Ribbon依赖和自动配置,因此我们不需要额外引入依赖,也不需要再注册 RestTemplate 对象。

2. 能干什么

feign是声明式的web service客户端,它让微服务之间的调用变得更简单了,类似controller调用service。Spring Cloud集成了Ribbon和Eureka,可在使用Feign时提供负载均衡的http客户端。

SpringCloud-Feign-LMLPHP

3. Quick Start

3.1 创建服务注册中心

3.1.1 引入依赖坐标

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

3.1.2 配置application.yml

server:
  port: 8080

spring:
  application:
    name: eureka-server

eureka:
  instance:
    hostname: localhost
  client:
    # 是否将自己注册到Eureka服务中,本身就是所有无需注册
    registerWithEureka: false
    # 是否从Eureka中获取注册信息
    fetchRegistry: false
    # 客户端与Eureka服务端进行交互的地址
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

3.1.3 配置启动类

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
   public static void main(String[] args) {
      SpringApplication.run(EurekaServerApplication.class, args);
   }
}

3.1.4 启动项目

SpringCloud-Feign-LMLPHP

3.2 创建服务提供者

3.2.1 引入依赖坐标

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

3.2.2 配置application.yml

server:
  # 缺省为8010
  port: ${port:8010}

spring:
  application:
    name: provider

eureka:
  client:
    # eureka server的路径
    serviceUrl:
      defaultZone: http://localhost:8080/eureka/

3.2.3 创建测试接口

package com.ldx.provider.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @Value("${server.port}")
    String port;

    @GetMapping("hi")
    public String hi(){
        // 通过返回port,使掉用端方便查看调用的是那个端口的服务
        return "hi~ my port ===" + port;
    }
    // 测试服务调用超时
    @GetMapping("hiWithTimeOut")
    public String hiWithTimeOut() throws InterruptedException {
        Thread.sleep(10000);
        return "hi~ my port ===" + port;
    }
}

3.2.4 配置启动项

SpringCloud-Feign-LMLPHP

3.2.5 启动服务

SpringCloud-Feign-LMLPHP

查看注册中心控制台

SpringCloud-Feign-LMLPHP

SpringCloud-Feign-LMLPHP

SpringCloud-Feign-LMLPHP

3.3 创建服务消费者

3.3.1 引入坐标依赖

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.8.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.ldx</groupId>
	<artifactId>consumer</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>consumer</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
		<spring-cloud.version>Hoxton.SR10</spring-cloud.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

3.3.2 配置application.yml

server:
  port: 8082

spring:
  application:
    name: consumer

eureka:
  client:
    # eureka server的路径
    serviceUrl:
      defaultZone: http://localhost:8080/eureka/

feign:
  #开启hystrix熔断机制
  hystrix:
    enabled: true
  client:
    config:
      #配置服务名为provider的相关信息
      provider:
        #打印的日志级别
        loggerLevel: FULL
        #指的是建立连接所用的时间
        connectTimeout: 2000
        #指的是建立连接后从服务器读取到可用资源所用的时间
        readTimeout: 5000
      #default代表所有服务
      default:
        #feign客户端建立连接超时时间
        connectTimeout: 2000
        #feign客户端建立连接后读取资源超时时间
        readTimeout: 3000
#feign.client.config.provider.loggerLevel 对应的日志级别需配合logging.level
logging:
  level:
    com.ldx.consumer.service.HelloService: debug

# 配置熔断超时时间
hystrix:
  command:
    default:
      execution:
        timeout:
          enabled: true
        isolation:
          thread:
            timeoutInMilliseconds: 50000

3.3.3 配置启动类

package com.ldx.consumer;

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

@EnableFeignClients
@SpringBootApplication
public class ConsumerApplication {
   public static void main(String[] args) {
      SpringApplication.run(ConsumerApplication.class, args);
   }
}

3.3.4 创建测试接口

package com.ldx.consumer.controller;

import com.ldx.consumer.service.HelloService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

@RestController
public class HelloController {
    @Resource
    HelloService helloService;

    @GetMapping("hi")
    public String hi(){
        return helloService.hi();
    }

    @GetMapping("hiWithTimeOut")
    public String hiWithTimeOut(){
        return helloService.hiWithTimeOut();
    }
}
package com.ldx.consumer.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "provider",fallback = HelloFallbackImpl.class)
public interface HelloService {
    @GetMapping("hi")
    String hi();

    @GetMapping("hiWithTimeOut")
    String hiWithTimeOut();
}
package com.ldx.consumer.service;

import org.springframework.stereotype.Component;

@Component
public class HelloFallbackImpl implements HelloService {
    @Override
    public String hi() {
        return "远程服务不可用,请稍后重试。。。。。";
    }

    @Override
    public String hiWithTimeOut() {
        return "请求超时。";
    }
}

3.3.5 启动项目

SpringCloud-Feign-LMLPHP

查看注册中心控制台

SpringCloud-Feign-LMLPHP

第一次访问:

SpringCloud-Feign-LMLPHP

第二次访问:

SpringCloud-Feign-LMLPHP

SpringCloud-Feign-LMLPHP

SpringCloud-Feign-LMLPHP

4. 客户端Hystrix整合

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

4.1 熔断器使用

在网络请求时,可能会出现异常请求,如果还想再异常情况下使系统可用,那么就需要容错处理,比如:网络请求超时时给用户提示“稍后重试”或使用本地快照数据等等。

Spring Cloud Feign就是通过Fallback实现的,有两种方式:

  1. @FeignClient.fallback = xxxFeignFallback.class指定一个实现Feign接口的实现类。
  2. @FeignClient.fallbackFactory = xxxFeignFactory.class指定一个实现FallbackFactory工厂接口类

注意:feign的注解@FeignClient:fallbackFactory与fallback方法不能同时使用,这个两个方法其实都类似于Hystrix的功能,当网络不通时返回默认的配置数据。

4.2 配置文件配置

feign:
  #开启hystrix熔断机制
  hystrix:
    enabled: true

请务必注意,从Spring Cloud Dalston开始,Feign默认是不开启Hystrix的。

因此,如使用Dalston以及以上版本请务必额外设置属性:feign.hystrix.enabled=true,否则 断路器不会生效。

Spring Cloud Angel/Brixton/Camden中,Feign默认都是开启Hystrix的。

4.3 fallback 实现

创建HelloFallbackImpl的回调实现,由spring创建使用@Component(其他的注册也可以)注解

HystrixTargeter.targetWithFallback方法实现了@FeignClient.fallback处理逻辑,通过源码可以知道HelloFallbackImpl回调类是从Spring容器中获取的,所以HelloFallbackImpl由spring创建。

4.3.1 HelloService 接口类

package com.ldx.consumer.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "provider",fallback = HelloFallbackImpl.class)
public interface HelloService {
    @GetMapping("hi")
    String hi();

    @GetMapping("hiWithTimeOut")
    String hiWithTimeOut();
}

4.3.2 HelloFallbackImpl回退类

package com.ldx.consumer.service;

import org.springframework.stereotype.Component;

@Component
public class HelloFallbackImpl implements HelloService {
    @Override
    public String hi() {
        return "远程服务不可用,请稍后重试。。。。。";
    }

    @Override
    public String hiWithTimeOut() {
        return "请求超时。";
    }
}

4.3.3 验证

SpringCloud-Feign-LMLPHP

SpringCloud-Feign-LMLPHP

4.4 FallbackFactory工厂

上面的实现方式简单,但是获取不到HTTP请求错误状态码和信息 ,这时就可以使用工厂模式来实现Fallback

同样工厂实现类也要交由spring管理。

4.4.1 HelloService 接口类

package com.ldx.consumer.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "provider",fallbackFactory = HelloFallbackFactory.class)
public interface HelloService {
    @GetMapping("hi")
    String hi();

    @GetMapping("hiWithTimeOut")
    String hiWithTimeOut();
}

4.4.2 HelloFallbackFactory回退类

package com.ldx.consumer.service;

import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;

@Component
public class HelloFallbackFactory implements FallbackFactory<HelloService> {
    @Override
    public HelloService create(Throwable throwable) {
        return new HelloService() {
            @Override
            public String hi() {
                return "请求失败~ error msg:" + throwable.getMessage();
            }

            @Override
            public String hiWithTimeOut() {
                return "请求超时~ error msg:" + throwable.getMessage();
            }
        };
    }
}

4.4.3 验证

SpringCloud-Feign-LMLPHP

SpringCloud-Feign-LMLPHP

5. 自定义ErrorDecoder

ErrorDecoder接口处理请求错误信息,默认实现ErrorDecoder.Default抛出FeignException异常

FeignException.status 方法返回HTTP状态码,FallbackFactory.create默认情况下可以强制转换成FeignException异常这样就可以获取到HTTP状态码了。

5.1 FeignErrorDecoder

@Configuration
public class FeginErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        ServiceException serviceException = new ServiceException();
        serviceException.setMethod(methodKey);
        if (response.status() >= 400 && response.status() <= 499) {
            serviceException.setCode(response.status());
            serviceException.setErrorMessage(response.reason());
            serviceException.setMessage("页面或者参数错误");
        }
        if (response.status() >= 500 && response.status() <= 599) {
            serviceException.setCode(response.status());
            serviceException.setErrorMessage(response.reason());
            serviceException.setMessage("服务器错误");
        }
        return serviceException;
    }
}

class ServiceException extends RuntimeException {
    private int code;
    private String message;
    private String method;
    private String errorMessage;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }

    public String getMethod() {
        return method;
    }

    public void setMethod(String method) {
        this.method = method;
    }

    @Override
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public String getLocalizedMessage() {
        return "错误码:" + code + ",错误信息:" + message + ",方法:" + method + ",具体错误信息:" + errorMessage;
    }
}
@GetMapping("hiWithError")
public String hiWithError() {
    int a = 1/0;
    return "hi~ my port ===" + port;
}

5.2 流程

在Feign客户端发生http请求层面(提供者业务代码报错)的错误时会调用decode方法。在decode方法中实现自定义的错误处理,当出现异常时首先会通过FeginErrorDecoder进行异常的封装,然后会调用HelloFallbackFactory进行异常的回调处理

SpringCloud-Feign-LMLPHP

6. 扩展

6.1 Feign使用HttpClient

6.1.2 导入POM依赖

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

6.1.3 配置文件配置

feign:
  httpclient:
     enabled: true

主要:这个配置可加可不加,在该版本中默认为true,可以不加,在HttpClientFeignLoadBalancedConfiguration源码中有的配置默认为true

@ConditionalOnProperty(
    value = {"feign.httpclient.enabled"},
    matchIfMissing = true
)

6.1.4 验证

首先在工程配置文件中,将配置项 feign.httpclient.enabled 的值,设置为 false 。然后,在HttpClientFeignLoadBalancedConfiguration 的 feignClient(…)方法内的某行打上断点,重新启动项目,注意观察会发现,整个启动过程中,断点没有被命中。接下来,将配置项 feign.httpclient.enabled 的值设置为 true,再一次启动项目,断点被命中。由此,可以验证 HttpClientFeignLoadBalancedConfiguration 自动配置类被启动。

6.2 在feign请求之前操作

@Component
public class TokenRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(feign.RequestTemplate template) {
        String method = template.method();
        String url = template.url();
        System.out.println("调用方法:" + method + ",URL地址:" + url);
    }
}

6.3 请求压缩

Spring Cloud Feign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。我们只需通过下面两个参数设置,就能开启请求与响应的压缩功能:

feign.compression.request.enabled=true
feign.compression.response.enabled=true

同时,我们还能对请求压缩做一些更细致的设置,比如下面的配置内容指定了压缩的请求数据类型,并设置了压缩的大小下限,只有超过这个大小的请求才会对其进行压缩。

feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048

上述配置的feign.compression.request.nime-types和feign.compression.requestmin-request-size均为默认值。

6.4 日志配置

Spring Cloud Feign在构建被@FeignClient注解修饰的服务客户端时,会为每一个客户端都创建一个feign的请求细节。可以在application.properties文件中使用logging.level.

logging.level.com.springcloud.user.service.HelloService=DEBUG

但是,只是添加了如上配置,还无法实现对DEBUG日志的输出。这时由于Feign客户端默认对Logger.Level对象定义为NONE级别,该界别不会记录任何Feign调用过程中对信息,所以我们需要调整它对级别,针对全局对日志级别,可以在应用主类中直接假如Logger.Level的Bean创建,具体如下:

@Bean
public Logger.Level feignLoggerLevel() {
    return Logger.Level.FULL;
}

在调整日志级别为FULL之后,我们调用接口测试,查看日志

2021-03-02 20:12:56.169 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] ---> GET http://provider/hi HTTP/1.1
2021-03-02 20:12:56.169 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] Accept-Encoding: gzip
2021-03-02 20:12:56.169 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] Accept-Encoding: deflate
2021-03-02 20:12:56.169 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] ---> END HTTP (0-byte body)
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] <--- HTTP/1.1 200  (5ms)
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] connection: keep-alive
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] content-length: 19
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] content-type: text/plain;charset=UTF-8
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] date: Tue, 02 Mar 2021 12:12:56 GMT
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] keep-alive: timeout=60
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi]
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] hi~ my port ===8011
2021-03-02 20:12:56.176 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] <--- END HTTP (19-byte body)

6.4.1 fegin日志级别

NONE:不记录任何信息。

BASIC:仅记录请求方法、URL以及响应状态码和执行时间。

HEADERS:出了记录BASIC级别的信息之外,还会记录请求和响应的头信息。

FULL:记录所有请求与响应的细节,包括头信息、请求体、元数据等。

7. FeignClient注解的一些属性

7.1 contextId

比如我们有个user服务,但user服务中有很多个接口,我们不想将所有的调用接口都定义在一个类中,比如:

Client 1

@FeignClient(name = "optimization-user")
public interface UserRemoteClient {
	@GetMapping("/user/get")
	public User getUser(@RequestParam("id") int id);
}

Client 2

@FeignClient(name = "optimization-user")
public interface UserRemoteClient2 {
	@GetMapping("/user2/get")
	public User getUser(@RequestParam("id") int id);
}

这种情况下启动就会报错了,因为Bean的名称冲突了,具体错误如下:

Description:
The bean 'optimization-user.FeignClientSpecification', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

解决方案可以增加下面的配置,作用是允许出现beanName一样的BeanDefinition。

spring.main.allow-bean-definition-overriding=true

另一种解决方案就是为每个Client手动指定不同的contextId,这样就不会冲突了。

上面给出了Bean名称冲突后的解决方案,下面来分析下contextId在Feign Client的作用,在注册Feign Client Configuration的时候需要一个名称,名称是通过getClientName方法获取的:

String name = getClientName(attributes);

registerClientConfiguration(registry, name,
attributes.get("configuration"));
private String getClientName(Map<String, Object> client) {
    if (client == null) {
      return null;
    }
    String value = (String) client.get("contextId");
    if (!StringUtils.hasText(value)) {
      value = (String) client.get("value");
    }
    if (!StringUtils.hasText(value)) {
      value = (String) client.get("name");
    }
    if (!StringUtils.hasText(value)) {
      value = (String) client.get("serviceId");
    }
    if (StringUtils.hasText(value)) {
      return value;
    }


    throw new IllegalStateException("Either 'name' or 'value' must be provided in @"
        + FeignClient.class.getSimpleName());
  }

可以看到如果配置了contextId就会用contextId,如果没有配置就会去value然后是name最后是serviceId。默认都没有配置,当出现一个服务有多个Feign Client的时候就会报错了。

其次的作用是在注册FeignClient中,contextId会作为Client 别名的一部分,如果配置了qualifier优先用qualifier作为别名。

7.2 qualifier

qualifier对应的是@Qualifier注解,使用场景跟上面的primary关系很淡,一般场景直接@Autowired直接注入就可以了。

如果我们的Feign Client有fallback实现,默认@FeignClient注解的primary=true, 意味着我们使用@Autowired注入是没有问题的,会优先注入你的Feign Client。

如果你鬼斧神差的把primary设置成false了,直接用@Autowired注入的地方就会报错,不知道要注入哪个对象。

解决方案很明显,你可以将primary设置成true即可,如果由于某些特殊原因,你必须得去掉primary=true的设置,这种情况下我们怎么进行注入,我们可以配置一个qualifier,然后使用@Qualifier注解进行注入,示列如下:

Feign Client定义

@FeignClient(name = "optimization-user", path="user", qualifier="userRemoteClient")
public interface UserRemoteClient {

	@GetMapping("/get")
	public User getUser(@RequestParam("id") int id);
}

Feign Client注入

@Autowired
@Qualifier("userRemoteClient")
private UserRemoteClient userRemoteClient;
03-24 22:50