OpenFeign

大佬文章,请优先查看:

服务调用/通信-OpenFeign最佳实践

1.OpenFeign 简介

OpenFeign官网文档

Spring Cloud OpenFeign 它是 Spring 官方推出的一种声明式服务调用与负载均衡组件。它底层基于 Netflix Feign,Netflix Feign 是 Netflix 设计的开源的声明式 WebService 客户端,用于简化服务间通信。

Spring Cloud openfeign 对 Feign 进行了增强,使其支持 Spring MVC 注解,另外还整合了 Ribbon 和 Nacos,从而使得 Feign 的使用更加方便。

Feign 是声明性(注解)Web 服务客户端。它使编写 Web 服务客户端更加容易。要使用 Feign,请创建一个接口并对其进行注解。它具有可插入注解支持,包括 Feign 注解和 JAX-RS 注解。

Feign 还支持可插拔编码器和解码器。Spring Cloud 添加了对 Spring MVC 注解的支持,并支持使用 HttpMessageConverters,Spring Web 中默认使用的注解。Spring Cloud 集成了 Ribbon 和 Eureka 以及 Spring Cloud LoadBalancer,以在使用 Feign 时提供负载均衡的 http 客户端。

Feign 是一个远程调用的组件,Feign 集成了 ribbon,ribbon 里面集成了 eureka。

2.OpenFeign 快速入门

feign服务提供者

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 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.12.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.hguo</groupId>
	<artifactId>feign-server</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>feign-server</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
		<spring-cloud.version>Hoxton.SR12</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.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>

server:
  port: 8081
spring:
  application:
    name: openFeign-server
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
     hostname: localhost
     prefer-ip-address: true
     instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}

添加访问接口

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 订单控制器
 *
 * @author leizi
 * @create 2023-04-20 23:44
 */
@RestController
public class OrderController {

    /**
     * 添加订单
     *
     * @param orderName 订单名称
     * @return
     */
    @GetMapping("/add")
    public String addOrder(@RequestParam("orderName") String orderName) {
        System.out.println("create order:" + orderName);
        return "success";
    }
}

启动测试访问

feign服务消费者

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 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.12.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<groupId>com.hguo</groupId>
	<artifactId>feign-client</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>feign-client</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
		<spring-cloud.version>Hoxton.SR12</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>

        <!--OpenFeign依赖-->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</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>
	</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>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

application.yml

server:
  port: 8082
spring:
  application:
    name: openFeign-client
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
     hostname: localhost
     prefer-ip-address: true
     instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}

添加feign接口

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

/**
 * @author leizi
 * @create 2023-05-04 20:43
 */
// value = "openFeign-server", value后面值必须是和提供者服务名一致
@FeignClient(value = "openFeign-server")
public interface OrderFeign {

    @GetMapping("/add")
    String addOrder(@RequestParam("orderName") String orderName);
}

添加访问接口

/**
 * @author leizi
 * @create 2023-05-04 20:46
 */
@RestController
public class UserController {

    @Autowired
    private OrderFeign orderFeign;


    /**
     * 根据用户id添加订单
     *
     * @param userId 用户id
     * @return
     */
    @GetMapping("/user/{userId}")
    public String addOrderByUserId(@PathVariable Integer userId) {
        return "根据用户id添加订单:" + orderFeign.addOrder(String.valueOf(userId));
    }
}

配置启动类,在启动类上加上@EnableFeignClients注解

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

@SpringBootApplication
@EnableFeignClients // 标记feign客户端
@EnableEurekaClient
public class FeignClientApplication {

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

}

访问测试

本次调用总结

@FeignClient 注解说明当前接口为 OpenFeign 通信客户端,参数值 openFeign-server 为服务提供者 ID(注意,OpenFeign服务名称不支持下划线_,这是一个坑),这一项必须与 注册中心中 注册 ID 保持一致。

在 OpenFeign 发送请求前会自动在 注册中心 查询 openFeign-server 所有可用实例信息,再通过内置的 Ribbon 负载均衡选择一个实例发起 RESTful 请求,进而保证通信高可用。

3.测试 feign 调用的负载均衡

启动多台 provider-order-service:

测试访问:

4.@FeignClient 注解属性详解

