【SpringCloud】-OpenFeign实战及源码解析、与Ribbon结合

一、背景介绍

二、正文

OpenFeign是什么?

OpenFeign(简称Feign)是一个声明式的Web服务客户端,用于简化服务之间的HTTP通信。与Nacos和Ribbon等组件协同,以支持在微服务体系结构中方便地进行服务间的通信; OpenFeign在默认情况下集成了Hystrix,提供了服务容错和服务降级的功能。

OpenFeign的作用是什么?

按照单一职责,也为了满足可复用、可扩展的核心我们可以对整体业务拆分成不同的服务,这样涉及到的一个问题就是某一个服务的逻辑实现需要依托另一个服务的信息,这样服务和服务之间需要通信来进行消息传递,但是服务和服务之间如果直接通信的话耦合关系变强,无法达到高复用的作用。使用了OpenFeign技术来解决这个问题,如果其中一个类需要调用另一个类的某一个方法的话,直接通过OpenFeign这个第三方转发这个调用,减少直接通信带来的耦合关系
通过使用@FeignClien注解标识UserApiService这个客户端,对【internetbar-provider-user】这个服务远程调用。OpenFeign会根据接口自动创建一个实现类,发起HTTP请求去用Get的方式访问远程服务的【/userManage/getUserInfoById/{userId}】这个接口

Feign和OpenFeign的关系?

OpenFeign是对Feign的增强,对mvc注解的增强,那如何理解这句话呢?

共同点:Feign和OpenFeign都是用于简化服务之间HTTP通信的Web服务客户端

不同点:
所属不同 :Feign是Netflix开发HTTP客户端;OpenFeign是SpringCloud对Feign的重新实现和增强,OpenFeign引入了SpringMVC的注解,例如:@GetMapping等注解可以类似于SpringMVC的一些效果
Hystrix集成 :OpenFeign默认集成Hystrix,提供服务容错和服务降级的功能;Feign中需要显式地添加Hystrix的依赖并配置

Java还有哪些服务调用方式?

Okhttp、HttpURLConnection、RestTemplate

使用OpenFeign和不使用的区别对比?

我们先从代码形式上来看一下两者如何使用:

基于RestTemplate使用HTTP请求调用服务

基于OpenFeign调用

Feign可以根据接口的定义生成客户端所需要的代码,这部分代码被Feign封装到底层,并且底层同步的实现了负载均衡和服务发现、服务容错等功能,不需要我们像使用RestTemplate一样额外的手动去配置,一个简单的API接口就可以帮助我们做了很多事情。


实战

引入依赖

java 复制代码
<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在启动类添加@EnableFeignClients注解

java 复制代码
@EnableFeignClients(defaultConfiguration = FeignAutoConfiguration.class)

添加FeignClient接口

java 复制代码
package cn.itcast.order.clients;

import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @BelongsProject: cloud-demo
 * @BelongsPackage: cn.itcast.order.clients
 * @CreateTime: 2023-03-17  12:29
 * @Description: TODO
 * @Version: 1.0
 */
@FeignClient("userservice")
public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

修改逻辑代码

使用RestTemplate**

java 复制代码
package cn.itcast.order.service;

import cn.itcast.order.clients.UserClient;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate template;

    public Order queryOrderById(Long orderId){
        //1、根据用户id查询用户的订单信息
        Order order = orderMapper.findById(orderId);

        //2、利用RestTemplate发起http请求,查询用户信息
        String url="http://userservice/user/"+order.getUserId();
        User forObject = template.getForObject(url, User.class);

        //3、将user信息封装到Order中
        order.setUser(forObject);

        // 返回
        return order;
    }
}

使用Feign接口

java 复制代码
package cn.itcast.order.service;

import cn.itcast.order.clients.UserClient;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private UserClient userClient;

    public Order queryOrderById(Long orderId) {
        //1、查询订单
        Order order = orderMapper.findById(orderId);
        //2、用Feign远程调用
        User user = userClient.findById(order.getUserId());
        //3、封装user到Order
        order.setUser(user);
        //4、返回
        return order;
    }
}

自定义配置

配置日志(以debug级别输出 )

当我们进行服务调用的过程中有可能出现接口调用失败的问题,或者我们想要输出请求或响应的详细信息,我们可以配置日志

日志级别有哪些?

NONE:不记录任何日志(默认值)

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

HEADERS:记录BASIC级别的基础上,记录请求和响应的header

FULL:记录请求和响应的header、body和元数据

方式一:配置文件

java 复制代码
全局:所有服务生效
feign:
  client:
    config:
      default:
        loggerLevel: FULL
