OpenFeign 入门与实战:快速搭建 Spring Cloud 微服务客户端

1. 前言

随着微服务架构的流行,服务之间的通信变得越来越重要。Spring Cloud 提供了一系列工具来帮助开发者构建分布式系统,其中 OpenFeign 是一个轻量级的 HTTP 客户端,它简化了 Web 服务客户端的开发。本文将介绍如何在 Spring Cloud 应用中使用 OpenFeign 来进行服务间的调用,并解决一些常见的配置问题。

2. 什么是 OpenFeign?

OpenFeign 是 Spring Cloud 的一部分,它是一个声明式的 Web 服务客户端。通过使用 OpenFeign,开发者可以以声明的方式编写 Web 服务客户端接口,而不需要创建模板化的 HTTP 请求。Feign 内置了 Ribbon,用于实现客户端负载均衡,使得调用服务注册中心中的服务变得更加容易。

官方地址:https://github.com/OpenFeign/feign

本篇文章示例代码:https://github.com/kerrsixy/cloud-demo

3. 快速入门

以调用user-service模块的 /user/{id} 接口为例。

3.1 调用端引入依赖

在order-service服务的pom文件中引入feign的依赖:

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

3.2 调用端添加注解

在order-service的启动类添加注解开启Feign的功能:

java 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

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

3.3 编写Feign的客户端

java 复制代码
import com.zjp.orderservice.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "user-service")
public interface UserClient {
    @GetMapping("/user/{id}")
    User getUser(@PathVariable("id") Long id);
}

如果该类里都是以/user开头的可以改为:

java 复制代码
import com.zjp.orderservice.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "user-service", path = "/user")
public interface UserClient {
    @GetMapping("/{id}")
    User getUser(@PathVariable("id") Long id);
}

如果没有配置中心或配置中心有多个同名服务需要指定url,则需要额外配置url:

java 复制代码
import com.zjp.orderservice.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "user-service", url = "http://localhost:8080", path = "/user")
public interface UserClient {
    @GetMapping("/{id}")
    User getUser(@PathVariable("id") Long id);
}

这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如:

  • 服务名称:user-service
  • 请求方式:GET
  • 请求路径:/user/{id}
  • 请求参数:Long id
  • 返回值类型:User

这样,Feign就可以帮助我们发送http请求。

注意:

  • 如果没有指定url,则name要写为服务名。
  • 如果指定了url,则name无论起什么名字都行,但是不能为null。

3.4. 测试

java 复制代码
import com.zjp.orderservice.entity.User;
import com.zjp.feginservice.client.UserClient;
import com.zjp.orderservice.entity.Order;
import com.zjp.orderservice.mapper.OrderMapper;
import com.zjp.orderservice.service.OrderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private UserClient userClient;

    @Override
    public User getOrderById(Long id) {
        User user = userClient.getUser(id);
        return user;
    }
}

3.5. 总结

使用Feign的步骤:

  1. 引入依赖
  2. 添加@EnableFeignClients注解
  3. 编写FeignClient接口
  4. 使用FeignClient中定义的方法代替RestTemplate

4. 自定义配置

Feign可以支持很多的自定义配置,如下表所示:

|------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 类型 | 作用 | 说明 |
| feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL |
| feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 |
| feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
| feign. Contract | 支持的注解格式 | 默认是SpringMVC的注解,原生注解:GitHub - OpenFeign/feign: Feign makes writing java http clients easier |
| feign. Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |

一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。

下面以日志为例来演示如何自定义配置。

4.1 配置文件方式

  1. 针对单个服务:
ruby 复制代码
feign:  
  client:
    config: 
      user-service: # 针对某个微服务的配置
        logger-level: FULL #  日志级别
  1. 针对所有服务:
ruby 复制代码
feign:  
  client:
    config: 
      default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
        logger-level: FULL #  日志级别

日志的级别分为四种:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

4.2. Java代码方式

也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象:

java 复制代码
import feign.Logger;
import org.springframework.context.annotation.Bean;

public class DefaultFeignConfiguration {
    @Bean
    public Logger.Level feignLogLevel() {
        return Logger.Level.BASIC; // 日志级别为BASIC
    }
}

如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中,例如:

java 复制代码
import com.zjp.orderservice.config.DefaultFeignConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

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

如果是局部生效,则把它放到对应的@FeignClient这个注解中,例如:

java 复制代码
import com.zjp.orderservice.entity.User;
import com.zjp.orderservice.config.DefaultFeignConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "user-service",configuration = DefaultFeignConfiguration.class)
public interface UserClient {
    @GetMapping("/user/{id}")
    User getUser(@PathVariable("id") Long id);
}

注意:

需要将feign包下的日志级别设置为DEBUG,feign的日志才能在控制台输出,例如:

