使用OpenFeign实现微服务间通信

摘要

本文介绍了OpenFeign在微服务中的作用、特点及用法,并通过代码展示如何基于SpringCloudAlibaba2021.0.5.0+OpenFeign3.1.8+Nacos2.2.0实现微服务之间的接口远程调用。

认识OpenFeign

OpenFeign是SpringCloud生态中的一个声明式HTTP客户端,用于微服务之间的通信。结合服务发现(如Eureka/Nacos)、负载均衡(Ribbon)和熔断机制(Hystrix)等功能,可实现微服务高效交互。

特点

  • 与Eureka、Nacos等注册中心无缝集成,实现服务注册与发现。
  • 通过接口和注解定义远程服务调用逻辑,替代传统的RestTemplate或HttpClient手动构造请求。
  • 集成Ribbon或SpringCloud LoadBalancer,支持客户端负载均衡策略。
  • 支持Hystrix,当服务不可用时自动触发降级逻辑。
  • 支持Java对象与HTTP请求体/响应体的自动转换(如JSON/XML)。
  • 可配置详细日志级别(DEBUG/INFO),便于调试和监控。

常用注解

  • @FeignClient声明Feign客户端,指定目标服务名称value、路径url、Spring容器标识contextId、降级机制fallbackFactory等。
  • @GetMapping、@PostMapping定义接口请求方式,value值必须是生产者接口的完整路径。
  • @RequestParam、@RequestBody、@PathVariable、@RequestHeader定义接口参数。

注意

SpringCloudGateway缺少HttpMessageConverters Bean会导致Feign解码失败,需要手动定义HttpMessageConverters转换器。

代码示例

父模块pom.xml

xml 复制代码
<modules>
    <module>feign-consumer</module>
    <module>feign-producer</module>
</modules>

<!--依赖版本管理-->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2021.0.8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2021.0.5.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

生产者模块feign-producer

1)引入依赖pom.xml

xml 复制代码
<dependencies>
    <!--nacos 注册中心 客户端依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--网关依赖 路由转发+请求限流+身份认证+负载均衡-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!-- SpringCloudLoadbalancer 负载均衡-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    <!-- Spring Cloud OpenFeign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

2)配置文件application.yml

yaml 复制代码
server:
  port: 12005 #服务端口

spring:
  application:
    name:feign-producer   #服务名
  servlet:
    context-path: /
  cloud:
    nacos:
      server-addr: localhost:8848 #nacos服务端地址,默认8848
      discovery:
        ephemeral:true   #默认是临时实例

3)启动类

java 复制代码
package org.coffeebeans;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * <li>ClassName: ProducerApplication </li>
 * <li>Author: OakWang </li>
 */
@Slf4j
@EnableDiscoveryClient
@SpringBootApplication
public class ProducerApplication {
    public static void main(String[] args) {
       SpringApplication springApplication = new SpringApplication(ProducerApplication.class);
       springApplication.run(args);
       log.info("ProducerApplication start success!");
    }
}

4)请求接口

typescript 复制代码
package org.coffeebeans.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.Date;

/**
 * <li>ClassName: ProducerController </li>
 * <li>Author: OakWang </li>
 */
@Slf4j
@RestController
@RequestMapping("/producer")
public class ProducerController {

    /**
     * 获取实时时间
     */
    @GetMapping(value = "/api1")
    public String api1() {
       return "随机数:" + Math.random();
    }

    @PostMapping(value = "/api2")
    public String api2(@RequestBody Object paramObject) {
       return "对象:" + paramObject;
    }

    @GetMapping(value = "/api3")
    public String api3(@RequestParam("param") String param) {
       return "单参:" + param;
    }

    @GetMapping(value = "/api4/{id}")
    public String api4(@PathVariable Long id) {
       return "变量:" + id;
    }

}

消费者模块feign-consumer

1)引入依赖pom.xml

xml 复制代码
<dependencies>
    <!--nacos 注册中心 客户端依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- SpringCloudLoadbalancer 负载均衡-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    <!-- Spring Cloud OpenFeign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--网关依赖 路由转发+请求限流+身份认证+负载均衡-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!-- Jackson JSON 库(用于序列化/反序列化) -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

2)配置文件application.yml

yaml 复制代码
server:
  port:12006   #服务端口

