SpringCloud-Feign如何实现微服务间的调用?
- 内容介绍
- 文章标签
- 相关推荐
本文共计4125个文字,预计阅读时间需要17分钟。
1. Feign简介Feign是Netflix公司开发的一个声明式的REST客户端,用于简化Spring Cloud微服务中的REST调用。结合Ribbon负载均衡和Hystrix服务熔断,Feign是Spring Cloud中微服务开发的基础组件。在使用过程中,我们也发现了它的便利性。
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 模拟${eureka.instance.hostname}:${server.port}/eureka/ 3.1.3 配置启动类
添加@EnableEurekaServer 标记为EurekaServer
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
3.1.4 启动项目
3.2 创建服务提供者 3.2.1 引入依赖坐标浏览器输入
localhost:8080访问注册中心
<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
这里将port设置为动态传参,主要是想通过设置vm参数来使启动两个端口不同的服务,以便后续通过consumer调用的时候实现负载均衡效果
server:
# 缺省为8010
port: ${port:8010}
spring:
application:
name: provider
eureka:
client:
# eureka server的路径
serviceUrl:
defaultZone: 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 配置启动项
3.2.5 启动服务通过idea的复制功能创建两个启动模板,且8011模板通过传port参数实现服务端口的动态替换
查看注册中心控制台
服务已注册成功
3.3 创建服务消费者 3.3.1 引入坐标依赖测试服务接口
这里之所以将全部的依赖都展示
因为Netflix 公司2018年已经宣布停止Hystrix的维护,导致springcloud的最新版本即:
2020.0.1已经完全废弃了Hystrix
Hystrix替代品为
文档链接为:Resilience4J
所以如果要使用Hystrix需使用
Hoxton.SR10及以下版本 Hoxton.SR10对应其他服务的版本
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="maven.apache.org/POM/4.0.0" xmlns:xsi="www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="maven.apache.org/POM/4.0.0 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: 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 配置启动类
通过EnableFeignClients开启服务对feign的支持
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();
}
}
@FeignClient:
- name:指定需要调用的微服务的名称(不分大小写),用于创建Ribbon的负载均衡器。 所以Ribbon会把
provider解析为注册中心的服务。- fallback:指定请求失败时的回退逻辑
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 启动项目
查看注册中心控制台
服务注册已成功
测试接口
第一次访问:
第二次访问:
测试请求服务超时
打印堆栈信息如下:
4. 客户端Hystrix整合当直接把服务提供者关闭后,请求返回结果也是熔断返回的信息,符合预期。
springcloud每个版本的差异比较大,有的低版本可能需要显性的引用hystrix依赖
当前使用的
Hoxton.SR10版本不需要引用,因为已经被feign整合了
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
4.1 熔断器使用
在网络请求时,可能会出现异常请求,如果还想再异常情况下使系统可用,那么就需要容错处理,比如:网络请求超时时给用户提示“稍后重试”或使用本地快照数据等等。
Spring Cloud Feign就是通过Fallback实现的,有两种方式:
- @FeignClient.fallback = xxxFeignFallback.class指定一个实现Feign接口的实现类。
- @FeignClient.fallbackFactory = xxxFeignFactory.class指定一个实现FallbackFactory工厂接口类
注意:feign的注解@FeignClient:fallbackFactory与fallback方法不能同时使用,这个两个方法其实都类似于Hystrix的功能,当网络不通时返回默认的配置数据。
4.2 配置文件配置在application.properties 启用hystrix
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 验证
4.4 FallbackFactory工厂关闭服务提供者后测试
上面的实现方式简单,但是获取不到HTTP请求错误状态码和信息 ,这时就可以使用工厂模式来实现Fallback
同样工厂实现类也要交由spring管理。
4.4.1 HelloService 接口类fallbackFactory 属性 指定了 自定义的回退工厂
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回退类
注意 实现的是
feign.hystrix.FallbackFactory
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 验证
5. 自定义ErrorDecoder关闭服务提供者后测试
ErrorDecoder接口处理请求错误信息,默认实现ErrorDecoder.Default抛出FeignException异常
FeignException.status 方法返回HTTP状态码,FallbackFactory.create默认情况下可以强制转换成FeignException异常这样就可以获取到HTTP状态码了。
自定义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客户端发生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日志级别
对于Feign的Logger级别主要有下面4类,可根据实际需要进行调整使用。
NONE:不记录任何信息。
BASIC:仅记录请求方法、URL以及响应状态码和执行时间。
HEADERS:出了记录BASIC级别的信息之外,还会记录请求和响应的头信息。
FULL:记录所有请求与响应的细节,包括头信息、请求体、元数据等。
7. FeignClient注解的一些属性比如我们有个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 qualifierqualifier对应的是@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;
本文共计4125个文字,预计阅读时间需要17分钟。
1. Feign简介Feign是Netflix公司开发的一个声明式的REST客户端,用于简化Spring Cloud微服务中的REST调用。结合Ribbon负载均衡和Hystrix服务熔断,Feign是Spring Cloud中微服务开发的基础组件。在使用过程中,我们也发现了它的便利性。
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 模拟${eureka.instance.hostname}:${server.port}/eureka/ 3.1.3 配置启动类
添加@EnableEurekaServer 标记为EurekaServer
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
3.1.4 启动项目
3.2 创建服务提供者 3.2.1 引入依赖坐标浏览器输入
localhost:8080访问注册中心
<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
这里将port设置为动态传参,主要是想通过设置vm参数来使启动两个端口不同的服务,以便后续通过consumer调用的时候实现负载均衡效果
server:
# 缺省为8010
port: ${port:8010}
spring:
application:
name: provider
eureka:
client:
# eureka server的路径
serviceUrl:
defaultZone: 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 配置启动项
3.2.5 启动服务通过idea的复制功能创建两个启动模板,且8011模板通过传port参数实现服务端口的动态替换
查看注册中心控制台
服务已注册成功
3.3 创建服务消费者 3.3.1 引入坐标依赖测试服务接口
这里之所以将全部的依赖都展示
因为Netflix 公司2018年已经宣布停止Hystrix的维护,导致springcloud的最新版本即:
2020.0.1已经完全废弃了Hystrix
Hystrix替代品为
文档链接为:Resilience4J
所以如果要使用Hystrix需使用
Hoxton.SR10及以下版本 Hoxton.SR10对应其他服务的版本
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="maven.apache.org/POM/4.0.0" xmlns:xsi="www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="maven.apache.org/POM/4.0.0 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: 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 配置启动类
通过EnableFeignClients开启服务对feign的支持
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();
}
}
@FeignClient:
- name:指定需要调用的微服务的名称(不分大小写),用于创建Ribbon的负载均衡器。 所以Ribbon会把
provider解析为注册中心的服务。- fallback:指定请求失败时的回退逻辑
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 启动项目
查看注册中心控制台
服务注册已成功
测试接口
第一次访问:
第二次访问:
测试请求服务超时
打印堆栈信息如下:
4. 客户端Hystrix整合当直接把服务提供者关闭后,请求返回结果也是熔断返回的信息,符合预期。
springcloud每个版本的差异比较大,有的低版本可能需要显性的引用hystrix依赖
当前使用的
Hoxton.SR10版本不需要引用,因为已经被feign整合了
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
4.1 熔断器使用
在网络请求时,可能会出现异常请求,如果还想再异常情况下使系统可用,那么就需要容错处理,比如:网络请求超时时给用户提示“稍后重试”或使用本地快照数据等等。
Spring Cloud Feign就是通过Fallback实现的,有两种方式:
- @FeignClient.fallback = xxxFeignFallback.class指定一个实现Feign接口的实现类。
- @FeignClient.fallbackFactory = xxxFeignFactory.class指定一个实现FallbackFactory工厂接口类
注意:feign的注解@FeignClient:fallbackFactory与fallback方法不能同时使用,这个两个方法其实都类似于Hystrix的功能,当网络不通时返回默认的配置数据。
4.2 配置文件配置在application.properties 启用hystrix
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 验证
4.4 FallbackFactory工厂关闭服务提供者后测试
上面的实现方式简单,但是获取不到HTTP请求错误状态码和信息 ,这时就可以使用工厂模式来实现Fallback
同样工厂实现类也要交由spring管理。
4.4.1 HelloService 接口类fallbackFactory 属性 指定了 自定义的回退工厂
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回退类
注意 实现的是
feign.hystrix.FallbackFactory
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 验证
5. 自定义ErrorDecoder关闭服务提供者后测试
ErrorDecoder接口处理请求错误信息,默认实现ErrorDecoder.Default抛出FeignException异常
FeignException.status 方法返回HTTP状态码,FallbackFactory.create默认情况下可以强制转换成FeignException异常这样就可以获取到HTTP状态码了。
自定义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客户端发生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日志级别
对于Feign的Logger级别主要有下面4类,可根据实际需要进行调整使用。
NONE:不记录任何信息。
BASIC:仅记录请求方法、URL以及响应状态码和执行时间。
HEADERS:出了记录BASIC级别的信息之外,还会记录请求和响应的头信息。
FULL:记录所有请求与响应的细节,包括头信息、请求体、元数据等。
7. FeignClient注解的一些属性比如我们有个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 qualifierqualifier对应的是@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;