contextId: 如果配置了contextId,该值将会作为beanName。

fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口

fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码

url: url一般用于调试,可以手动指定@FeignClient调用的地址

5.调用超时设置

因为 ribbon 默认调用超时时长为1s ,可以修改,超时调整可以查看DefaultClientConfigImpl

server:
    port: 8082
spring:
    application:
        name: feign-client
eureka:
    client:
        service-url:
            defaultZone: http://127.0.0.1:8761/eureka
            
# feign只是帮你封装了远程调用的功能 底层还是ribbon 所以我们需要去修改ribbon的时间
ribbon:
    ReadTimeout: 3000 # 3s超时时间
    ConnectTimeout: 3000 # 链接服务的超时时间
logging:
    level:
        com.hguo.feignclient.feign.OrderFeign: debug  # 打印这个接口下面的日志

6.OpenFeign 调用参数处理

Feign 传参确保消费者和提供者的参数列表一致 包括返回值 方法签名要一致

  1. 通过 URL 传参数,GET 请求,参数列表使用@PathVariable(“”)

  2. 如果是 GET 请求,每个基本参数必须加@RequestParam(“”)

  3. 如果是 POST 请求,而且是对象集合等参数,必须加@Requestbody 或者@RequestParam

import com.powernode.domain.Order;
import org.springframework.web.bind.annotation.*;

import javax.annotation.PostConstruct;
import java.util.Date;


/**
 * url    /doOrder/热干面/add/油条/aaa
 * get传递一个参数
 * get传递多个参数
 * post传递一个对象
 * post传递一个对象+一个基本参数
 */
@RestController
public class ParamController {

    @GetMapping("testUrl/{name}/and/{age}")
    public String testUrl(@PathVariable("name") String name, @PathVariable("age") Integer age) {
        System.out.println(name + ":" + age);
        return "ok";
    }

    @GetMapping("oneParam")
    public String oneParam(@RequestParam(required = false) String name) {
        System.out.println(name);
        return "ok";
    }


    @GetMapping("twoParam")
    public String twoParam(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age) {
        System.out.println(name);
        System.out.println(age);
        return "ok";
    }

    @PostMapping("oneObj")
    public String oneObj(@RequestBody Order order) {
        System.out.println(order);
        return "ok";
    }


    @PostMapping("oneObjOneParam")
    public String oneObjOneParam(@RequestBody Order order,@RequestParam("name") String name) {
        System.out.println(name);
        System.out.println(order);
        return "ok";
    }


    /**
     单独传递时间对象
    */
    @GetMapping("testTime")
    public String testTime(@RequestParam Date date){
        System.out.println(date);
        return "ok";
    }
}

Feign 接口

import com.powernode.domain.Order;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

/**
 * @FeignClient(value = "order-service")
 * value 就是提供者的应用名称
 */
@FeignClient(value = "order-service")
public interface UserOrderFeign {

    /**
     * 你需要调用哪个controller  就写它的方法签名
     * 方法签名(就是包含一个方法的所有的属性)
     *
     * @return
     */
    @GetMapping("doOrder")
    String doOrder();

    @GetMapping("testUrl/{name}/and/{age}")
    public String testUrl(@PathVariable("name") String name, @PathVariable("age") Integer age);

    @GetMapping("oneParam")
    public String oneParam(@RequestParam(required = false) String name);

    @GetMapping("twoParam")
    public String twoParam(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age);

    @PostMapping("oneObj")
    public String oneObj(@RequestBody Order order);

    @PostMapping("oneObjOneParam")
    public String oneObjOneParam(@RequestBody Order order, @RequestParam("name") String name);

    @GetMapping("testTime")
    public String testTime(@RequestParam Date date);
}

创建 TestController 类

import com.powernode.domain.Order;
import com.powernode.feign.UserOrderFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;

@RestController
public class UserController {

    /**
     * 接口是不能做事情的
     * 如果想做事 必须要有对象
     * 那么这个接口肯定是被创建出代理对象的
     * 动态代理 jdk(java interface 接口 $Proxy )  cglib(subClass 子类)
     * jdk动态代理 只要是代理对象调用的方法必须走 java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])
     */
    @Autowired
    public UserOrderFeign userOrderFeign;