java 复制代码
#某服务生效
feign:
  client:
    config:
      userservice:
        loggerLevel: FULL

方式二:配置类

添加配置类

java 复制代码
package cn.itcast.order.config;

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

/**
 * @BelongsProject: cloud-demo
 * @BelongsPackage: cn.itcast.order.config
 * @CreateTime: 2023-03-17  13:05
 * @Description: TODO
 * @Version: 1.0
 */
public class DefaultFeignConfiguration {

    @Bean
    public Logger.Level feignLogLevel() {
        return Logger.Level.BASIC;
    }
}

方式三:在启动类上添加注解@EnableFeignClients

全局配置:对所有生效

@EnableFeignClients(defaultConfiguration =FeignAutoConfiguration.class)

局部配置:只对userservice服务生效

@EnableFeignClients(value = "userservice", defaultConfiguration = FeignAutoConfiguration.class)


性能优化

底层的客户端实现:

URLConnection:默认实现,不支持连接池

Apache HttpClient:支持连接池

OKHttp:支持连接池

优化包括 :

使用连接池代替默认的URLConnection

日志级别,最好用basic或none


实操------Feign工程化

引入feign-httpClient依赖

java 复制代码
 <dependency>
       <groupId>io.github.openfeign</groupId>
       <artifactId>feign-httpclient</artifactId>
</dependency>

配置文件开启httpClient功能,设置连接池参数

java 复制代码
feign:
  client:
    config:
      default:  #default全局的配置
        loggerLevel: BASIC  #日志级别(基本的请求和响应信息)
  httpclient:
    enabled: true #开启feign对HttpClient的支持
    max-connections: 200  #最大的连接数
    max-connections-per-route: 50 #每个路径的最大连接数

目的:将公共的配置抽取出来

新建一个Maven项目,服务名叫------feign-api

引入依赖

java 复制代码
<?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>cloud-demo</artifactId>
        <groupId>cn.itcast.demo</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>feign-api</artifactId>

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

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

</project>

第一步、公有接口------工程化核心

在项目中创建一个Feign客户端接口,通过使用@FeignClient注解来声明要调用的服务和服务接口

java 复制代码
package cn.itcast.feign.clients;

import cn.itcast.feign.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @BelongsProject: cloud-demo
 * @BelongsPackage: cn.itcast.order.clients
 * @CreateTime: 2023-03-17  12:29
 * @Description: TODO
 * @Version: 1.0
 */
@FeignClient("userservice")
public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

第二步、配置类---用于配置Feign客户端的一些属性,如日志级别等

java 复制代码
package cn.itcast.feign.config;

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

/**
 * @BelongsProject: cloud-demo
 * @BelongsPackage: cn.itcast.order.config
 * @CreateTime: 2023-03-17  13:05
 * @Description: TODO
 * @Version: 1.0
 */
public class DefaultFeignConfiguration {

    @Bean
    public Logger.Level feignLogLevel() {
        return Logger.Level.BASIC;
    }
}

实体类

java 复制代码
package cn.itcast.feign.pojo;

import lombok.Data;

@Data
public class User {
    private Long id;
    private String username;
    private String address;
}

orderservice服务

第三步、启动类---启用Feign客户端

在Spring Boot应用程序中,通过使用@EnableFeignClients注解启用Feign客户端,并指定要扫描的Feign客户端接口所在的包。

java 复制代码
package cn.itcast.order;

import cn.itcast.feign.clients.UserClient;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = UserClient.class,defaultConfiguration = FeignAutoConfiguration.class)

public class OrderApplication {

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

    /**
     * @Description: 将RestTemplate注册到容器中
     * @Date: 2023/3/13 11:28
     * @return: org.springframework.web.client.RestTemplate
     **/
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    /**
     * @Description: 修改负载均衡策略方式
     * @Date: 2023/3/13 19:56
     * @return: com.netflix.loadbalancer.IRule
     **/
    @Bean
    public IRule randomRule() {
        return new RandomRule();
    }
}

实体类

java 复制代码
package cn.itcast.order.pojo;

import cn.itcast.feign.pojo.User;
import lombok.Data;

@Data
public class Order {
    private Long id;
    private Long price;
    private String name;
    private Integer num;
    private Long userId;
    private User user;
}

Mapper类

java 复制代码
package cn.itcast.order.mapper;

import cn.itcast.order.pojo.Order;
import org.apache.ibatis.annotations.Select;

public interface OrderMapper {

    @Select("select * from tb_order where id = #{id}")
    Order findById(Long id);
}

Controller类

java 复制代码
package cn.itcast.order.web;

import cn.itcast.order.pojo.Order;
import cn.itcast.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {
   @Autowired
   private OrderService orderService;

    @GetMapping("/{orderId}")
    public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
        // 根据id查询订单并返回
        return orderService.queryOrderById(orderId);
    }
}

第四步、使用Feign客户端---service类

在需要调用远程服务的地方,注入Feign客户端并使用

java 复制代码
package cn.itcast.order.service;

import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private UserClient userClient;

    public Order queryOrderById(Long orderId) {
        //1、查询订单
        Order order = orderMapper.findById(orderId);
        //2、用Feign远程调用
        User user = userClient.findById(order.getUserId());
        //3、封装user到Order
        order.setUser(user);
        //4、返回
        return order;
    }
}

通过上述的四个步骤(公有接口、配置类、启动类---启用Feign客户端、使用Feign客户端---service类)我们就实现了Feign的工程化,以声明式的方式定义了服务调用接口,配置了Feign客户端

基于工程化的思想,所有的服务调用被抽成公共的API,单独放在一个包中,我们只需要调用公共接口,不需要写太多重复代码,哪个服务需要调用引用对应的包就可以了,避免冗余代码,这也是设计模式思想(可复用)的体现。


集成Hystrix

我们需要先了解雪崩效应,通过OpenFeign进行多服务之间调用,如果服务提供者出现了故障将会导致服务消费者不可用。比方说login服务需要调用user服务获取登录用户的基本信息,此时user服务可能服务挂掉或者被攻击了,此时login服务可能将会处于等待过程,我们可以使用Hystrix进行熔断,当远程服务调用失败或超时时,熔断器会出发,并执行降级逻辑。

关于OpenFeign和Hystrix的集成使用请等待我之后关于Hystrix的博客详解哦~~~


源码解析

1、Spring启动时初始化-生成代理类

在服务启动时,Spring会加载配置并且初始化Spring容器、扫描并加载项目当中用到的组件,此时OpenFeign的配置也会被程序加载(包括:自定义Feign配置类、负载均衡配置、熔断器配置等),扫描带有@FeignClient注解的接口,Feign会使用动态代理生成代理类并注册到Spring容器中

2、服务发现和负载均衡

Nacos注册中心:如果项目中用到了Nacos注册中心,Feign会与Nacos集成通过服务名进行服务发现

Ribbon负载均衡:如果项目中使用了Ribbon,可以实现对服务实例的负载均衡

大家可以看下面这张图片,我们点进OpenFeign的依赖,会发现里面自动集成了Ribbon

那Feign和Ribbon、RestTemplate三者之间的关系是什么样的呢?

3、请求处理

场景:服务A调用服务B

当服务A通过OpenFeign的方式调用服务B时,实际上是通过代理类最终调用LoadBalancerFeignClient的execute方法,execute方法内部整合了Ribbon去实现负载均衡,最终会找到真实要请求的服务地址,从而发送RibbonRequest请求,返回响应内容给服务A
这也是Feign的核心代码:

为什么Feign第一次调用耗时很长?

这个问题是我在网上看到的一个问题,大家也可以参考参考图中说到的原因:


三、总结

OpenFeign帮助我们简化了服务之间的通信,同时继承了负载均衡、熔断器等功能,帮助我们更好的实现服务调用。之后我将会针对OpenFeign中集成的Hystrix做分享,大家敬请期待

如果有想要交流的内容欢迎在评论区进行留言,如果这篇文档受到了您的喜欢那就留下你点赞+收藏+评论脚印支持一下博主~

相关推荐
Rain的Java大神之路6 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
需要8266 天前
MySQL 索引 B+ 树与“查询为什么变慢了
java·spring boot·spring·spring cloud·tomcat
需要8266 天前
Arthas 一个命令排查线上问题
java·jvm·spring·spring cloud
苏渡苇7 天前
Spring Insight 里如何把 Span 画成瀑布时间线
后端·spring·spring cloud·springboot·监控·apm
叶总没有会7 天前
Nacos—注册中心
spring cloud·微服务
苏渡苇8 天前
Spring Insight 里如何把 Span 收成一条链路
spring boot·spring·spring cloud·系统监控·apm
叶总没有会8 天前
微服务--分布式基础
java·spring·spring cloud·微服务
孙启超9 天前
【AI开发之Rust】第 7 课:错误处理 —— panic、Result 与 `?`
人工智能·分布式·后端·爬虫·spring cloud·架构·rust
行百里er10 天前
Spring Insight 里如何把 Span 收成一条链路
spring boot·spring cloud·监控
砍材农夫10 天前
spring|spring web|拦截器、过滤器、aop
java·spring boot·spring·spring cloud