1. feign 概述与核心概念
1.1 什么是 feign
feign 是一个声明式的 http 客户端,由 netflix 开发并开源,后来捐赠给 spring 社区,成为 spring cloud 生态中的重要组件。它极大地简化了 http api 调用的编码工作,让开发者能够像调用本地方法一样调用远程服务。
核心特性:
- 声明式 api 定义
- 模板化的请求构建
- 与 spring mvc 注解兼容
- 集成了 ribbon 负载均衡
- 集成了 hystrix 熔断器
- 支持请求/响应压缩
- 灵活的配置机制
1.2 feign 的工作原理
feign 通过动态代理技术,在运行时根据接口定义生成具体的实现类。当调用接口方法时,feign 会将方法参数转换为 http 请求,发送到目标服务,并将响应结果转换回方法返回值。
// 接口定义
@feignclient(name = "user-service")
public interface userservice {
@getmapping("/users/{id}")
user getuserbyid(@pathvariable("id") long id);
}
// 运行时,feign 会生成类似如下的实现
public class userserviceproxy implements userservice {
@override
public user getuserbyid(long id) {
// 1. 构建 http 请求
// 2. 发送请求到 user-service
// 3. 解析响应
// 4. 返回结果
}
}1.3 feign 与其他 http 客户端的对比
| 特性 | feign | resttemplate | webclient | okhttp |
|---|---|---|---|---|
| 声明式支持 | ✅ | ❌ | ❌ | ❌ |
| 负载均衡 | ✅ | 需配合 ribbon | ✅ | ❌ |
| 熔断器集成 | ✅ | 需手动配置 | 需手动配置 | ❌ |
| 配置简便性 | 高 | 中 | 中 | 低 |
| 性能 | 高 | 中 | 高 | 很高 |
| 异步支持 | ✅ (新版本) | ❌ | ✅ | ✅ |
2. spring boot 集成 feign
2.1 环境准备与依赖配置
首先,确保你的 spring boot 项目版本合适。推荐使用 spring boot 2.3+ 和 spring cloud hoxton+。
maven 依赖配置:
<properties>
<java.version>11</java.version>
<spring-boot.version>2.7.0</spring-boot.version>
<spring-cloud.version>2021.0.3</spring-cloud.version>
</properties>
<dependencies>
<!-- spring boot starter web -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>
<!-- spring cloud openfeign -->
<dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-starter-openfeign</artifactid>
</dependency>
<!-- 负载均衡 (spring cloud 2020.0.0 后需要显式引入) -->
<dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-starter-loadbalancer</artifactid>
</dependency>
<!-- 服务发现 (如果使用服务注册中心) -->
<dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-starter-netflix-eureka-client</artifactid>
</dependency>
</dependencies>
<dependencymanagement>
<dependencies>
<dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-dependencies</artifactid>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencymanagement>gradle 依赖配置:
plugins {
id 'org.springframework.boot' version '2.7.0'
id 'io.spring.dependency-management' version '1.0.11.release'
id 'java'
}
ext {
set('springcloudversion', "2021.0.3")
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
}
dependencymanagement {
imports {
mavenbom "org.springframework.cloud:spring-cloud-dependencies:${springcloudversion}"
}
}2.2 启用 feign 客户端
在 spring boot 主应用类上添加 @enablefeignclients 注解:
@springbootapplication
@enablefeignclients
public class application {
public static void main(string[] args) {
springapplication.run(application.class, args);
}
}@enablefeignclients 注解的详细配置:
@enablefeignclients(
basepackages = "com.example.clients", // 扫描的包路径
defaultconfiguration = defaultfeignconfig.class, // 默认配置类
clients = {userservice.class, orderservice.class} // 指定具体的客户端类
)
public class application {
// ...
}2.3 基础配置示例
application.yml 配置:
spring:
application:
name: feign-client-demo
cloud:
loadbalancer:
enabled: true
# feign 配置
feign:
client:
config:
default: # 全局默认配置
connecttimeout: 5000
readtimeout: 5000
loggerlevel: basic
user-service: # 针对特定服务的配置
connecttimeout: 3000
readtimeout: 3000
loggerlevel: full
# 日志配置(显示 feign 请求详情)
logging:
level:
com.example.clients: debug # feign 客户端接口所在包
# eureka 配置(如果使用服务注册中心)
eureka:
client:
service-url:
defaultzone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true3. feign 客户端定义与使用
3.1 基础接口定义
简单的 feign 客户端示例:
@feignclient(
name = "user-service", // 服务名称
url = "http://localhost:8080", // 直接指定 url(可选)
path = "/api/v1" // 统一路径前缀(可选)
)
public interface userserviceclient {
/**
* 根据id查询用户
*/
@getmapping("/users/{userid}")
responseentity<user> getuserbyid(@pathvariable("userid") long userid);
/**
* 查询用户列表
*/
@getmapping("/users")
responseentity<list<user>> getusers(
@requestparam(value = "page", defaultvalue = "0") int page,
@requestparam(value = "size", defaultvalue = "20") int size
);
/**
* 创建用户
*/
@postmapping("/users")
responseentity<user> createuser(@requestbody user user);
/**
* 更新用户
*/
@putmapping("/users/{userid}")
responseentity<user> updateuser(
@pathvariable("userid") long userid,
@requestbody user user
);
/**
* 删除用户
*/
@deletemapping("/users/{userid}")
responseentity<void> deleteuser(@pathvariable("userid") long userid);
}3.2 支持的各种 http 方法
feign 支持 spring mvc 的所有 http 方法注解:
@feignclient(name = "user-service")
public interface userserviceclient {
// get 请求
@getmapping("/users/{id}")
user getuser(@pathvariable long id);
// post 请求
@postmapping("/users")
user createuser(@requestbody user user);
// put 请求
@putmapping("/users/{id}")
user updateuser(@pathvariable long id, @requestbody user user);
// patch 请求
@patchmapping("/users/{id}")
user patchuser(@pathvariable long id, @requestbody map<string, object> updates);
// delete 请求
@deletemapping("/users/{id}")
void deleteuser(@pathvariable long id);
// 自定义 http 方法
@requestmapping(method = requestmethod.head, value = "/users/{id}/exists")
responseentity<void> checkuserexists(@pathvariable long id);
}3.3 参数传递的多种方式
路径参数:
@feignclient(name = "user-service")
public interface userserviceclient {
// 基本路径参数
@getmapping("/users/{userid}")
user getuser(@pathvariable("userid") long id);
// 多个路径参数
@getmapping("/organizations/{orgid}/users/{userid}")
user getuserinorganization(
@pathvariable("orgid") long orgid,
@pathvariable("userid") long userid
);
// 使用正则表达式约束路径参数
@getmapping("/users/{id:[0-9]+}")
user getuserwithregex(@pathvariable("id") long id);
}查询参数:
@feignclient(name = "user-service")
public interface userserviceclient {
// 基本查询参数
@getmapping("/users")
list<user> getusers(@requestparam("role") string role);
// 可选查询参数
@getmapping("/users")
list<user> searchusers(
@requestparam(value = "name", required = false) string name,
@requestparam(value = "email", required = false) string email,
@requestparam(value = "page", defaultvalue = "0") int page,
@requestparam(value = "size", defaultvalue = "20") int size
);
// map 作为查询参数(会展开为 key=value 形式)
@getmapping("/users/search")
list<user> searchusers(@requestparam map<string, object> params);
// 对象作为查询参数(会展开对象的属性)
@getmapping("/users/advanced-search")
list<user> advancedsearch(usersearchcriteria criteria);
}请求头参数:
@feignclient(name = "user-service")
public interface userserviceclient {
// 单个请求头
@getmapping("/users/profile")
userprofile getprofile(@requestheader("authorization") string token);
// 多个请求头
@postmapping("/users")
user createuser(
@requestbody user user,
@requestheader("x-request-id") string requestid,
@requestheader("x-user-role") string userrole
);
// map 形式的多请求头
@getmapping("/users/{id}")
user getuserwithheaders(
@pathvariable long id,
@requestheader map<string, string> headers
);
}请求体参数:
@feignclient(name = "user-service")
public interface userserviceclient {
// 简单对象请求体
@postmapping("/users")
user createuser(@requestbody user user);
// 复杂嵌套对象
@postmapping("/users/batch")
batchresult createusers(@requestbody batchuserrequest request);
// 多部分请求(文件上传)
@postmapping(value = "/users/avatar", consumes = mediatype.multipart_form_data_value)
responseentity<string> uploadavatar(
@requestpart("file") multipartfile file,
@requestpart("metadata") avatarmetadata metadata
);
}3.4 复杂请求示例
综合各种参数的复杂请求:
@feignclient(name = "order-service")
public interface orderserviceclient {
@postmapping(
value = "/v2/organizations/{orgid}/orders",
consumes = mediatype.application_json_value,
produces = mediatype.application_json_value
)
order createorder(
// 路径参数
@pathvariable("orgid") string organizationid,
// 查询参数
@requestparam(value = "validate", defaultvalue = "true") boolean validate,
@requestparam(value = "notify", defaultvalue = "false") boolean notify,
// 请求头
@requestheader("x-api-key") string apikey,
@requestheader("x-request-id") string requestid,
@requestheader("x-user-context") string usercontext,
// 请求体
@requestbody createorderrequest request,
// 时间相关参数(自定义格式)
@requestparam("orderdate")
@datetimeformat(pattern = "yyyy-mm-dd") localdate orderdate
);
@putmapping(
value = "/v2/organizations/{orgid}/orders/{orderid}/items",
consumes = mediatype.application_json_value
)
order updateorderitems(
@pathvariable("orgid") string organizationid,
@pathvariable("orderid") string orderid,
@requestheader("x-api-version") string apiversion,
@requestheader map<string, string> additionalheaders,
@requestbody list<orderitem> items
);
}4. 高级特性与配置
4.1 自定义配置类
创建自定义 feign 配置:
@configuration
public class feignconfig {
/**
* 配置契约(使用默认的 spring mvc 注解)
*/
@bean
public contract feigncontract() {
return new springmvccontract();
}
/**
* 配置编码器
*/
@bean
public encoder feignencoder() {
return new springencoder(new objectfactory<>() {
@override
public httpmessageconverters getobject() {
return new httpmessageconverters(
new mappingjackson2httpmessageconverter()
);
}
});
}
/**
* 配置解码器
*/
@bean
public decoder feigndecoder() {
return new responseentitydecoder(new springdecoder(new objectfactory<>() {
@override
public httpmessageconverters getobject() {
return new httpmessageconverters(
new mappingjackson2httpmessageconverter()
);
}
}));
}
/**
* 配置日志级别
*/
@bean
public logger.level feignloggerlevel() {
return logger.level.full;
}
/**
* 配置错误解码器
*/
@bean
public errordecoder errordecoder() {
return new customerrordecoder();
}
/**
* 配置请求拦截器
*/
@bean
public requestinterceptor requestinterceptor() {
return new customrequestinterceptor();
}
}自定义错误解码器:
public class customerrordecoder implements errordecoder {
private final errordecoder defaulterrordecoder = new default();
@override
public exception decode(string methodkey, response response) {
// 根据 http 状态码返回不同的异常
switch (response.status()) {
case 400:
return new badrequestexception("bad request");
case 401:
return new unauthorizedexception("unauthorized");
case 403:
return new forbiddenexception("forbidden");
case 404:
return new notfoundexception("not found");
case 500:
return new internalservererrorexception("internal server error");
default:
return defaulterrordecoder.decode(methodkey, response);
}
}
}
// 自定义异常类
public class feignclientexception extends runtimeexception {
private final int status;
private final string message;
public feignclientexception(int status, string message) {
super(message);
this.status = status;
this.message = message;
}
// getters...
}
public class notfoundexception extends feignclientexception {
public notfoundexception(string message) {
super(404, message);
}
}
public class badrequestexception extends feignclientexception {
public badrequestexception(string message) {
super(400, message);
}
}自定义请求拦截器:
@component
public class customrequestinterceptor implements requestinterceptor {
private final objectmapper objectmapper;
private final authservice authservice;
public customrequestinterceptor(objectmapper objectmapper, authservice authservice) {
this.objectmapper = objectmapper;
this.authservice = authservice;
}
@override
public void apply(requesttemplate template) {
// 添加认证头
if (authservice.isauthenticated()) {
template.header("authorization", "bearer " + authservice.gettoken());
}
// 添加请求 id 用于追踪
template.header("x-request-id", uuid.randomuuid().tostring());
// 添加时间戳
template.header("x-timestamp", instant.now().tostring());
// 记录请求日志
logrequest(template);
// 对特定请求进行处理
if (template.method().equals("post") || template.method().equals("put")) {
processbody(template);
}
}
private void logrequest(requesttemplate template) {
logger logger = loggerfactory.getlogger(customrequestinterceptor.class);
if (logger.isdebugenabled()) {
logger.debug("feign request: {} {} - headers: {}",
template.method(),
template.url(),
template.headers());
}
}
private void processbody(requesttemplate template) {
if (template.body() != null) {
try {
// 可以对请求体进行加密或签名等处理
string body = new string(template.body());
map<string, object> bodymap = objectmapper.readvalue(body, map.class);
// 添加签名信息
bodymap.put("signature", generatesignature(body));
template.body(objectmapper.writevalueasbytes(bodymap));
} catch (ioexception e) {
throw new runtimeexception("failed to process request body", e);
}
}
}
private string generatesignature(string data) {
// 生成数据签名
return "signature-" + data.hashcode();
}
}4.2 多种配置方式
1. 代码配置方式:
@feignclient(
name = "user-service",
url = "${feign.client.user-service.url:http://localhost:8080}",
configuration = userservicefeignconfig.class,
fallback = userservicefallback.class,
fallbackfactory = userservicefallbackfactory.class
)
public interface userserviceclient {
// ...
}
@configuration
public class userservicefeignconfig {
@bean
public requestinterceptor oauth2feignrequestinterceptor() {
return new oauth2feignrequestinterceptor();
}
@bean
public retryer feignretryer() {
return new retryer.default(100, 1000, 3);
}
@bean
public request.options feignoptions() {
return new request.options(5000, 10000);
}
}2. 属性文件配置方式:
feign:
client:
config:
# 全局默认配置
default:
connecttimeout: 5000
readtimeout: 15000
loggerlevel: basic
retryer: com.example.config.customretryer
errordecoder: com.example.config.customerrordecoder
requestinterceptors:
- com.example.config.customrequestinterceptor
- com.example.config.authrequestinterceptor
# 特定服务配置
user-service:
connecttimeout: 3000
readtimeout: 10000
loggerlevel: full
retryer: com.example.config.userserviceretryer
order-service:
connecttimeout: 10000
readtimeout: 30000
loggerlevel: headers
# http 客户端配置
httpclient:
enabled: true
max-connections: 200
max-connections-per-route: 50
connection-timeout: 2000
connection-timer-repeat: 3000
# okhttp 配置
okhttp:
enabled: false
# 压缩配置
compression:
request:
enabled: true
mime-types: text/xml,application/xml,application/json
min-request-size: 2048
response:
enabled: true
# 熔断器配置
circuitbreaker:
enabled: true
# 负载均衡配置
spring:
cloud:
loadbalancer:
retry:
enabled: true
cache:
enabled: true
capacity: 2564.3 继承与多态支持
基础接口定义:
// 基础 crud 接口
public interface basecrudclient<t, id> {
@getmapping("/{id}")
t findbyid(@pathvariable("id") id id);
@getmapping
list<t> findall();
@postmapping
t create(@requestbody t entity);
@putmapping("/{id}")
t update(@pathvariable("id") id id, @requestbody t entity);
@deletemapping("/{id}")
void delete(@pathvariable("id") id id);
}
// 扩展接口
public interface searchableclient<t, id> extends basecrudclient<t, id> {
@postmapping("/search")
list<t> search(@requestbody searchcriteria criteria);
@getmapping("/exists/{id}")
boolean exists(@pathvariable("id") id id);
}
// 具体实现
@feignclient(name = "user-service")
public interface userserviceclient extends searchableclient<user, long> {
// 扩展用户特定方法
@getmapping("/by-username/{username}")
user findbyusername(@pathvariable("username") string username);
@getmapping("/by-email/{email}")
user findbyemail(@pathvariable("email") string email);
@postmapping("/{userid}/roles")
user assignroles(@pathvariable("userid") long userid, @requestbody list<string> roles);
// 批量操作
@postmapping("/batch")
list<user> createbatch(@requestbody list<user> users);
// 异步方法(新版本支持)
@getmapping("/{id}/async")
completablefuture<user> findbyidasync(@pathvariable("id") long id);
}5. 熔断与降级处理
5.1 fallback 机制
基本 fallback 实现:
@feignclient(
name = "user-service",
fallback = userservicefallback.class
)
public interface userserviceclient {
// 接口方法...
}
@component
public class userservicefallback implements userserviceclient {
private static final logger logger = loggerfactory.getlogger(userservicefallback.class);
@override
public user getuserbyid(long userid) {
logger.warn("userservice fallback triggered for getuserbyid: {}", userid);
// 返回默认用户或空对象
return user.builder()
.id(userid)
.username("fallback-user")
.email("fallback@example.com")
.status("inactive")
.build();
}
@override
public list<user> getusers(int page, int size) {
logger.warn("userservice fallback triggered for getusers");
return collections.emptylist();
}
@override
public user createuser(user user) {
logger.error("userservice fallback triggered for createuser");
throw new serviceunavailableexception("user service is temporarily unavailable");
}
// 实现其他方法...
}5.2 fallbackfactory 机制
fallbackfactory 提供更多上下文信息:
@feignclient(
name = "user-service",
fallbackfactory = userservicefallbackfactory.class
)
public interface userserviceclient {
// 接口方法...
}
@component
@slf4j
public class userservicefallbackfactory implements fallbackfactory<userserviceclient> {
private final metricservice metricservice;
private final circuitbreakerregistry circuitbreakerregistry;
public userservicefallbackfactory(metricservice metricservice,
circuitbreakerregistry circuitbreakerregistry) {
this.metricservice = metricservice;
this.circuitbreakerregistry = circuitbreakerregistry;
}
@override
public userserviceclient create(throwable cause) {
return new userservicefallbackimpl(cause);
}
private class userservicefallbackimpl implements userserviceclient {
private final throwable cause;
public userservicefallbackimpl(throwable cause) {
this.cause = cause;
logfallback(cause);
}
@override
public user getuserbyid(long userid) {
log.warn("fallback triggered for getuserbyid: {}, cause: {}", userid, cause.getmessage());
// 记录指标
metricservice.recordfallback("user-service", "getuserbyid");
// 根据异常类型返回不同的降级结果
if (cause instanceof feignexception) {
feignexception feignexception = (feignexception) cause;
if (feignexception.status() == 404) {
return null; // 对于 404,返回 null 是合适的
}
}
// 返回默认的降级数据
return createfallbackuser(userid);
}
@override
public list<user> getusers(int page, int size) {
log.warn("fallback triggered for getusers, cause: {}", cause.getmessage());
metricservice.recordfallback("user-service", "getusers");
// 返回缓存数据或空列表
return getcachedusers();
}
@override
public user createuser(user user) {
log.error("fallback triggered for createuser, cause: {}", cause.getmessage());
metricservice.recordfallback("user-service", "createuser");
// 对于写操作,可以排队或抛出异常
throw new serviceunavailableexception("user service unavailable: " + cause.getmessage(), cause);
}
private user createfallbackuser(long userid) {
return user.builder()
.id(userid)
.username("temporary-user")
.email("temp@example.com")
.createdat(instant.now())
.build();
}
private list<user> getcachedusers() {
// 从本地缓存获取数据
// 这里返回空列表作为示例
return collections.emptylist();
}
private void logfallback(throwable cause) {
if (cause instanceof feignexception.serviceunavailable) {
log.error("user service is unavailable", cause);
} else if (cause instanceof feignexception.badrequest) {
log.warn("bad request to user service", cause);
} else if (cause instanceof feignexception.internalservererror) {
log.error("user service internal error", cause);
} else {
log.warn("user service call failed", cause);
}
}
}
}5.3 resilience4j 集成
resilience4j 配置:
# resilience4j 配置
resilience4j:
circuitbreaker:
instances:
userservice:
registerhealthindicator: true
slidingwindowsize: 10
minimumnumberofcalls: 5
permittednumberofcallsinhalfopenstate: 3
automatictransitionfromopentohalfopenenabled: true
waitdurationinopenstate: 10s
failureratethreshold: 50
eventconsumerbuffersize: 10
retry:
instances:
userservice:
maxattempts: 3
waitduration: 500ms
enableexponentialbackoff: true
exponentialbackoffmultiplier: 2
timelimiter:
instances:
userservice:
timeoutduration: 5s
cancelrunningfuture: true
bulkhead:
instances:
userservice:
maxconcurrentcalls: 20
maxwaitduration: 10ms
# feign resilience4j 配置
feign:
circuitbreaker:
enabled: true
group:
enabled: true
resilience4j:
enabled: true使用 resilience4j 注解:
@feignclient(name = "user-service", configuration = resilience4jconfig.class)
public interface userserviceclient {
@circuitbreaker(name = "userservice", fallbackmethod = "getuserbyidfallback")
@retry(name = "userservice", fallbackmethod = "getuserbyidfallback")
@timelimiter(name = "userservice", fallbackmethod = "getuserbyidfallback")
@bulkhead(name = "userservice", fallbackmethod = "getuserbyidfallback")
@getmapping("/users/{id}")
completablefuture<user> getuserbyid(@pathvariable long id);
// 降级方法
default completablefuture<user> getuserbyidfallback(long id, throwable throwable) {
log.warn("fallback method called for getuserbyid: {}, exception: {}", id, throwable.getmessage());
return completablefuture.completedfuture(createfallbackuser(id));
}
private user createfallbackuser(long id) {
return user.builder()
.id(id)
.username("fallback-user")
.email("fallback@example.com")
.build();
}
}6. 性能优化与最佳实践
6.1 连接池配置
http 客户端连接池优化:
# apache httpclient 配置
feign:
httpclient:
enabled: true
max-connections: 1000
max-connections-per-route: 500
connection-timeout: 2000
connection-timer-repeat: 3000
time-to-live: 900000
disable-ssl-validation: false
follow-redirects: true
# okhttp 配置
feign:
okhttp:
enabled: true
readtimeout: 60
connecttimeout: 60
writetimeout: 120
followredirects: true
followsslredirects: true
retryonconnectionfailure: true
# 线程池配置
spring:
task:
execution:
pool:
core-size: 20
max-size: 100
queue-capacity: 50
keep-alive: 60s6.2 超时与重试配置
@configuration
public class timeoutretryconfig {
/**
* 自定义重试器
*/
@bean
public retryer feignretryer() {
// 重试间隔 100ms,最大间隔 1s,最大重试次数 3 次
return new retryer.default(100, 1000, 3);
}
/**
* 超时配置
*/
@bean
public request.options feignoptions() {
// 连接超时 5s,读取超时 10s
return new request.options(5000, 10000);
}
/**
* 自定义重试策略
*/
@bean
public retryer customretryer() {
return new retryer() {
private final int maxattempts;
private final long backoff;
int attempt;
public customretryer() {
this(100, 3);
}
public customretryer(long backoff, int maxattempts) {
this.backoff = backoff;
this.maxattempts = maxattempts;
this.attempt = 1;
}
@override
public void continueorpropagate(retryableexception e) {
if (attempt++ >= maxattempts) {
throw e;
}
try {
thread.sleep(backoff);
} catch (interruptedexception ignored) {
thread.currentthread().interrupt();
throw e;
}
}
@override
public retryer clone() {
return new customretryer(backoff, maxattempts);
}
};
}
}6.3 缓存策略
本地缓存实现:
@component
@slf4j
public class feignresponsecache {
private final cache<cachekey, object> cache;
private final objectmapper objectmapper;
public feignresponsecache(objectmapper objectmapper) {
this.objectmapper = objectmapper;
this.cache = caffeine.newbuilder()
.maximumsize(1000)
.expireafterwrite(5, timeunit.minutes)
.recordstats()
.build();
}
/**
* 从缓存获取数据
*/
@suppresswarnings("unchecked")
public <t> t get(string servicename, string method, object[] args, class<t> type) {
cachekey key = new cachekey(servicename, method, args);
object cached = cache.getifpresent(key);
if (cached != null) {
log.debug("cache hit for {}.{}", servicename, method);
try {
// 深度拷贝返回,避免缓存对象被修改
string json = objectmapper.writevalueasstring(cached);
return objectmapper.readvalue(json, type);
} catch (exception e) {
log.warn("failed to deserialize cached object", e);
return null;
}
}
return null;
}
/**
* 存储数据到缓存
*/
public void put(string servicename, string method, object[] args, object result) {
if (result == null) {
return;
}
cachekey key = new cachekey(servicename, method, args);
try {
// 深度拷贝存储,避免后续修改影响缓存
string json = objectmapper.writevalueasstring(result);
object cloned = objectmapper.readvalue(json, result.getclass());
cache.put(key, cloned);
log.debug("cached response for {}.{}", servicename, method);
} catch (exception e) {
log.warn("failed to cache response", e);
}
}
/**
* 清除缓存
*/
public void evict(string servicename, string method) {
cache.invalidateall(
cache.asmap().keyset().stream()
.filter(key -> key.servicename.equals(servicename) && key.method.equals(method))
.collect(collectors.toset())
);
}
/**
* 缓存键
*/
@data
@allargsconstructor
private static class cachekey {
private final string servicename;
private final string method;
private final object[] args;
@override
public boolean equals(object o) {
if (this == o) return true;
if (o == null || getclass() != o.getclass()) return false;
cachekey cachekey = (cachekey) o;
return objects.equals(servicename, cachekey.servicename) &&
objects.equals(method, cachekey.method) &&
arrays.equals(args, cachekey.args);
}
@override
public int hashcode() {
int result = objects.hash(servicename, method);
result = 31 * result + arrays.hashcode(args);
return result;
}
}
}缓存拦截器:
@component
public class cacherequestinterceptor implements requestinterceptor {
private final feignresponsecache responsecache;
private final objectmapper objectmapper;
public cacherequestinterceptor(feignresponsecache responsecache, objectmapper objectmapper) {
this.responsecache = responsecache;
this.objectmapper = objectmapper;
}
@override
public void apply(requesttemplate template) {
// 检查请求是否可缓存
if (iscacheablerequest(template)) {
string cachekey = generatecachekey(template);
template.header("x-cache-key", cachekey);
}
}
private boolean iscacheablerequest(requesttemplate template) {
return "get".equals(template.method()) &&
!template.headers().containskey("cache-control");
}
private string generatecachekey(requesttemplate template) {
try {
map<string, object> keydata = new hashmap<>();
keydata.put("url", template.url());
keydata.put("method", template.method());
keydata.put("headers", template.headers());
keydata.put("queries", template.queries());
return hashing.sha256()
.hashstring(objectmapper.writevalueasstring(keydata), standardcharsets.utf_8)
.tostring();
} catch (exception e) {
return uuid.randomuuid().tostring();
}
}
}7. 实战案例与复杂场景
7.1 微服务间认证传递
认证上下文传递:
@component
public class authenticationforwardinterceptor implements requestinterceptor {
private static final string auth_header = "authorization";
private static final string user_header = "x-user-id";
private static final string roles_header = "x-user-roles";
@override
public void apply(requesttemplate template) {
// 从安全上下文中获取认证信息
authentication authentication = securitycontextholder.getcontext().getauthentication();
if (authentication != null && authentication.isauthenticated()) {
// 传递认证令牌
object credentials = authentication.getcredentials();
if (credentials instanceof string) {
template.header(auth_header, "bearer " + credentials);
}
// 传递用户信息
object principal = authentication.getprincipal();
if (principal instanceof userdetails) {
userdetails userdetails = (userdetails) principal;
template.header(user_header, userdetails.getusername());
// 传递用户角色
string roles = userdetails.getauthorities().stream()
.map(grantedauthority::getauthority)
.collect(collectors.joining(","));
template.header(roles_header, roles);
} else if (principal instanceof string) {
template.header(user_header, (string) principal);
}
}
}
}7.2 文件上传下载
文件上传客户端:
@feignclient(
name = "file-service",
configuration = fileservicefeignconfig.class
)
public interface fileserviceclient {
/**
* 上传单个文件
*/
@postmapping(value = "/files/upload", consumes = mediatype.multipart_form_data_value)
responseentity<fileuploadresponse> uploadfile(
@requestpart("file") multipartfile file,
@requestpart(value = "metadata", required = false) filemetadata metadata
);
/**
* 批量上传文件
*/
@postmapping(value = "/files/upload-batch", consumes = mediatype.multipart_form_data_value)
responseentity<batchfileuploadresponse> uploadfiles(
@requestpart("files") multipartfile[] files,
@requestpart(value = "metadata", required = false) batchfilemetadata metadata
);
/**
* 下载文件
*/
@getmapping(value = "/files/{fileid}/download", produces = mediatype.application_octet_stream_value)
responseentity<resource> downloadfile(@pathvariable string fileid);
/**
* 获取文件信息
*/
@getmapping("/files/{fileid}")
responseentity<fileinfo> getfileinfo(@pathvariable string fileid);
/**
* 删除文件
*/
@deletemapping("/files/{fileid}")
responseentity<void> deletefile(@pathvariable string fileid);
}
@configuration
public class fileservicefeignconfig {
@bean
public encoder feignformencoder() {
return new springformencoder(new springencoder(new objectfactory<>() {
@override
public httpmessageconverters getobject() {
return new httpmessageconverters(
new bytearrayhttpmessageconverter(),
new stringhttpmessageconverter(),
new resourcehttpmessageconverter(),
new mappingjackson2httpmessageconverter()
);
}
}));
}
@bean
public logger.level feignloggerlevel() {
return logger.level.headers;
}
}7.3 流式处理
流式响应处理:
@feignclient(name = "streaming-service")
public interface streamingserviceclient {
/**
* 获取流式数据
*/
@getmapping(value = "/stream/data", produces = mediatype.application_ndjson_value)
flux<datachunk> getstreamdata(
@requestparam("query") string query,
@requestparam(value = "chunksize", defaultvalue = "100") int chunksize
);
/**
* 上传流式数据
*/
@postmapping(value = "/stream/upload", consumes = mediatype.application_ndjson_value)
mono<streamuploadresult> uploadstream(@requestbody flux<datachunk> datastream);
/**
* 双向流式通信
*/
@postmapping(value = "/stream/bidirectional",
consumes = mediatype.application_ndjson_value,
produces = mediatype.application_ndjson_value)
flux<processingresult> bidirectionalstream(@requestbody flux<processingrequest> requests);
}
// 响应式支持配置
@configuration
@enablereactivefeignclients
public class reactivefeignconfig {
@bean
public reactivehttprequestinterceptor logrequestinterceptor() {
return (request, context) -> {
log.debug("reactive feign request: {} {}", request.method(), request.url());
return mono.just(request);
};
}
}8. 监控与调试
8.1 日志配置
详细的日志配置:
logging:
level:
# feign 客户端日志
com.example.clients: debug
# feign 内部日志
feign: debug
org.springframework.cloud.openfeign: debug
# http 客户端日志
org.apache.http: warn
okhttp3: warn
pattern:
console: "%d{yyyy-mm-dd hh:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-mm-dd hh:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/feign-client.log
# 自定义 feign 日志
feign:
client:
config:
default:
loggerlevel: full自定义日志格式化:
@slf4j
public class customfeignlogger extends feign.logger {
private final objectmapper objectmapper = new objectmapper();
@override
protected void log(string configkey, string format, object... args) {
if (log.isdebugenabled()) {
string message = string.format(methodtag(configkey) + format, args);
log.debug(message);
}
}
@override
protected void logrequest(string configkey, level loglevel, request request) {
if (log.isinfoenabled()) {
string requestinfo = string.format(
"feign request: %s %s - headers: %s - body: %s",
request.method(),
request.url(),
request.headers(),
request.body() != null ? new string(request.body()) : "empty"
);
log.info("{} - {}", methodtag(configkey), requestinfo);
}
}
@override
protected response logandrebufferresponse(string configkey,
level loglevel,
response response,
long elapsedtime) throws ioexception {
if (log.isinfoenabled()) {
string responseinfo = string.format(
"feign response: %s - time: %dms - headers: %s",
response.status(),
elapsedtime,
response.headers()
);
log.info("{} - {}", methodtag(configkey), responseinfo);
}
// 记录响应体(需要重新缓冲)
byte[] bodydata = util.tobytearray(response.body().asinputstream());
if (log.isdebugenabled() && bodydata.length > 0) {
try {
object json = objectmapper.readvalue(bodydata, object.class);
string prettyjson = objectmapper.writerwithdefaultprettyprinter().writevalueasstring(json);
log.debug("response body:\n{}", prettyjson);
} catch (exception e) {
log.debug("response body: {}", new string(bodydata));
}
}
return response.tobuilder().body(bodydata).build();
}
}8.2 指标收集
micrometer 指标集成:
@component
public class feignmetricsinterceptor implements requestinterceptor {
private final meterregistry meterregistry;
private final counter successcounter;
private final counter errorcounter;
private final timer requesttimer;
public feignmetricsinterceptor(meterregistry meterregistry) {
this.meterregistry = meterregistry;
// 初始化指标
this.successcounter = counter.builder("feign.client.requests")
.description("number of successful feign requests")
.tag("outcome", "success")
.register(meterregistry);
this.errorcounter = counter.builder("feign.client.requests")
.description("number of failed feign requests")
.tag("outcome", "error")
.register(meterregistry);
this.requesttimer = timer.builder("feign.client.duration")
.description("time taken for feign requests")
.register(meterregistry);
}
@override
public void apply(requesttemplate template) {
// 开始计时
template.header("x-request-start-time", string.valueof(system.currenttimemillis()));
}
/**
* 在响应处理中记录指标
*/
public void recordmetrics(string servicename, string method, int statuscode, long duration) {
tags tags = tags.of(
"service", servicename,
"method", method,
"status", string.valueof(statuscode)
);
if (statuscode >= 200 && statuscode < 400) {
successcounter.increment();
} else {
errorcounter.increment();
}
requesttimer.record(duration, timeunit.milliseconds);
// 记录直方图
distributionsummary.builder("feign.client.duration.histogram")
.tags(tags)
.register(meterregistry)
.record(duration);
}
}8.3 分布式追踪
集成 sleuth 分布式追踪:
spring:
sleuth:
enabled: true
feign:
enabled: true
sampler:
probability: 1.0 # 采样率
zipkin:
base-url: http://localhost:9411
enabled: true
sender:
type: web
# 自定义追踪配置
feign:
tracing:
enabled: true自定义追踪拦截器:
@component
public class customtracinginterceptor implements requestinterceptor {
private final tracer tracer;
private final objectmapper objectmapper;
public customtracinginterceptor(tracer tracer, objectmapper objectmapper) {
this.tracer = tracer;
this.objectmapper = objectmapper;
}
@override
public void apply(requesttemplate template) {
// 获取当前追踪上下文
span currentspan = tracer.currentspan();
if (currentspan != null) {
// 添加追踪头
template.header("x-trace-id", currentspan.context().traceid());
template.header("x-span-id", currentspan.context().spanid());
// 添加自定义业务追踪信息
template.header("x-business-id", generatebusinessid());
template.header("x-client-version", "1.0.0");
// 记录请求信息到 span
currentspan.tag("feign.target.service", template.feigntarget().name());
currentspan.tag("feign.target.url", template.url());
currentspan.tag("feign.target.method", template.method());
// 记录请求头(脱敏后)
map<string, string> safeheaders = sanitizeheaders(template.headers());
currentspan.tag("feign.request.headers",
safeheaders.tostring());
}
}
private string generatebusinessid() {
return uuid.randomuuid().tostring();
}
private map<string, string> sanitizeheaders(map<string, collection<string>> headers) {
map<string, string> safeheaders = new hashmap<>();
headers.foreach((key, values) -> {
if (issensitiveheader(key)) {
safeheaders.put(key, "***");
} else {
safeheaders.put(key, string.join(",", values));
}
});
return safeheaders;
}
private boolean issensitiveheader(string headername) {
string lowerheader = headername.tolowercase();
return lowerheader.contains("auth") ||
lowerheader.contains("token") ||
lowerheader.contains("password") ||
lowerheader.contains("secret") ||
lowerheader.contains("key");
}
}9. 常见问题与解决方案
9.1 性能问题排查
连接池监控:
@component
@slf4j
public class connectionpoolmonitor {
private final httpclientconnectionmanager connectionmanager;
private final meterregistry meterregistry;
public connectionpoolmonitor(httpclientconnectionmanager connectionmanager,
meterregistry meterregistry) {
this.connectionmanager = connectionmanager;
this.meterregistry = meterregistry;
startmonitoring();
}
private void startmonitoring() {
scheduledexecutorservice scheduler = executors.newscheduledthreadpool(1);
scheduler.scheduleatfixedrate(this::monitor, 0, 30, timeunit.seconds);
}
private void monitor() {
try {
poolstats totalstats = connectionmanager.gettotalstats();
routestats routestats = connectionmanager.getroutes().stream()
.findfirst()
.map(connectionmanager::getstats)
.orelse(null);
// 记录指标
gauge("feign.connection.pool.total.connections", totalstats.getavailable() + totalstats.getleased());
gauge("feign.connection.pool.available.connections", totalstats.getavailable());
gauge("feign.connection.pool.leased.connections", totalstats.getleased());
gauge("feign.connection.pool.max.connections", totalstats.getmax());
gauge("feign.connection.pool.pending.connections", totalstats.getpending());
// 记录日志
if (log.isdebugenabled()) {
log.debug("connection pool stats - total: {}, available: {}, leased: {}, max: {}, pending: {}",
totalstats.getavailable() + totalstats.getleased(),
totalstats.getavailable(),
totalstats.getleased(),
totalstats.getmax(),
totalstats.getpending());
}
// 检查连接池健康状态
checkpoolhealth(totalstats);
} catch (exception e) {
log.warn("failed to monitor connection pool", e);
}
}
private void gauge(string name, number value) {
meterregistry.gauge(name, value);
}
private void checkpoolhealth(poolstats stats) {
double utilization = (double) stats.getleased() / stats.getmax();
if (utilization > 0.8) {
log.warn("connection pool utilization is high: {}%", (int)(utilization * 100));
}
if (stats.getpending() > 10) {
log.warn("high number of pending connections: {}", stats.getpending());
}
}
}9.2 超时问题处理
动态超时配置:
@component
public class dynamictimeoutconfig {
private final map<string, request.options> timeoutconfigs = new concurrenthashmap<>();
private final configservice configservice;
public dynamictimeoutconfig(configservice configservice) {
this.configservice = configservice;
loadinitialconfigs();
startconfigrefresh();
}
public request.options gettimeoutconfig(string servicename) {
return timeoutconfigs.getordefault(servicename, getdefaulttimeout());
}
private void loadinitialconfigs() {
// 从配置中心加载超时配置
map<string, servicetimeoutconfig> configs = configservice.gettimeoutconfigs();
configs.foreach((servicename, timeoutconfig) -> {
request.options options = new request.options(
timeoutconfig.getconnecttimeout(),
timeoutconfig.getreadtimeout()
);
timeoutconfigs.put(servicename, options);
});
}
private void startconfigrefresh() {
scheduledexecutorservice scheduler = executors.newscheduledthreadpool(1);
scheduler.scheduleatfixedrate(this::refreshconfigs, 5, 5, timeunit.minutes);
}
private void refreshconfigs() {
try {
loadinitialconfigs();
log.info("refreshed feign timeout configurations");
} catch (exception e) {
log.warn("failed to refresh timeout configurations", e);
}
}
private request.options getdefaulttimeout() {
return new request.options(5000, 10000);
}
}
// 超时配置类
@data
public class servicetimeoutconfig {
private int connecttimeout;
private int readtimeout;
private int retrycount;
private boolean enabled;
}9.3 序列化问题
自定义序列化配置:
@configuration
public class customserializationconfig {
@bean
public objectmapper objectmapper() {
objectmapper mapper = new objectmapper();
// 配置序列化选项
mapper.configure(deserializationfeature.fail_on_unknown_properties, false);
mapper.configure(serializationfeature.write_dates_as_timestamps, false);
mapper.configure(serializationfeature.fail_on_empty_beans, false);
// 注册 javatime 模块
mapper.registermodule(new javatimemodule());
// 配置日期格式
mapper.setdateformat(new simpledateformat("yyyy-mm-dd hh:mm:ss"));
// 配置时区
mapper.settimezone(timezone.gettimezone("asia/shanghai"));
return mapper;
}
@bean
public encoder feignencoder(objectmapper objectmapper) {
return new springencoder(new objectfactory<>() {
@override
public httpmessageconverters getobject() {
return new httpmessageconverters(
new mappingjackson2httpmessageconverter(objectmapper)
);
}
});
}
@bean
public decoder feigndecoder(objectmapper objectmapper) {
return new responseentitydecoder(new springdecoder(new objectfactory<>() {
@override
public httpmessageconverters getobject() {
return new httpmessageconverters(
new mappingjackson2httpmessageconverter(objectmapper)
);
}
}));
}
}10. 总结
spring boot 声明式调用 feign 是一个强大而灵活的 http 客户端工具,它通过声明式接口大大简化了微服务间的通信。本文从基础概念到高级特性,从简单使用到复杂场景,全面介绍了 feign 的各个方面。
关键要点总结:
- 声明式优势:通过接口定义即可完成 http 调用,代码简洁易维护
- 丰富注解:支持 spring mvc 注解,学习成本低
- 灵活配置:支持全局配置、服务特定配置、代码配置、属性配置等多种方式
- 容错机制:完善的 fallback 和熔断器集成,提高系统稳定性
- 性能优化:连接池、超时、重试、缓存等机制保障高性能
- 监控追踪:完善的日志、指标、追踪支持,便于问题排查
- 扩展性强:支持自定义编码器、解码器、拦截器等组件
到此这篇关于spring boot 声明式调用 feign 从入门到精通实例详解的文章就介绍到这了,更多相关spring boot 声明式调用 feign内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论