    /**
     * 总结
     * 浏览器(前端)-------> user-service(/userDoOrder)-----RPC(feign)--->order-service(/doOrder)
     * feign的默认等待时间时1s
     * 超过1s就在直接报错超时
     *
     * @return
     */
    @GetMapping("userDoOrder")
    public String userDoOrder() {
        System.out.println("有用户进来了");
        // 这里需要发起远程调用
        String s = userOrderFeign.doOrder();
        return s;
    }


    @GetMapping("testParam")
    public String testParam(){
        String cxs = userOrderFeign.testUrl("cxs", 18);
        System.out.println(cxs);

        String t = userOrderFeign.oneParam("老唐");
        System.out.println(t);

        String lg = userOrderFeign.twoParam("雷哥", 31);
        System.out.println(lg);

        Order order = Order.builder()
                .name("牛排")
                .price(188D)
                .time(new Date())
                .id(1)
                .build();

        String s = userOrderFeign.oneObj(order);
        System.out.println(s);

        String param = userOrderFeign.oneObjOneParam(order, "稽哥");
        System.out.println(param);
        return "ok";
    }

    /**
     * Sun Mar 20 10:24:13 CST 2022
     * Mon Mar 21 00:24:13 CST 2022  +- 14个小时
     * 1.不建议单独传递时间参数
     * 2.转成字符串   2022-03-20 10:25:55:213 因为字符串不会改变
     * 3.jdk LocalDate 年月日    LocalDateTime 会丢失s
     * 4.改feign的源码
     *
     * @return
     */
    @GetMapping("time")
    public String time(){
        Date date = new Date();
        System.out.println(date);
        String s = userOrderFeign.testTime(date);

        LocalDate now = LocalDate.now();
        LocalDateTime now1 = LocalDateTime.now();

        return s;
    }

}

时间日期参数问题

使用 feign 远程调用时,传递 Date 类型,接收方的时间会相差 14 个小时,是因为时区造成的。

处理方案:

  1. 使用字符串传递参数,接收方转换成时间类型(推荐使用)不要单独传递时间

  2. 使用 JDK8 的 LocalDate(日期) 或 LocalDateTime(日期和时间,接收方只有秒,没有毫秒)

  3. 自定义转换方法

传参总结:
get 请求只用来传递基本参数 而且加注解@RequestParam
post 请求用来传递对象参数 并且加注解@RequestBody

7.OpenFeign 源码分析

7.1 OpenFeign 的原理是什么?

根据上面的案例,我们知道 feign 是接口调用,接口如果想做事,必须要有实现类,可是我们并没有写实现类,只是加了一个@FeignClient(value="xxx-service")的注解。

所以我们猜测 feign 帮我们创建了代理对象,然后完成真实的调用。
动态代理 1jdk (invoke) 2cglib 子类继承的

  1. 给接口创建代理对象(启动扫描)

  2. 代理对象执行进入 invoke 方法

  3. 在 invoke 方法里面做远程调用
    具体我们这次的流程:

A. 扫描注解得到要调用的服务名称和 url

B. 拿到 provider-order-service/doOrder,通过 ribbon 的负载均衡拿到一个服务,
provider-order-service/doOrder --->http://ip:port/doOrder

C. 发起请求,远程调用

7.2 OpenFeign 的内部是如何实现

7.2.1 如何扫描注解@FeignClient

查看启动类的@EnableFeignClients

进入 FeignClientsRegistrar 这个类 去查看里面的东西

真正的扫描拿到注解和服务名称

7.2.2 如何创建代理对象去执行调用?

当我们启动时,在 ReflectiveFeign 类的 newInstance 方法,给接口创建了代理对象。

ReflectiveFeign 类中的 invoke 方法帮我们完成调用

SynchronousMethodHandler 的 invoke 中给每一个请求创建了一个 requestTemplate 对象,去执行请求。

executeAndDecode

我们去看 LoadBalancerFeignClient 的 execute 方法

executeWithLoadBalancer 继续往下看

8.OpenFeign 的日志功能

从前面的测试中我们可以看出,没有任何关于远程调用的日志输出,如请求头,参数,OpenFeign 的调用默认是不打日志的。Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而揭开 Feign 中 Http 请求的所有细节。