spring:
 application:
    name: feign-consumer    #作为消费者使用
 servlet:
    context-path: /
 cloud:
    nacos:
      server-addr: localhost:8848 #nacos服务端地址,默认8848
      discovery:
        ephemeral: true   #默认是临时实例
    loadbalancer:
      ribbon:
        enabled:true #启动SpringCloudLoadbalancer

# 超时配置
feign:
 client:
    config:
      default:
        connectTimeout:5000
        readTimeout:5000
        
# Feign日志级别
logging:
 level:
    feign.client: DEBUG

3)启动类

java 复制代码
package org.coffeebeans;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * <li>ClassName: ConsumerApplication </li>
 * <li>Author: OakWang </li>
 */
@Slf4j
@EnableFeignClients// 指定 Feign 接口包路径
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {

    public static void main(String[] args) {
       SpringApplication springApplication = new SpringApplication(ConsumerApplication.class);
       springApplication.run(args);
       log.info("ConsumerApplication start success!");
    }

}

4)openfeign接口定义

less 复制代码
package org.coffeebeans.service;

import org.coffeebeans.fallback.ProducerClientFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

//@FeignClient声明Feign客户端,指定目标服务名称value、路径url、Spring容器标识contextId、降级机制fallbackFactory
@FeignClient(contextId = "myFeignClient", value = "feign-producer", url = "http://localhost:12005", fallbackFactory = ProducerClientFallbackFactory.class)
public interface MyFeignClient {

    @GetMapping(value = "/producer/api1")
    String api1();

    @PostMapping(value = "/producer/api2")
    String api2(@RequestBody Object paramObject);

    @GetMapping(value = "/producer/api3")
    String api3(@RequestParam("param") String param);

    @GetMapping(value = "/producer/api4/{id}")
    String api4(@PathVariable("id") Long id);

}

5)降级处理

typescript 复制代码
package org.coffeebeans.fallback;

import lombok.extern.slf4j.Slf4j;
import org.coffeebeans.service.MyFeignClient;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

/**
 * <li>ClassName: ProducerClientFallbackFactory </li>
 * <li>Author: OakWang </li>
 * 降级实现类
 */
@Slf4j
@Component
public class ProducerClientFallbackFactory implements FallbackFactory<MyFeignClient> {
    @Override
    public MyFeignClient create(Throwable cause) {
       return new MyFeignClient() {
          @Override
          public String api1() {
             log.error("feign-consumer 调用 feign-producer 服务失败: {}", cause.getMessage());
             return null;
          }

          @Override
          public String api2(Object paramObject) {
             log.error("feign-consumer 调用 feign-producer 服务失败: {}", cause.getMessage());
             return null;
          }

          @Override
          public String api3(String param) {
             log.error("feign-consumer 调用 feign-producer 服务失败: {}", cause.getMessage());
             return null;
          }

          @Override
          public String api4(Long id) {
             log.error("feign-consumer 调用 feign-producer 服务失败: {}", cause.getMessage());
             return null;
          }
       };
    }
}

6)接口调用

kotlin 复制代码
package org.coffeebeans.controller;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.coffeebeans.service.MyFeignClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <li>ClassName: ConsumerController </li>
 * <li>Author: OakWang </li>
 */
@Slf4j
@RestController
@RequestMapping("/consumer")
public class ConsumerController {

    @Autowired
    private MyFeignClient myFeignClient;  //引入openfeign接口

    @GetMapping(value = "/test1")
    public String test1() {
       return "消费者->生产者:" + myFeignClient.api1();
    }

    @GetMapping(value = "/test2")
    public String test2() {
       ParamObject paramObject = new ParamObject();
       paramObject.setParam("我是对象参数");
       return"消费者->生产者:" + myFeignClient.api2(paramObject);
    }

    @Data
    static class ParamObject {
       private String param;
    }

    @GetMapping(value = "/test3")
    public String test3() {
       return "消费者->生产者:" + myFeignClient.api3("单参");
    }

    @GetMapping(value = "/test4")
    public String test4() {
       return "消费者->生产者:" + myFeignClient.api4(2L);
    }

}

7)报错处理

arduino 复制代码
原因:
Stringget ProducerTime4();返回使用text/plain,需自定义转换器。
Feign默认使用Spring Decoder进行响应解码,而Spring Decoder依赖HttpMessageConverters。
SpringCloud Gateway基于WebFlux,缺少HttpMessageConverters Bean导致Feign解码失败,抛出No qualifying bean of type 'HttpMessageConverters'错误。

解决:
自定义转换器 FeignConfig和HttpMsgConverConfig
7.1)消费者 自定义转换器FeignConfig
arduino 复制代码
package org.coffeebeans.config;

import feign.codec.Decoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import java.util.ArrayList;
import java.util.List;

/**
 * <li>ClassName: FeignConfig </li>
 * <li>Author: OakWang </li>
 */
@Configuration
public class FeignConfig {

    /**
     * 配置Feign客户端的Decoder bean
     * 用于解码响应数据为ResponseEntity对象
     */
    @Bean
    public Decoder feignDecoder() {
        return new ResponseEntityDecoder(new SpringDecoder(feignHttpMessageConverter()));
    }

    /**
     * 配置Feign使用的HTTP消息转换器工厂
     * 返回一个包含自定义Jackson消息转换器的HttpMessageConverters对象
     */
    public ObjectFactory<HttpMessageConverters> feignHttpMessageConverter() {
        final HttpMessageConverters httpMessageConverters = new HttpMessageConverters(new GateWayMappingJackson2HttpMessageConverter());
        return () -> httpMessageConverters;
    }

    /**
     * 自定义Jackson消息转换器
     * 设置支持的媒体类型(text/html和application/json),并指定字符集为UTF-8
     */
    public static class GateWayMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
        GateWayMappingJackson2HttpMessageConverter(){
            List<MediaType> mediaTypes = new ArrayList<>();
            mediaTypes.add(MediaType.valueOf(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8"));
            mediaTypes.add(MediaType.valueOf(MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8"));
            setSupportedMediaTypes(mediaTypes);
        }
    }
}
7.2)消费者 自定义转换器HttpMsgConverConfig
kotlin 复制代码
package org.coffeebeans.config;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import reactor.core.publisher.Mono;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * <li>ClassName: HttpMsgConverConfig </li>
 * <li>Author: OakWang </li>
 */
@Configuration
public class HttpMsgConverConfig {

    /**
     * 定义一个Bean,用于从请求中提取客户端的IP地址作为限流的Key。
     * KeyResolver是Spring Cloud Gateway提供的接口,用于动态解析限流的Key。
     */
    @Bean
    KeyResolver userKeyResolver() {
        return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress());
    }

    /**
     * 定义一个Bean,用于配置HTTP消息转换器。
     * ConditionalOnMissingBean表示只有在容器中不存在同类型Bean时才创建。
     * HttpMessageConverters是Spring Boot中用于管理HTTP消息转换器的工具类。
     */
    @Bean
    @ConditionalOnMissingBean
    public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
        return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
    }
}

测试结果

1)启动nacos

2)启动生产者和消费者

3)调用消费者接口

总结

以上我们了解了OpenFeign在微服务中扮演的角色和作用,并通过代码实现了openfeign在微服务中的基本通信。本文中的代码示例是将feign接口和消费者集成在一起做展示,也可单独起一个模块来集中处理转发逻辑,但需要生产者和消费者都引入此模块。

关注公众号:咖啡Beans

在这里,我们专注于软件技术的交流与成长,分享开发心得与笔记,涵盖编程、AI、资讯、面试等多个领域。无论是前沿科技的探索,还是实用技巧的总结,我们都致力于为大家呈现有价值的内容。期待与你共同进步,开启技术之旅。

相关推荐
我不是混子1 小时前
说说单例模式
java
间彧4 小时前
SimpleDateFormat既然不推荐使用,为什么java 8+中不删除此类
java
间彧4 小时前
DateTimeFormatter相比SimpleDateFormat在性能上有何差异?
java
间彧4 小时前
为什么说SimpleDateFormat是经典的线程不安全类
java
MacroZheng4 小时前
横空出世!MyBatis-Plus 同款 ES ORM 框架,用起来够优雅!
java·后端·elasticsearch
用户0332126663675 小时前
Java 查找并替换 Excel 中的数据:详细教程
java
间彧5 小时前
ThreadLocal实现原理与应用实践
java
若水不如远方5 小时前
Netty的四种零拷贝机制:深入原理与实战指南
java·netty