OpenFeign:声明式REST客户端深度解析
概述:什么是OpenFeign?
OpenFeign 是一个声明式的Web Service客户端,它让编写HTTP客户端变得更简单。通过创建接口并添加注解,就可以完成服务提供者的接口绑定,类似于MyBatis的Mapper接口。
核心特点
- 声明式API:只需定义接口,无需实现
- 与Spring Cloud深度集成:支持Spring MVC注解
- 负载均衡:与Ribbon/LoadBalancer无缝集成
- 熔断降级:支持与Sentinel/Hystrix集成
- 可扩展性:支持编码器、解码器、拦截器等
快速开始:基础远程调用
环境准备
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>4.0.0</version>
</dependency>
1. 启用OpenFeign
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients // 启用Feign客户端
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
2. 定义Feign客户端接口
用户服务客户端:
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户服务Feign客户端
* name/value: 指定服务名(注册中心中的服务名)
* url: 直接指定URL(用于测试或指定具体地址)
* path: 所有请求的前缀
*/
@FeignClient(
name = "user-service", // 服务名
path = "/api/users", // 公共路径前缀
configuration = UserFeignConfig.class // 自定义配置
)
public interface UserServiceClient {
/**
* GET请求:获取用户信息
* @param userId 用户ID
* @return 用户信息
*/
@GetMapping("/{userId}")
UserDTO getUserById(@PathVariable("userId") Long userId);
/**
* GET请求带查询参数:分页查询用户
*/
@GetMapping("/list")
PageResult<UserDTO> getUserList(
@RequestParam("page") Integer page,
@RequestParam("size") Integer size,
@RequestParam(value = "name", required = false) String name
);
/**
* POST请求:创建用户
*/
@PostMapping("/create")
ResultDTO<Long> createUser(@RequestBody UserCreateRequest request);
/**
* PUT请求:更新用户
*/
@PutMapping("/update/{userId}")
ResultDTO<Void> updateUser(
@PathVariable("userId") Long userId,
@RequestBody UserUpdateRequest request
);
/**
* DELETE请求:删除用户
*/
@DeleteMapping("/delete/{userId}")
ResultDTO<Void> deleteUser(@PathVariable("userId") Long userId);
/**
* 复杂参数:多个查询参数
*/
@GetMapping("/search")
List<UserDTO> searchUsers(
@RequestParam Map<String, Object> params
);
/**
* 带Header的请求
*/
@GetMapping("/profile")
UserDTO getUserProfile(@RequestHeader("Authorization") String token);
}
3. DTO定义
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class UserDTO {
private Long id;
private String username;
private String email;
private String phone;
private Integer status;
private LocalDateTime createTime;
}
@Data
public class UserCreateRequest {
private String username;
private String password;
private String email;
private String phone;
}
@Data
public class UserUpdateRequest {
private String email;
private String phone;
private Integer status;
}
@Data
public class PageResult<T> {
private Integer page;
private Integer size;
private Long total;
private List<T> list;
}
@Data
public class ResultDTO<T> {
private Integer code;
private String message;
private T data;
public static <T> ResultDTO<T> success(T data) {
ResultDTO<T> result = new ResultDTO<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
}
4. 使用Feign客户端
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private UserServiceClient userServiceClient;
@GetMapping("/{orderId}")
public OrderDTO getOrderDetail(@PathVariable Long orderId) {
// 1. 获取用户信息
UserDTO user = userServiceClient.getUserById(123L);
// 2. 创建订单响应
OrderDTO order = new OrderDTO();
order.setId(orderId);
order.setUserId(user.getId());
order.setUsername(user.getUsername());
order.setStatus(1);
return order;
}
@PostMapping("/create")
public ResultDTO<Long> createOrder(@RequestBody OrderCreateRequest request) {
// 1. 验证用户是否存在
UserDTO user = userServiceClient.getUserById(request.getUserId());
if (user == null) {
return ResultDTO.error(400, "用户不存在");
}
// 2. 创建订单
OrderDTO order = new OrderDTO();
order.setUserId(request.getUserId());
order.setAmount(request.getAmount());
order.setStatus(0);
// 3. 保存订单...
Long orderId = saveOrder(order);
return ResultDTO.success(orderId);
}
@GetMapping("/user-orders/{userId}")
public List<OrderDTO> getUserOrders(@PathVariable Long userId) {
// 获取用户信息
UserDTO user = userServiceClient.getUserById(userId);
// 查询用户的订单
return findOrdersByUserId(userId);
}
}
超时控制
1. 全局配置
# application.yml
feign:
client:
config:
default: # 全局默认配置
connect-timeout: 5000 # 连接超时时间(毫秒)
read-timeout: 10000 # 读取超时时间(毫秒)
logger-level: basic # 日志级别
2. 针对特定服务的配置
feign:
client:
config:
user-service: # 针对user-service的配置
connect-timeout: 3000
read-timeout: 8000
logger-level: full
product-service: # 针对product-service的配置
connect-timeout: 2000
read-timeout: 5000
3. Java配置类方式
import feign.Request;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignTimeoutConfig {
@Bean
public Request.Options feignOptions() {
// 连接超时5秒,读取超时10秒
return new Request.Options(
5000, // connectTimeoutMillis
10000, // readTimeoutMillis
true // followRedirects
);
}
}
// 针对特定Feign客户端的配置
@Configuration
public class UserFeignConfig {
@Bean
public Request.Options userServiceOptions() {
return new Request.Options(3000, 8000, true);
}
// 覆盖重试器(禁用重试)
@Bean
public Retryer userServiceRetryer() {
return Retryer.NEVER_RETRY; // 不重试
}
}
4. 自定义超时异常处理
import feign.Response;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
public class FeignErrorConfig {
@Bean
public ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}
static class CustomErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default();
@Override
public Exception decode(String methodKey, Response response) {
// 根据状态码返回不同的异常
if (response.status() == HttpStatus.REQUEST_TIMEOUT.value()) {
return new FeignTimeoutException("请求超时: " + methodKey);
}
if (response.status() == HttpStatus.SERVICE_UNAVAILABLE.value()) {
return new ServiceUnavailableException("服务不可用: " + methodKey);
}
// 其他异常使用默认处理
return defaultErrorDecoder.decode(methodKey, response);
}
}
static class FeignTimeoutException extends RuntimeException {
public FeignTimeoutException(String message) {
super(message);
}
}
static class ServiceUnavailableException extends RuntimeException {
public ServiceUnavailableException(String message) {
super(message);
}
}
}
重试机制
1. 默认重试配置
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignRetryConfig {
/**
* 默认重试器
* period: 初始重试间隔(毫秒)
* maxPeriod: 最大重试间隔(毫秒)
* maxAttempts: 最大重试次数(包括第一次调用)
*/
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(
100, // 初始间隔100ms
1000, // 最大间隔1s
3 // 最大尝试3次(初始1次 + 重试2次)
);
}
/**
* 自定义重试器:指数退避策略
*/
@Bean
public Retryer exponentialBackoffRetryer() {
return new Retryer() {
private final int maxAttempts = 3;
private int attempt = 1;
@Override
public void continueOrPropagate(RetryableException e) {
if (attempt++ >= maxAttempts) {
throw e;
}
try {
// 指数退避:100ms, 200ms, 400ms
long waitTime = (long) (100 * Math.pow(2, attempt - 2));
Thread.sleep(waitTime);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw e;
}
}
@Override
public Retryer clone() {
return new Retryer.Default();
}
};
}
}
2. 基于特定异常的重试
import feign.codec.ErrorDecoder;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.context.annotation.Bean;
public class FeignRetryWithTemplateConfig {
@Bean
public RetryTemplate feignRetryTemplate() {
return RetryTemplate.builder()
.maxAttempts(3) // 最大重试次数
.exponentialBackoff(100, 2, 1000) // 初始100ms,倍数2,最大1000ms
.retryOn(FeignException.class) // 对Feign异常重试
.retryOn(IOException.class) // 对IO异常重试
.notRetryOn(IllegalArgumentException.class) // 不对参数异常重试
.build();
}
/**
* 带条件的重试
*/
@Bean
public RetryTemplate conditionalRetryTemplate() {
return RetryTemplate.builder()
.maxAttempts(4)
.fixedBackoff(200) // 固定间隔200ms
.retryOn(context -> {
// 自定义重试条件
Throwable throwable = context.getLastThrowable();
if (throwable instanceof FeignException) {
FeignException fe = (FeignException) throwable;
// 只对5xx错误重试
return fe.status() >= 500 && fe.status() < 600;
}
return false;
})
.build();
}
}
3. 配置文件方式
# application.yml
spring:
cloud:
openfeign:
client:
config:
default:
retryer: feign.Retryer.Default # 使用默认重试器
feign:
retry:
enabled: true
max-attempts: 3
backoff-delay: 100
max-backoff-delay: 1000
retry-on-status: 500,502,503,504 # 对这些状态码重试
从URLConnection切换到HttpClient5
1. 添加HttpClient5依赖
<!-- pom.xml -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<version>12.2</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
2. 配置HttpClient5
import feign.httpclient.ApacheHttp5Client;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.core5.util.TimeValue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class HttpClient5Config {
/**
* 配置连接池
*/
@Bean
public HttpClientConnectionManager connectionManager() {
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager();
// 最大连接数
connectionManager.setMaxTotal(200);
// 每个路由的最大连接数(每个目标主机的最大连接数)
connectionManager.setDefaultMaxPerRoute(50);
// 连接存活时间(秒)
connectionManager.setValidateAfterInactivity(
TimeValue.of(30, TimeUnit.SECONDS)
);
return connectionManager;
}
/**
* 创建HttpClient5客户端
*/
@Bean
public CloseableHttpClient httpClient5(
HttpClientConnectionManager connectionManager) {
return HttpClients.custom()
.setConnectionManager(connectionManager)
// 连接超时
.setDefaultRequestConfig(
RequestConfig.custom()
.setConnectTimeout(5000, TimeUnit.MILLISECONDS)
.setConnectionRequestTimeout(5000, TimeUnit.MILLISECONDS)
.setResponseTimeout(10000, TimeUnit.MILLISECONDS)
.build()
)
// 启用重试
.setRetryStrategy(new DefaultHttpRequestRetryStrategy(3))
// 启用连接保活
.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
.build();
}
/**
* 配置Feign使用HttpClient5
*/
@Bean
public Client feignClient(CloseableHttpClient httpClient5) {
return new ApacheHttp5Client(httpClient5);
}
}
3. 配置文件启用
# application.yml
feign:
httpclient:
hc5:
enabled: true # 启用HttpClient5
client:
httpclient:
enabled: false # 禁用旧版HttpClient
okhttp:
enabled: false # 禁用OkHttp
4. 高级HttpClient5配置
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
@Configuration
public class AdvancedHttpClient5Config {
@Bean
public CloseableHttpClient advancedHttpClient5() {
// 认证凭证提供者
BasicCredentialsProvider credentialsProvider =
new BasicCredentialsProvider();
credentialsProvider.setCredentials(
new AuthScope("api.example.com", 443),
new UsernamePasswordCredentials("username", "password".toCharArray())
);
// SSL配置
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(null, new TrustAllStrategy()) // 信任所有(仅测试用)
.build();
return HttpClients.custom()
.setSSLContext(sslContext)
.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
.setDefaultCredentialsProvider(credentialsProvider)
// 连接池配置
.setConnectionManager(createConnectionManager())
// 代理配置
.setProxy(new HttpHost("proxy.example.com", 8080))
// 启用压缩
.addRequestInterceptorFirst((request, entity, context) -> {
request.addHeader("Accept-Encoding", "gzip, deflate");
})
.build();
}
private PoolingHttpClientConnectionManager createConnectionManager() {
PoolingHttpClientConnectionManager cm =
new PoolingHttpClientConnectionManager();
// 连接存活策略
cm.setValidateAfterInactivity(TimeValue.of(30, TimeUnit.SECONDS));
// 连接池监控
cm.setMaxTotal(200);
cm.setDefaultMaxPerRoute(50);
// 定期关闭空闲连接
IdleConnectionEvictor idleConnEvictor = new IdleConnectionEvictor(cm,
30, TimeUnit.SECONDS, // 最大空闲时间
10, TimeUnit.SECONDS); // 检查间隔
idleConnEvictor.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
idleConnEvictor.shutdown();
}));
return cm;
}
}
请求和响应压缩
1. 启用GZIP压缩
# application.yml
feign:
compression:
request:
enabled: true # 启用请求压缩
mime-types: text/xml,application/xml,application/json # 压缩类型
min-request-size: 2048 # 最小压缩大小(字节)
response:
enabled: true # 启用响应压缩
2. 自定义压缩配置
import feign.codec.Encoder;
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CompressionConfig {
/**
* 请求压缩拦截器
*/
@Bean
public RequestInterceptor compressionRequestInterceptor() {
return requestTemplate -> {
// 对于大于1KB的请求体启用压缩
if (requestTemplate.body() != null
&& requestTemplate.body().length > 1024) {
requestTemplate.header("Content-Encoding", "gzip");
// 这里应该实现实际的压缩逻辑
// 实际使用中,Feign会自动处理
}
};
}
/**
* 支持压缩的编码器
*/
@Bean
public Encoder feignEncoder() {
return new Encoder.Default() {
@Override
public void encode(Object object, Type bodyType,
RequestTemplate template) throws EncodeException {
super.encode(object, bodyType, template);
// 如果请求体较大,添加压缩头
if (template.body() != null && template.body().length > 1024) {
template.header("Content-Encoding", "gzip");
}
}
};
}
}
3. 压缩过滤器(服务器端)
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
public class ServerCompressionConfig {
/**
* 响应压缩过滤器
*/
@Bean
public FilterRegistrationBean<GzipFilter> gzipFilter() {
FilterRegistrationBean<GzipFilter> registration =
new FilterRegistrationBean<>();
registration.setFilter(new GzipFilter());
registration.addUrlPatterns("/*");
registration.setName("gzipFilter");
registration.setOrder(1);
return registration;
}
static class GzipFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String acceptEncoding = request.getHeader("Accept-Encoding");
if (acceptEncoding != null && acceptEncoding.contains("gzip")) {
// 对响应进行GZIP压缩
GZIPResponseWrapper wrappedResponse =
new GZIPResponseWrapper(response);
try {
filterChain.doFilter(request, wrappedResponse);
} finally {
wrappedResponse.finish();
}
} else {
filterChain.doFilter(request, response);
}
}
}
}
日志打印
1. 日志级别配置
# application.yml
logging:
level:
com.example.feign.UserServiceClient: DEBUG # Feign客户端日志级别
feign:
client:
config:
default:
logger-level: FULL # NONE, BASIC, HEADERS, FULL
2. 自定义日志配置
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignLogConfig {
/**
* 全局Feign日志级别
*/
@Bean
public Logger.Level feignLoggerLevel() {
// NONE: 不记录日志
// BASIC: 仅记录请求方法、URL、响应状态码、执行时间
// HEADERS: 记录BASIC信息+请求头和响应头
// FULL: 记录请求和响应的所有信息
return Logger.Level.FULL;
}
/**
* 自定义日志记录器
*/
@Bean
public Logger feignLogger() {
return new CustomFeignLogger();
}
static class CustomFeignLogger extends Logger {
private final org.slf4j.Logger logger =
org.slf4j.LoggerFactory.getLogger(CustomFeignLogger.class);
@Override
protected void log(String configKey, String format, Object... args) {
// 自定义日志格式
String message = String.format(format, args);
// 根据不同配置键使用不同日志级别
if (configKey.contains("UserServiceClient")) {
logger.debug("[Feign] {} - {}", configKey, message);
} else {
logger.info("[Feign] {} - {}", configKey, message);
}
}
@Override
protected void logRequest(String configKey, Level logLevel,
Request request) {
if (logger.isDebugEnabled()) {
StringBuilder builder = new StringBuilder();
builder.append("\n=== Feign Request ===\n");
builder.append("ConfigKey: ").append(configKey).append("\n");
builder.append("Method: ").append(request.method()).append("\n");
builder.append("URL: ").append(request.url()).append("\n");
builder.append("Headers: ").append(request.headers()).append("\n");
if (logLevel == Level.FULL && request.body() != null) {
builder.append("Body: ")
.append(new String(request.body()))
.append("\n");
}
builder.append("=====================\n");
logger.debug(builder.toString());
}
}
@Override
protected Response logAndRebufferResponse(String configKey,
Level logLevel, Response response, long elapsedTime)
throws IOException {
if (logger.isDebugEnabled()) {
StringBuilder builder = new StringBuilder();
builder.append("\n=== Feign Response ===\n");
builder.append("ConfigKey: ").append(configKey).append("\n");
builder.append("Status: ").append(response.status()).append("\n");
builder.append("Time: ").append(elapsedTime).append("ms\n");
builder.append("Headers: ").append(response.headers()).append("\n");
if (logLevel == Level.FULL) {
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
builder.append("Body: ").append(new String(bodyData)).append("\n");
// 重新构建Response,因为body流已被读取
response = Response.builder()
.body(bodyData)
.headers(response.headers())
.status(response.status())
.request(response.request())
.build();
}
builder.append("======================\n");
logger.debug(builder.toString());
}
return response;
}
}
}
3. 请求/响应日志拦截器
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LoggingInterceptorConfig {
private static final Logger log =
LoggerFactory.getLogger(LoggingInterceptorConfig.class);
/**
* 请求日志拦截器
*/
@Bean
public RequestInterceptor requestLoggingInterceptor() {
return template -> {
log.info("[Feign Request] {} {}",
template.method(),
template.url());
if (log.isDebugEnabled()) {
log.debug("[Feign Headers] {}", template.headers());
if (template.body() != null) {
log.debug("[Feign Body] {}",
new String(template.body()));
}
}
};
}
/**
* 响应日志拦截器(通过自定义Client实现)
*/
@Bean
public Client loggingFeignClient(Client delegate) {
return new Client() {
@Override
public Response execute(Request request, Request.Options options)
throws IOException {
long startTime = System.currentTimeMillis();
try {
Response response = delegate.execute(request, options);
long elapsedTime = System.currentTimeMillis() - startTime;
log.info("[Feign Response] {} {} - {}ms",
request.method(),
request.url(),
elapsedTime);
if (log.isDebugEnabled()) {
log.debug("[Feign Response Status] {}", response.status());
log.debug("[Feign Response Headers] {}", response.headers());
}
return response;
} catch (Exception e) {
long elapsedTime = System.currentTimeMillis() - startTime;
log.error("[Feign Error] {} {} - {}ms - {}",
request.method(),
request.url(),
elapsedTime,
e.getMessage(), e);
throw e;
}
}
};
}
}
与Spring Cloud Gateway集成
1. 网关路由配置
# gateway-service application.yml
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service # 使用负载均衡
predicates:
- Path=/api/users/**
filters:
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY, SERVICE_UNAVAILABLE
- name: CircuitBreaker
args:
name: userServiceCircuitBreaker
fallbackUri: forward:/fallback/user-service
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
- StripPrefix=1 # 去掉路径前缀
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
2. Feign客户端通过网关调用
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
/**
* 通过网关调用的Feign客户端
*/
@FeignClient(
name = "gateway-service", // 通过网关调用
path = "/api/users",
configuration = GatewayFeignConfig.class
)
public interface GatewayUserServiceClient {
@GetMapping("/{userId}")
UserDTO getUserThroughGateway(@PathVariable Long userId);
@PostMapping("/create")
ResultDTO<Long> createUserThroughGateway(@RequestBody UserCreateRequest request);
}
/**
* 网关Feign配置
*/
@Configuration
public class GatewayFeignConfig {
@Bean
public RequestInterceptor gatewayRequestInterceptor() {
return template -> {
// 添加网关需要的Header
template.header("X-Forwarded-For", getClientIp());
template.header("X-Request-Id", generateRequestId());
// 认证信息
template.header("Authorization", getAuthToken());
};
}
private String getClientIp() {
// 获取客户端IP
return "127.0.0.1";
}
private String generateRequestId() {
return UUID.randomUUID().toString();
}
private String getAuthToken() {
// 从安全上下文获取token
return "Bearer token123";
}
}
3. 网关熔断降级
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/fallback")
public class GatewayFallbackController {
@GetMapping("/user-service")
public ResultDTO<?> userServiceFallback() {
return ResultDTO.error(503, "用户服务暂时不可用,请稍后重试");
}
@GetMapping("/order-service")
public ResultDTO<?> orderServiceFallback() {
return ResultDTO.error(503, "订单服务暂时不可用,请稍后重试");
}
}
与Spring Cloud Sentinel集成
1. 添加Sentinel依赖
<!-- pom.xml -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2022.0.0.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-feign</artifactId>
</dependency>
2. Sentinel配置
# application.yml
spring:
cloud:
sentinel:
enabled: true
transport:
dashboard: localhost:8080 # Sentinel控制台地址
port: 8719
# 数据源配置(使用Nacos)
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-sentinel
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow # 流控规则
ds2:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-sentinel-degrade
groupId: DEFAULT_GROUP
data-type: json
rule-type: degrade # 降级规则
# Feign Sentinel支持
feign:
sentinel:
enabled: true # 启用Sentinel对Feign的支持
3. Feign客户端Sentinel配置
import com.alibaba.cloud.sentinel.feign.SentinelFeign;
import feign.hystrix.FallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
/**
* 支持Sentinel的Feign客户端
*/
@FeignClient(
name = "user-service",
path = "/api/users",
fallback = UserServiceClientFallback.class,
fallbackFactory = UserServiceClientFallbackFactory.class
)
public interface SentinelUserServiceClient {
@GetMapping("/{userId}")
UserDTO getUserWithSentinel(@PathVariable Long userId);
@PostMapping("/create")
ResultDTO<Long> createUserWithSentinel(@RequestBody UserCreateRequest request);
}
/**
* 简单降级实现
*/
@Component
class UserServiceClientFallback implements SentinelUserServiceClient {
@Override
public UserDTO getUserWithSentinel(Long userId) {
UserDTO fallbackUser = new UserDTO();
fallbackUser.setId(userId);
fallbackUser.setUsername("fallback-user");
return fallbackUser;
}
@Override
public ResultDTO<Long> createUserWithSentinel(UserCreateRequest request) {
return ResultDTO.error(503, "用户服务暂时不可用,创建用户失败");
}
}
/**
* 带异常信息的降级工厂
*/
@Component
class UserServiceClientFallbackFactory
implements FallbackFactory<SentinelUserServiceClient> {
@Override
public SentinelUserServiceClient create(Throwable cause) {
return new SentinelUserServiceClient() {
@Override
public UserDTO getUserWithSentinel(Long userId) {
log.error("调用用户服务失败,用户ID: {}", userId, cause);
UserDTO fallbackUser = new UserDTO();
fallbackUser.setId(userId);
fallbackUser.setUsername("fallback-user");
fallbackUser.setEmail("fallback@example.com");
return fallbackUser;
}
@Override
public ResultDTO<Long> createUserWithSentinel(UserCreateRequest request) {
log.error("创建用户失败: {}", request.getUsername(), cause);
return ResultDTO.error(503,
"用户服务暂时不可用,原因: " + cause.getMessage());
}
};
}
}
/**
* Sentinel Feign配置
*/
@Configuration
public class SentinelFeignConfig {
/**
* 启用Sentinel Feign支持
*/
@Bean
@Scope("prototype")
public Feign.Builder feignSentinelBuilder() {
return SentinelFeign.builder();
}
/**
* Sentinel请求拦截器
*/
@Bean
public RequestInterceptor sentinelRequestInterceptor() {
return template -> {
// 在请求头中添加Sentinel需要的上下文信息
String resourceName = template.method() + ":" + template.path();
template.header("SENTINEL_RESOURCE", resourceName);
// 添加调用链追踪ID
template.header("X-B3-TraceId", MDC.get("X-B3-TraceId"));
template.header("X-B3-SpanId", MDC.get("X-B3-SpanId"));
};
}
}
4. Sentinel规则配置
// Nacos中的Sentinel规则配置
// user-service-sentinel.json (流控规则)
[
{
"resource": "GET:/api/users/{userId}",
"count": 100,
"grade": 1,
"limitApp": "default",
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
},
{
"resource": "POST:/api/users/create",
"count": 50,
"grade": 1,
"limitApp": "default",
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
// user-service-sentinel-degrade.json (降级规则)
[
{
"resource": "GET:/api/users/{userId}",
"count": 1000,
"grade": 0,
"timeWindow": 5,
"minRequestAmount": 5,
"statIntervalMs": 1000,
"slowRatioThreshold": 0.5
},
{
"resource": "POST:/api/users/create",
"count": 0.5,
"grade": 1,
"timeWindow": 10,
"minRequestAmount": 10,
"statIntervalMs": 1000
}
]
5. Sentinel Dashboard配置
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* Sentinel全局异常处理
*/
@ControllerAdvice
public class SentinelGlobalExceptionHandler {
/**
* 处理Sentinel流控/降级异常
*/
@ExceptionHandler(BlockException.class)
@ResponseBody
public ResultDTO<?> sentinelBlockHandler(BlockException e) {
String resourceName = e.getRule().getResource();
return ResultDTO.error(429,
"请求过于频繁,资源[" + resourceName + "]已被限流,请稍后重试");
}
/**
* 处理Feign调用异常
*/
@ExceptionHandler(FeignException.class)
@ResponseBody
public ResultDTO<?> feignExceptionHandler(FeignException e) {
int status = e.status();
switch (status) {
case 429:
return ResultDTO.error(429, "服务限流,请稍后重试");
case 503:
return ResultDTO.error(503, "服务暂时不可用,请稍后重试");
case 504:
return ResultDTO.error(504, "服务调用超时,请稍后重试");
default:
return ResultDTO.error(500, "服务调用失败: " + e.getMessage());
}
}
}
高级特性与最佳实践
1. 请求拦截器链
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignInterceptorChainConfig {
/**
* 认证拦截器
*/
@Bean
public RequestInterceptor authenticationInterceptor() {
return template -> {
// 从安全上下文获取token
String token = SecurityContextHolder.getContext()
.getAuthentication()
.getCredentials()
.toString();
template.header("Authorization", "Bearer " + token);
};
}
/**
* 跟踪拦截器
*/
@Bean
public RequestInterceptor tracingInterceptor() {
return template -> {
// 传递跟踪信息
template.header("X-Request-Id", UUID.randomUUID().toString());
template.header("X-B3-TraceId", MDC.get("traceId"));
template.header("X-B3-SpanId", MDC.get("spanId"));
};
}
/**
* 性能监控拦截器
*/
@Bean
public RequestInterceptor metricsInterceptor() {
return template -> {
long startTime = System.currentTimeMillis();
template.requestInterceptors().add(request -> {
long elapsedTime = System.currentTimeMillis() - startTime;
// 记录指标
Metrics.recordFeignCall(
template.feignTarget().name(),
template.method(),
template.url(),
elapsedTime
);
return request;
});
};
}
}
2. 自定义编解码器
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CustomCodecConfig {
/**
* 自定义编码器(支持多种数据格式)
*/
@Bean
public Encoder customEncoder() {
return new Encoder.Default() {
@Override
public void encode(Object object, Type bodyType,
RequestTemplate template) throws EncodeException {
// 根据Content-Type选择编码方式
String contentType = template.headers()
.getOrDefault("Content-Type",
Collections.singletonList("application/json"))
.iterator()
.next();
if ("application/json".equals(contentType)) {
// JSON编码
try {
String json = new ObjectMapper().writeValueAsString(object);
template.body(json);
} catch (JsonProcessingException e) {
throw new EncodeException("JSON编码失败", e);
}
} else if ("application/xml".equals(contentType)) {
// XML编码
try {
String xml = convertToXml(object);
template.body(xml);
} catch (Exception e) {
throw new EncodeException("XML编码失败", e);
}
} else {
super.encode(object, bodyType, template);
}
}
};
}
/**
* 自定义解码器
*/
@Bean
public Decoder customDecoder() {
return new ResponseEntityDecoder(new SpringDecoder(messageConverters())) {
@Override
public Object decode(Response response, Type type)
throws IOException, FeignException {
// 处理特定错误码
if (response.status() == 404) {
return handleNotFound(response, type);
}
if (response.status() == 401) {
throw new RetryableException(
401,
"认证失败",
response.request().httpMethod(),
null,
response.request()
);
}
return super.decode(response, type);
}
};
}
private Object handleNotFound(Response response, Type type) {
// 对于404响应,返回空值而不是抛出异常
if (type instanceof Class) {
Class<?> clazz = (Class<?>) type;
if (clazz == UserDTO.class) {
return null;
}
if (clazz == List.class) {
return Collections.emptyList();
}
}
// 其他类型使用默认处理
return new Object();
}
}
3. 性能优化配置
# application.yml - 性能优化配置
feign:
# 连接池配置
httpclient:
connection-timeout: 2000
max-connections: 200
max-connections-per-route: 50
time-to-live: 900
time-to-live-unit: seconds
follow-redirects: true
disable-ssl-validation: false
# 压缩配置
compression:
request:
enabled: true
mime-types: text/xml,application/xml,application/json
min-request-size: 2048
response:
enabled: true
# 重试配置
retry:
enabled: true
max-attempts: 3
period: 100
max-period: 1000
# 超时配置
client:
config:
default:
connect-timeout: 5000
read-timeout: 10000
logger-level: basic
# 启用响应缓存
cache:
enabled: true
max-size: 1000
ttl: 300000 # 5分钟
4. 监控与指标
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignMetricsConfig {
@Bean
public FeignMetricsInterceptor feignMetricsInterceptor(
MeterRegistry meterRegistry) {
return new FeignMetricsInterceptor(meterRegistry);
}
static class FeignMetricsInterceptor implements RequestInterceptor {
private final MeterRegistry meterRegistry;
private final ThreadLocal<Long> startTime = new ThreadLocal<>();
public FeignMetricsInterceptor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
public void apply(RequestTemplate template) {
startTime.set(System.currentTimeMillis());
template.requestInterceptors().add(request -> {
Long start = startTime.get();
if (start != null) {
long duration = System.currentTimeMillis() - start;
// 记录指标
meterRegistry.timer("feign.client.requests",
"service", template.feignTarget().name(),
"method", template.method(),
"path", template.path())
.record(duration, TimeUnit.MILLISECONDS);
startTime.remove();
}
return request;
});
}
}
}
总结
OpenFeign 作为一个强大的声明式HTTP客户端,为微服务之间的调用提供了简洁优雅的解决方案。通过本文的详细介绍,你应该掌握了:
- 基础使用:如何定义和使用Feign客户端
- 超时控制:连接超时和读取超时的配置
- 重试机制:多种重试策略的实现
- HTTP客户端切换:从URLConnection切换到HttpClient5
- 压缩优化:请求和响应的压缩配置
- 日志管理:详细的日志打印配置
- 集成能力:与Gateway和Sentinel的深度集成
在实际项目中,建议根据具体需求选择合适的配置组合,并持续监控Feign调用的性能指标,不断优化配置参数,以达到最佳的性能和稳定性。
点击查看更多内容
为 TA 点赞
评论
共同学习,写下你的评论
评论加载中...
作者其他优质文章
正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