8.1OpenFeign 的日志级别

  • NONE 不打日志,默认值
  • BASIC 只记录 method、url、响应码,执行时间
  • HEADERS 只记录请求和响应的 header
  • FULL 全部都记录

8.2 配置类方式

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {
    @Bean
    Logger.Level feignLogger() {
        return Logger.Level.FULL;
    }
}

8.3 配置文件方式

logging:
	level:
		com.hguo.feignclient.feign.OrderFeign: info  # 打印这个接口下面的日志

feign:
  client:
    config:
      default: # 项目全局
        loggerLevel: HEADERS
      order-service: #@FeignClient注解中配置的服务名
        loggerLevel: FULL

上面修改了 openfeign 的日志级别是 debug,但是 springboot 默认日志级别是 info,因为 debug<info,所以需要也改为debug,openfeign 的日志才会生效

logging:
    level:
    	com.hguo.feignclient.feign.OrderFeign: debug

9.文件上传

@PostMapping(value = "/upload-file")
public String handleFileUpload(@RequestPart(value = "file") MultipartFile file) {
    // File upload logic
}

public class FeignSupportConfig {
    @Bean
    public Encoder multipartFormEncoder() {
        return new SpringFormEncoder(new SpringEncoder(new ObjectFactory<HttpMessageConverters>() {
            @Override
            public HttpMessageConverters getObject() throws BeansException {
                return new HttpMessageConverters(new RestTemplate().getMessageConverters());
            }
        }));
    }
}

@FeignClient(name = "file", url = "http://localhost:8081", configuration = FeignSupportConfig.class)
public interface UploadClient {
    @PostMapping(value = "/upload-file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    String fileUpload(@RequestPart(value = "file") MultipartFile file);
}

10.性能优化

替换默认通信组件

OpenFeign 默认使用 Java 自带的 URLConnection 对象创建 HTTP 请求,但接入生产时,如果能将底层通信组件更换为 Apache HttpClient、OKHttp 这样的专用通信组件,基于这些组件自带的连接池,可以更好地对 HTTP 连接对象进行重用与管理。

<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-okhttp</artifactId>
</dependency>

<!-- 或者添加 httpclient 框架依赖 -->
<dependency>
   <groupId>io.github.openfeign</groupId>
   <artifactId>feign-httpclient</artifactId>
</dependency>

然后在配置文件中加入如下:

feign:
  okhttp:
    enabled: true
    
# 或者
feign:
  httpclient:
    enabled: true

经过上面设置已经可以使用okhttp了,因为在FeignAutoConfiguration中已实现自动装配

11.数据压缩

在 OpenFeign 中,默认并没有开启数据压缩功能。但如果你在服务间单次传递数据超过 1K 字节,强烈推荐开启数据压缩功能。默认 OpenFeign 使用 Gzip 方式压缩数据,对于大文本通常压缩后尺寸只相当于原始数据的 10%~30%,这会极大提高带宽利用率。,在项目配置文件 application.yml 中添加以下配置:

feign:
  compression:
    request:
      enabled: true  # 开启请求数据的压缩功能
      mime-types: text/xml,application/xml, application/json  # 压缩类型
      min-request-size: 1024  # 最小压缩值标准,当数据大于 1024 才会进行压缩
    response:
      enabled: true  # 开启响应数据压缩功能

Tip提醒: 如果应用属于计算密集型,CPU 负载长期超过 70%,因数据压缩、解压缩都需要 CPU 运算,开启数据压缩功能反而会给 CPU 增加额外负担,导致系统性能降低,这是不可取的。这种情况 建议不要开启数据的压缩功能

12.负载均衡

OpenFeign 使用时默认引用 Ribbon 实现客户端负载均衡,它默认的负载均衡策略是轮询策略。那如何设置 Ribbon 默认的负载均衡策略呢?

只需在 application.yml 中调整微服务通信时使用的负载均衡类即可。

warehouse-service: #服务提供者的微服务ID
  ribbon:
    #设置对应的负载均衡类
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

Tip提醒: 出于性能方面的考虑,我们可以选择用权重策略或区域敏感策略来替代轮询策略,因为这样的执行效率最高。

posted @ 2023-06-11 19:22  Lz_蚂蚱  阅读(131)  评论(0)    收藏  举报