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日志,逐步排查。

相关推荐
轮到我狗叫了27 分钟前
栈的应用,力扣394.字符串解码力扣946.验证栈序列力扣429.N叉树的层序遍历力扣103.二叉树的锯齿形层序遍历
java·算法·leetcode
冰之杍2 小时前
Vscode进行Java开发环境搭建
java·ide·vscode
跳动的梦想家h5 小时前
黑马点评 秒杀下单出现的问题:服务器异常---java.lang.NullPointerException: null(已解决)
java·开发语言·redis
苹果醋35 小时前
前端面试之九阴真经
java·运维·spring boot·mysql·nginx
哎呦没5 小时前
Spring Boot OA:企业办公自动化的高效路径
java·spring boot·后端
真心喜欢你吖5 小时前
Spring Boot与MyBatis-Plus的高效集成
java·spring boot·后端·spring·mybatis
2402_857589365 小时前
企业办公自动化:Spring Boot OA管理系统开发与实践
java·spring boot·后端
G丶AEOM6 小时前
JVM逃逸分析机制
java·jvm
无聊写博客6 小时前
JDK、JRE、JVM的区别
java·开发语言·jvm
message丶小和尚6 小时前
SpringBoot升级全纪录之项目启动
java·spring boot·mybatis