【Spring Boot 】通过 OpenFeign 请求第三方接口

Spring Boot 通过 OpenFeign 请求第三方接口

本文档说明在一个全新的 Spring Boot 项目 中,如何用 OpenFeign 以声明式接口的方式调用第三方 HTTP 接口------不写 HttpClient、不手写 URL 拼接,只声明一个接口即可。

文中示例的包名、类名、地址、密钥均为演示用占位值,请按实际项目替换。


一、整体思路

OpenFeign 让调用 HTTP 接口像调用本地方法一样:

复制代码
业务 Service
   │  thirdPartyClient.createOrder(req)
   ▼
ThirdPartyClient(@FeignClient 声明的接口)
   │  1. 按注解拼装 URL / 请求体 / 请求头
   │  2. 由指定的编码器、解码器、日志级别处理
   ▼
第三方 HTTP 接口(POST https://api.example.com/api/v1/orders)

要落地的内容一共 5 块:

# 内容 示例文件
1 引入依赖 pom.xml
2 启动类开启 Feign DemoApplication.java
3 声明 Feign 客户端接口 ThirdPartyClient.java
4 可选:自定义 Feign 配置(日志/编解码) DemoFeignConfig.java
5 请求/响应 DTO + 配置 + 调用示例 DemoRequest.java、DemoResponse.java、OrderService.java

二、引入依赖

在项目 pom.xml 中引入 OpenFeign:

xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>2.2.2.RELEASE</version>
</dependency>

版本需与 Spring Boot 版本匹配(2.2.2.RELEASE 适用于 Spring Boot 2.x;Spring Boot 3.x 请改用对应的 Spring Cloud 版本)。


三、启动类开启 Feign

在启动类上加 @EnableFeignClients,否则 @FeignClient 接口不会被扫描注册:

java 复制代码
package com.example.demo;

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

@EnableFeignClients
@SpringBootApplication
public class DemoApplication {

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

若 Feign 接口不在启动类的同级或子包 下,需指定扫描包:

@EnableFeignClients(basePackages = "com.example.demo.feign")


四、声明 Feign 客户端接口

新建一个接口,用 @FeignClient 标注即可(无需写实现类):

com.example.demo.feign.ThirdPartyClient

java 复制代码
package com.example.demo.feign;

import com.example.demo.dto.DemoRequest;
import com.example.demo.dto.DemoResponse;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * 第三方服务调用客户端
 */
@FeignClient(
        name = "thirdPartyClient",
        url = "${third-party.url}",
        configuration = DemoFeignConfig.class
)
public interface ThirdPartyClient {

    /**
     * POST 请求:创建资源
     * 对应接口:POST https://api.example.com/api/v1/orders
     */
    @PostMapping("/api/v1/orders")
    DemoResponse createOrder(@RequestBody DemoRequest req);

    /**
     * GET 请求:查询资源(路径参数)
     * 对应接口:GET https://api.example.com/api/v1/orders/{orderNo}
     */
    @GetMapping("/api/v1/orders/{orderNo}")
    DemoResponse queryOrder(@PathVariable("orderNo") String orderNo);

    /**
     * GET 请求:查询参数 + 自定义请求头
     */
    @GetMapping("/api/v1/users")
    DemoResponse getUser(@RequestParam("phone") String phone,
                         @RequestHeader("Authorization") String token);
}

@FeignClient 三个属性

属性 说明
name 必填,作为该客户端的 Bean 名称 (即使写了 url 也不能省略)
url 第三方接口的根地址 ,支持占位符 ${third-party.url},便于按环境切换
configuration 该客户端专属的配置类(日志级别、编解码器等)

方法签名规则 :接口方法与第三方接口一一对应,路径写在 @PostMapping / @GetMapping 里,参数用注解声明:

注解 用途
@RequestBody JSON 请求体(POST/PUT)
@RequestParam URL 查询参数 ?key=value
@PathVariable 路径参数 /orders/{orderNo}
@RequestHeader 自定义请求头

五、自定义 Feign 配置(可选)

如果需要详细日志 、或第三方返回的 Content-Type 不是标准的 application/json,可以自定义配置类:

com.example.demo.feign.DemoFeignConfig

java 复制代码
package com.example.demo.feign;

import feign.Logger;
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.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

import java.util.ArrayList;
import java.util.List;

public class DemoFeignConfig {

    /**
     * 日志级别:FULL 会打印请求/响应头与 Body
     */
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

    /**
     * 自定义消息转换器:额外支持把 text/plain 的响应体按 JSON 解析
     */
    @Bean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        List<MediaType> mediaTypes = new ArrayList<>();
        mediaTypes.add(MediaType.TEXT_PLAIN);
        mediaTypes.add(MediaType.APPLICATION_JSON);
        converter.setSupportedMediaTypes(mediaTypes);
        return converter;
    }

    /**
     * 手动配置解码器,确保使用上面自定义的转换器
     */
    @Bean
    public Decoder feignDecoder() {
        MappingJackson2HttpMessageConverter converter = mappingJackson2HttpMessageConverter();
        ObjectFactory<HttpMessageConverters> messageConverters = () -> new HttpMessageConverters(converter);
        return new ResponseEntityDecoder(new SpringDecoder(messageConverters));
    }
}

⚠️ 该类故意不加 @Configuration 注解 :只在 @FeignClient(configuration = ...) 中引用,避免它变成全局默认配置、影响其他 Feign 客户端。

如果第三方接口完全标准、也不需要调试日志,本章可以整章省略 ,@FeignClient 里去掉 configuration 属性即可。


六、请求 / 响应 DTO

第三方接口字段常为下划线命名 (如 request_id),用 @JsonProperty 做映射。

请求体 com.example.demo.dto.DemoRequest

java 复制代码
package com.example.demo.dto;

import lombok.Data;

/**
 * 第三方接口请求体
 */
@Data
public class DemoRequest {

    /**
     * 应用 ID
     */
    private String appId;

    /**
     * 随机字符串
     */
    private String nonce;

    /**
     * 时间戳(格式:yyyyMMddHHmmss)
     */
    private String timestamp;

    /**
     * 签名
     */
    private String sign;

    /**
     * 业务数据,第三方要求以 JSON 字符串传递
     */
    private String payload;
}

响应体 com.example.demo.dto.DemoResponse

java 复制代码
package com.example.demo.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.alibaba.fastjson.JSONObject;
import lombok.Data;

/**
 * 第三方接口响应体
 */
@Data
public class DemoResponse {

    /**
     * 响应码(0 表示成功,非 0 为失败)
     */
    private Integer code;

    /**
     * 响应信息
     */
    private String msg;

    /**
     * 请求 ID
     */
    @JsonProperty("request_id")
    private String requestId;

    /**
     * 业务数据(结构不固定时可用 JSONObject 接收)
     */
    private JSONObject data;
}

七、配置项

7.1 YAML 配置

yaml 复制代码
# 第三方接口配置
third-party:
  url: "https://api.example.com"     # 第三方接口根地址
  app-id: "your-app-id"              # 应用 ID
  app-secret: "your-app-secret"      # 应用密钥(用于签名)

# 开启 Feign 日志(配合 Logger.Level 使用)
logging:
  level:
    com.example.demo.feign.ThirdPartyClient: debug

密钥类配置建议走配置中心 / 环境变量,不要提交明文到代码仓库。

7.2 配置绑定类(可选)

需要读取自定义配置项时,用 @ConfigurationProperties 绑定:

java 复制代码
package com.example.demo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@ConfigurationProperties(prefix = "third-party")
@Configuration
@Data
public class ThirdPartyProperties {
    private String url;
    private String appId;
    private String appSecret;
}

7.3 超时配置(建议)

Feign 默认超时较短,建议显式配置:

yaml 复制代码
feign:
  client:
    config:
      thirdPartyClient:          # 对应 @FeignClient 的 name
        connectTimeout: 5000
        readTimeout: 10000
        loggerLevel: full

八、调用示例

在 Service 中直接注入 Feign 接口,像调用本地方法一样调用:

java 复制代码
package com.example.demo.service;

import com.alibaba.fastjson.JSON;
import com.example.demo.config.ThirdPartyProperties;
import com.example.demo.dto.DemoRequest;
import com.example.demo.dto.DemoResponse;
import com.example.demo.feign.ThirdPartyClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@Slf4j
@Service
public class OrderService {

    @Autowired
    private ThirdPartyClient thirdPartyClient;
    @Autowired
    private ThirdPartyProperties properties;

    /**
     * 调用第三方接口创建订单
     */
    public void createOrder(String orderNo) {
        // 1. 组装业务数据(第三方要求以 JSON 字符串传递)
        Map<String, Object> payload = new HashMap<>();
        payload.put("orderNo", orderNo);

        // 2. 生成签名等公共参数
        String nonce = UUID.randomUUID().toString().replace("-", "");
        String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
        String sign = md5(properties.getAppSecret() + nonce + timestamp);

        DemoRequest req = new DemoRequest();
        req.setAppId(properties.getAppId());
        req.setNonce(nonce);
        req.setTimestamp(timestamp);
        req.setSign(sign);
        req.setPayload(JSON.toJSONString(payload));

        log.info("请求第三方接口,参数:{}", JSON.toJSONString(req));

        // 3. 发起请求(等价于一次 HTTP POST)
        DemoResponse res = thirdPartyClient.createOrder(req);
        log.info("第三方接口响应:{}", JSON.toJSONString(res));

        // 4. 判码
        if (res == null || res.getCode() == null || res.getCode() != 0) {
            throw new RuntimeException("调用第三方接口失败:" + (res == null ? "无响应" : res.getMsg()));
        }
    }

    private String md5(String text) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] digest = md.digest(text.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            throw new RuntimeException("生成签名失败", e);
        }
    }
}

九、常用写法补充

java 复制代码
// 1. GET + 查询参数
@GetMapping("/api/v1/orders")
DemoResponse list(@RequestParam("page") int page,
                  @RequestParam("size") int size);

// 2. GET + 路径参数
@GetMapping("/api/v1/orders/{orderNo}")
DemoResponse detail(@PathVariable("orderNo") String orderNo);

// 3. POST + 请求体
@PostMapping("/api/v1/orders")
DemoResponse create(@RequestBody DemoRequest req);

// 4. 自定义请求头(如鉴权 Token)
@GetMapping("/api/v1/users")
DemoResponse getUser(@RequestParam("phone") String phone,
                     @RequestHeader("Authorization") String token);

// 5. 表单提交(application/x-www-form-urlencoded)
@PostMapping(value = "/api/v1/login", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
DemoResponse login(@RequestParam("username") String username,
                   @RequestParam("password") String password);

// 6. 上传文件(multipart/form-data)
@PostMapping(value = "/api/v1/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
DemoResponse upload(@RequestPart("file") MultipartFile file);

十、注意事项

  1. name 必填 :即使通过 url 指定了绝对地址,@FeignClient 的 name 也不能省略(它同时是 Bean 名称)。
  2. 配置类不要加 @Configuration :只在 configuration 属性中引用,否则会变成全局配置,影响项目内所有 Feign 客户端。
  3. 日志要两级生效 :Logger.Level.FULL 只是设了级别,还需 logging.level.{Feign接口全类名}=debug 才会真正输出。
  4. Content-Type 兼容 :第三方返回 text/plain 但内容是 JSON 时,必须自定义 Decoder(见第五章),否则反序列化会失败。
  5. 超时与重试 :Feign 默认超时较短,务必显式配置 connectTimeout / readTimeout。
  6. 异常处理 :第三方返回非 2xx 时 Feign 会抛 FeignException,建议在 Service 层捕获或统一异常处理,必要时做降级(fallback / Sentinel)。
  7. 敏感信息保护 :app-id / app-secret 等不要硬编码,走配置中心或环境变量。
  8. 开关控制 :第三方接口不稳定或测试环境下,可加一个 enabled 开关直接短路,避免无效请求。
  9. 字段命名差异 :第三方用下划线命名(request_id)时,用 @JsonProperty 映射,避免手工转换。
  10. JSON 字符串参数 :第三方要求某字段传「字符串形式的 JSON」时,需用 JSON.toJSONString(...) 转换后再塞入 DTO。
相关推荐
范什么特西1 小时前
亲和路由(inspect)
java
她说可以呀1 小时前
Docker 镜像的两种创建方式
java·分布式·docker
霸道流氓气质1 小时前
AgentFlow 入门与实战:Task-Based 并发、EWMA 动态调度与四道门禁的 Java 生产级编排
java·开发语言
UrbanJazzerati1 小时前
AutoHotkey v2 零基础快速入门 Part 3 像素亮度检测完全指南
后端
浮链序1 小时前
Shopify 把 10 亿商品开放给任意 agent 了:我用零依赖 Python 撸了个带预算闸门的代购网关
后端·代码规范
愿得一猪蹄1 小时前
# Java线程池实战,别再new Thread了
java
ee又momo1 小时前
# 深入理解Java注解,从使用到自定义
java
Qlly1 小时前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝2 小时前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构