Haskell 复制代码
logging:
  level:
    com.zjp.orderservice.client: DEBUG

5. Feign的原理

6. Feign使用优化

6.1 http连接池

Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:

  • URLConnection:默认实现,不支持连接池
  • Apache HttpClient :支持连接池
  • OKHttp:支持连接池

因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。

这里我们用Apache的HttpClient来演示。

  1. 引入依赖

在order-service的pom文件中引入Apache的HttpClient依赖:

XML 复制代码
<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-httpclient</artifactId>
</dependency>
  1. 配置连接池

在order-service的application.yml中添加配置:

Haskell 复制代码
feign:
  httpclient:
    enabled: true # 开启feign对HttpClient的支持
    max-connections: 200 # 最大的连接数
    max-connections-per-route: 50 # 每个路径的最大连接数
  1. 校验是否配置成功

在FeignClientFactoryBean中的loadBalance方法中打断点:

Debug方式启动order-service服务,可以看到这里的client,底层就是Apache HttpClient:

6.2 feign超时

Feign的底层用的是Ribbon或者LoadBalancer,我们可以手动设置超时时间。

可以针对单个服务:

Haskell 复制代码
feign:
  client:
    config:
      user-service:
        connect-timeout: 5000 # 请求连接的超时时间
        read-timeout: 5000 # 请求响应的超时时间

可以针对所有服务:

Haskell 复制代码
feign:
  client:
    config:
      default:
        connect-timeout: 5000 # 请求连接的超时时间
        read-timeout: 5000 # 请求响应的超时时间

6.3 总结

Feign的优化:

  1. 日志级别尽量用basic

  2. 使用HttpClient或OKHttp代替URLConnection

1)引入feign-httpClient依赖

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

  1. 修改 OpenFeign 的超时时间,让 OpenFeign 能够正确的处理业务。

7. 最佳实践

在项目开发过程中可以发现,Feign的客户端与服务提供者的controller代码非常相似:

feign客户端:

java 复制代码
@FeignClient(name = "user-service")
public interface UserClient {
    @GetMapping("/user/{id}")
    User getUser(@PathVariable("id") Long id);
}

服务提供者:

java 复制代码
@GetMapping("/user/{id}")
public User getUser(@PathVariable("id") Long id) {
    return userService.getUserById(id);
}

有没有一种办法简化这种重复的代码编写呢?

7.1 继承方式(不推荐)

  1. 定义一个API接口,利用定义方法,并基于SpringMVC注解做声明。

  2. Feign客户端和Controller都集成改接口

优点:

  • 代码简单
  • 实现了代码的共享

缺点:

  • 服务提供方、服务消费方紧耦合
  • 参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解

7.2 抽取方式

将Feign的Client抽取为独立模块,并且把接口有关的实体类、默认的Feign配置都放到这个模块中,提供给所有消费者使用。

例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。

7.2.1. 抽取

  1. 创建新moudle
  1. 在fegin-service中然后引入feign的starter依赖
XML 复制代码
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 在order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到fegin-service项目中。
  1. 删除order-service中编写的UserClient、User、DefaultFeignConfiguration。

7.2.2 在order-service中使用feign-service

  1. 在order-service的pom文件中中引入feign-service的依赖:
XML 复制代码
<dependency>
    <groupId>com.zjp</groupId>
    <artifactId>fegin-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
  1. 确保order-service服务能扫描到fegin-service服务下的包

方式一:指定Feign应该扫描的包

在order-service的启动类加@EnableFeignClients(basePackages = "com.zjp.feginservice.client")

java 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients(basePackages = "com.zjp.feginservice.client")
@SpringBootApplication
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

方式二:指定需要加载的Client接口

在order-service的启动类加@EnableFeignClients(clients = {UserClient.class})

java 复制代码
import com.zjp.feginservice.client.UserClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

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

注意:

order-service项目启动时,启动类会扫描其所在的包及其子包,如果不@EnableFeignClients注解,在order-service项目启动过程中无法扫描fegin-service项目下的包,会出现以下错误:

java 复制代码
***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 1 of constructor in com.zjp.orderservice.service.impl.OrderServiceImpl required a bean of type 'com.zjp.feginservice.client.UserClient' that could not be found.


Action:

Consider defining a bean of type 'com.zjp.feginservice.client.UserClient' in your configuration.

8. Feign的拦截器

在order-service服务中,创建自定义的RequestInterceptor。

java 复制代码
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@Component
public class FeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = servletRequestAttributes.getRequest();
        log.info("===request: {}", requestTemplate.url());
        log.info("Request URL: {}", request.getRequestURL());
        log.info("Request Method: {}", request.getMethod());
        log.info("Request Headers: {}", request.getHeaderNames());
        // 设置请求头
        requestTemplate.header("Authorization", "Bearer token");
        // 将上游请求头全部设置到下游请求中
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            String value = request.getHeader(name);
            requestTemplate.header(name, value);
        }
    }
}

