提示:本文主要对OpenFeign 服务接口调用进行总结
前言
提示:这里可以添加本文要记录的大概内容:
本文主要对OpenFeign 服务接口调用进行总结
提示:以下是本篇文章正文内容
一、概述
1、OpenFeign是什么
官网解释:
https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/#spring-cloud-openfeign
Feign是一个声明式WebService客户端,使用Feign能让编写Web Service客户端更加简单,它的使用方法是定义一个服务接口然后在上面添加注解,Feign也支持可拔插式的编码器和解码器,Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters,Feign可以与Eureka和Ribbon组合使用以支持负载均衡
Feign是一个声明式的Web服务客户端,让编写Web服务客户端变得非常容易,只需创建一个接口并在接口上添加注解即可
GitHub:https://github.com/spring-cloud/spring-cloud-openfeign
Feign是什么:
Feign 是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。
它的使用方法是定义一个服务接口然后在上面添加注解。
Feign也支持可拔插式的编码器和解码器。
Spring Cloud 对Feign进行了封装,使其支持Spring MVC 标准注解和HttpMessageConverters。
Feign可以与Eureka和Ribbon组合使用以支持负载均衡。
Feign能干什么:
Feign旨在使编写Java Http客户端变得更容易。
前面在使用 Ribbon + RestTemplate时,利用RestTemplate对Http请求进行封装,形成了一套模板化的调用方法。
但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。
所以,Feign 在此基础上做了进一步封装,由它来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需要创建一个接口并使用注解的方式来配置它
(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),
即可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon时,
自动封装服务调用客户端的开发量。
Feign集成了Ribbon。
利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过Feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。
Feign和OpenFeign两者的区别
Feign | OpenFeign |
---|---|
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。 | OpenFeign 是Spring Cloud在Feign的基础上支持了Spring MVC的注解,如@RequestMapping等等,OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。 |
二、OpenFeign使用步骤
接口+注解:微服务调用接口+@FeignClient
2.1 新建cloud-consumer-feign-order80, Feign在消费端使用
2.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>cloud2022</artifactId>
<groupId>com.atguidu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-feign-order80</artifactId>
<dependencies>
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency><!--引入自定义的api通用包-->
<groupId>com.atguidu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
</dependency>
</dependencies>
</project>
2.3 application.yml
server:
port: 80
eureka:
client:
register-with-eureka: false #不将自己注册进eureka
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
2.4 主启动类 OrderFeignMain80.java
package com.atguigu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients //使用Feign,激活并开启
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class, args);
}
}
2.5 业务类
2.5.1 新建PaymentFeignService接口并新增注解@FeignClient
业务逻辑接口+@FeignClient配置调用provider服务
新建PaymentFeignService接口并新增注解@FeignClient
com.atguigu.springcloud.service.PaymentFeignService
@FeignClient value 的内容就是 Eureka 中要调用的微服务的名
Service 中的内容是复制的 8001 的 Controller 的方法头
package com.atguigu.springcloud.service;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
//@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService { //接口的方法与提供服务的controller方法一致
@GetMapping(value = "/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}
2.5.2 控制层Controller
package com.atguigu.springcloud.controller;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentFeignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
public class OrderFeignController {
@Resource
private PaymentFeignService paymentFeignService;
@GetMapping(value = "/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
return paymentFeignService.getPaymentById(id);
}
}
2.6 测试
先启动两个Eureka集群7001/7002
再启动两个微服务8001/8002
启动OpenFeign
http://localhost/consumer/payment/get/1
Feign自带负载均衡配置,以上测试8001、8002间隔提供服务
这里能调到和方法名无关,只和路径有关,只是路径!路径!!
三、OpenFeign的超时控制
3.1 超时设置,故意设置超时演示出错的情况
服务提供方8001故意写暂停程序
8001 PaymentController.java paymentFeignTimeout()
package com.atguigu.springcloud.controller;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
*/
@RestController
@Slf4j
public class PaymentController{
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@Resource
private DiscoveryClient discoveryClient;
@GetMapping(value = "/payment/lb")
public String getPaymentLB() {
return serverPort;
}
@GetMapping(value = "/payment/discovery")
public Object discovery() {
List<String> services = discoveryClient.getServices();
for (String service : services) {
log.info("service:{}", service);
}
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
log.info(instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t"
+ instance.getUri());
}
return this.discoveryClient;
}
@PostMapping(value = "/payment/create")
public CommonResult create(@RequestBody Payment payment) {//此处一定要加@RequestBody,否则80调用8001时,无法传递数据
log.info("前端传递数据:"+payment);
int result = paymentService.create(payment);
log.info("************************插入结果:" +result);
if (result > 0) {
return new CommonResult(200,"插入数据库成功,serverPort为: "+serverPort,result);
} else {
return new CommonResult(444,"插入数据库失败,serverPort为: "+serverPort, null);
}
}
@GetMapping(value = "/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
Payment payment = paymentService.getPaymentById(id);
log.info("*************************查询结果:" +payment);
if (payment != null) {
return new CommonResult(200, "查询成功,serverPort为: "+serverPort, payment);
} else {
return new CommonResult(445,"没有对应id: "+id+" 的记录,serverPort为: "+serverPort, null);
}
}
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeOut() {
System.out.println("*****paymentFeignTimeOut from port: " + serverPort);
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return serverPort;
}
}
服务消费方80添加超时方法,PaymentFeignService
package com.atguigu.springcloud.service;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
//@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService { //接口的方法与提供服务的controller方法一致
@GetMapping(value = "/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
@GetMapping(value = "/payment/feign/timeout")
String paymentFeignTimeOut();
}
服务消费方80添加超时方法,OrderFeignController
服务消费方80 OrderFeignController 添加超时方法 paymentFeignTimeout()
package com.atguigu.springcloud.controller;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentFeignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
public class OrderFeignController {
@Resource
private PaymentFeignService paymentFeignService;
@GetMapping(value = "/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
return paymentFeignService.getPaymentById(id);
}
@GetMapping(value = "/consumer/payment/feign/timeout")
public String paymentFeignTimeOut() {
return paymentFeignService.paymentFeignTimeOut();
}
}
3.2 测试
测试
测试服务提供者直接提供的接口:
8001正常访问,三秒后返回端口
启动7001、7002,8001,feign80,
注意:
不要启动8002,在多个客户端上启动了服务提供者服务,而这两边的服务方法没同步,
所以在调用feign时,会采用负载均衡,在多个客户端上一边读取一次。
如果这个时候恰好读的是没有指定方法的客户端提供的服务,就会报404错误。
重启feign80,访问消费者接口。
http://localhost/consumer/payment/feign/timeout
OpenFeign默认等待一秒钟,一秒钟后得不到微服务提供者的结果,就超时后报错。为了避免服务提供方需要花费处理的时间超过Feign 客户端默认的等待时间而报错,我们就需要设置Feign客户端的超时控制。
在yml中开启配置:
server:
port: 80
eureka:
client:
register-with-eureka: false #不将自己注册进eureka
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
# 设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
ReadTimeout: 5000 #指建立连接后从服务读取到可用资源所用的时间
ConnectTimeout: 5000 #指建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
测试:
三秒钟后返回端口号,不再报错
3.3 OpenFeign日志打印功能
3.3.1 是什么
Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节。说白了就是对Feign接口的调用情况进行监控和输出。
3.3.2 日志级别
级别 | 描述 |
---|---|
NONE | 默认的,不显示任何日志 |
BASIC | 仅记录请求方法、URL、响应状态码和执行时间 |
HEADERS | 除了BASIC中定义的信息之外,还有请求和响应的头信息 |
FULL | 除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据 |
3.3.3 配置日志bean
package com.atguigu.springcloud.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
3.3.4 YML文件里需要开启日志的Feign客户端
server:
port: 80
eureka:
client:
register-with-eureka: false #不将自己注册进eureka
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
# 设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
ReadTimeout: 5000 #指建立连接后从服务读取到可用资源所用的时间
ConnectTimeout: 5000 #指建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
logging:
level:
com.atguigu.springcloud.service.PaymentFeignService: debug #feign日志以什么级别监控哪个接口
重启80 OrderFeign服务 访问 http://localhost/consumer/payment/get/1
打印日志如下: