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);
十、注意事项
name必填 :即使通过url指定了绝对地址,@FeignClient的name也不能省略(它同时是 Bean 名称)。- 配置类不要加
@Configuration:只在configuration属性中引用,否则会变成全局配置,影响项目内所有 Feign 客户端。 - 日志要两级生效 :
Logger.Level.FULL只是设了级别,还需logging.level.{Feign接口全类名}=debug才会真正输出。 Content-Type兼容 :第三方返回text/plain但内容是 JSON 时,必须自定义Decoder(见第五章),否则反序列化会失败。- 超时与重试 :Feign 默认超时较短,务必显式配置
connectTimeout/readTimeout。 - 异常处理 :第三方返回非 2xx 时 Feign 会抛
FeignException,建议在 Service 层捕获或统一异常处理,必要时做降级(fallback/ Sentinel)。 - 敏感信息保护 :
app-id/app-secret等不要硬编码,走配置中心或环境变量。 - 开关控制 :第三方接口不稳定或测试环境下,可加一个
enabled开关直接短路,避免无效请求。 - 字段命名差异 :第三方用下划线命名(
request_id)时,用@JsonProperty映射,避免手工转换。 - JSON 字符串参数 :第三方要求某字段传「字符串形式的 JSON」时,需用
JSON.toJSONString(...)转换后再塞入 DTO。