重启测试:

注意:

Feign 默认情况下不会自动传递所有请求头到被调用的服务端。这是因为 Feign 有一个默认的配置,它只会传递一部分特定的请求头。如果你需要传递特定的请求头,需要手动配置。

  1. 默认传递的请求头:

1)Feign 默认会传递一些基本的请求头,如 Accept-Encoding、Connection 和 User-Agent 等。

2)其他请求头需要手动配置才能传递。

  1. 手动配置请求头:

1)在feign接口的注解上添加headers参数,例如:

java 复制代码
import com.zjp.common.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "user-service")
public interface UserClient {
    @GetMapping(value = "/user/{id}", headers = {"Authorization: Hello World!"})
    User getUser(@PathVariable("id") Long id);
}

2)通过参数传递,例如:

java 复制代码
import com.zjp.common.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;

@FeignClient(name = "user-service")
public interface UserClient {
    @GetMapping(value = "/user/{id}")
    User getUser(@RequestHeader("Authorization") String token, @PathVariable("id") Long id);
}

调用时,需要手动传请求头

java 复制代码
import com.zjp.common.entity.User;
import com.zjp.feginservice.client.UserClient;
import com.zjp.orderservice.entity.Order;
import com.zjp.orderservice.mapper.OrderMapper;
import com.zjp.orderservice.service.OrderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final OrderMapper orderMapper;
    private final UserClient userClient;

    @Override
    public Order getOrderById(Long id) {
        Order order = orderMapper.selectOrderById(id);
        User user = userClient.getUser("token",order.getUserId());
        order.setUser(user);
        return order;
    }
}

3)通过拦截器传递请求头

9. Feign的异常捕获

9.1 自定义 ErrorDecoder

在fegin-service模块中自定义ErrorDecoder,例如:

java 复制代码
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CustomErrorDecoder implements ErrorDecoder {
    private final ErrorDecoder defaultErrorDecoder = new Default();

    @Override
    public Exception decode(String s, Response response) {
        if (response.status() > 200) {
            log.error("请求失败");
            return new RuntimeException("请求失败");
        } else {
            return defaultErrorDecoder.decode(s, response);
        }
    }
}

将 CustomErrorDecoder配置到feign接口:

java 复制代码
import com.zjp.common.entity.User;
import com.zjp.feginservice.client.decoder.CustomErrorDecoder;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "user-service", configuration = CustomErrorDecoder.class)
public interface UserClient {
    @GetMapping(value = "/user/{id}")
    User getUser(@PathVariable("id") Long id);
}

注意:

  1. ErrorDecoder与fallback的执行顺序:先走ErrorDecoder拦截器,再走熔断的fallback。
  2. @FeignClient加上decode404 = true这一个参数,Feign对于2XX和404 ,都不会走Fallback了。

10. 常见报错

10.1 feign接口注入失败

  • 包扫描路径问题
  • 没有添加注册中心的依赖,无法从注册中心拉取ip地址和端口;
  • 如果是单元测试,注意junit的版本,如果是4.X的版本需要在测试类上添加@Runwith;
  • 没有做到依赖最小化,加入一些无关依赖;
  • 启动类的包名层级只有一层,如只在com包下。

10.2 feign接口调用失败

  • 检查feign接口的url地址,需要跟controller的完整的url保持一致;

  • 检查feign接口的参数,参数上的@RequestParam注解不能省略;

  • 检查接口的返回值,是否漏掉了泛型信息,如果没加分型,默认元素类型是LinkedHashMap;

  • @EnableFeignClients 注解没有指定需要扫描的包/类;

  • 更新代码却未重启服务;

  • 其他情况需要开启feign日志,逐步排查。

相关推荐
chuanauc5 分钟前
Kubernets K8s 学习
java·学习·kubernetes
一头生产的驴21 分钟前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
YuTaoShao28 分钟前
【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
java·算法·leetcode·矩阵
zzywxc78731 分钟前
AI 正在深度重构软件开发的底层逻辑和全生命周期,从技术演进、流程重构和未来趋势三个维度进行系统性分析
java·大数据·开发语言·人工智能·spring
YuTaoShao3 小时前
【LeetCode 热题 100】56. 合并区间——排序+遍历
java·算法·leetcode·职场和发展
程序员张33 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
llwszx6 小时前
深入理解Java锁原理(一):偏向锁的设计原理与性能优化
java·spring··偏向锁
小马爱打代码6 小时前
微服务外联Feign调用:第三方API调用的负载均衡与容灾实战
微服务·架构·负载均衡
云泽野6 小时前
【Java|集合类】list遍历的6种方式
java·python·list
二进制person7 小时前
Java SE--方法的使用
java·开发语言·算法