Feign拦截器

Feign 拦截器:请求增强与上下文传递的利器

Feign 拦截器(RequestInterceptor)是 Feign 提供的请求增强机制,可在 HTTP 请求发送前对其进行统一处理(如添加请求头、修改参数、日志记录等),常用于传递上下文信息(如用户认证令牌、客户端 IP)、统一配置请求头等场景。

Feign 拦截器的核心作用

Feign 拦截器通过实现RequestInterceptor接口的apply方法,在请求构建完成后、发送前对RequestTemplate进行修改,实现以下核心功能:

  • 传递上下文信息:如将当前用户的 Token、用户 ID 从上游服务传递到下游服务。
  • 统一添加请求头:如添加Content-TypeAcceptX-Request-Id等通用头信息。
  • 请求日志记录:记录请求 URL、参数等信息,便于调试和审计。
  • 参数加密 / 签名:对敏感参数进行加密,或为请求添加签名验证。

Feign 拦截器的实现与配置

1. 自定义拦截器实现

通过实现RequestInterceptor接口,重写apply方法编写拦截逻辑:

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

/**
 * Feign拦截器:传递请求头和上下文信息
 */
public class FeignContextInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 从当前请求上下文获取HttpServletRequest(仅适用于Web环境)
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            
            // 1. 传递所有上游请求头(如Token、用户信息)
            Enumeration<String> headerNames = request.getHeaderNames();
            if (headerNames != null) {
                while (headerNames.hasMoreElements()) {
                    String headerName = headerNames.nextElement();
                    // 跳过Feign自带的头信息(如Host),避免冲突
                    if (!"Host".equalsIgnoreCase(headerName)) {
                        String headerValue = request.getHeader(headerName);
                        template.header(headerName, headerValue);
                    }
                }
            }
            
            // 2. 添加自定义请求头(如客户端IP)
            String clientIp = getClientIp(request);
            template.header("X-Client-IP", clientIp);
        }
    }

    // 获取客户端真实IP
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

2. 注册拦截器

拦截器需要注册到 Feign 的配置中才能生效,支持全局注册局部注册两种方式:

(1)全局注册(对所有 Feign 客户端生效)

通过@Configuration类注册拦截器,使其对所有 Feign 客户端生效:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignGlobalConfig {
    // 注册全局Feign拦截器
    @Bean
    public RequestInterceptor feignContextInterceptor() {
        return new FeignContextInterceptor();
    }
}
(2)局部注册(仅对特定 Feign 客户端生效)

@FeignClientconfiguration属性指定的配置类中注册拦截器,仅对当前客户端生效:

// 局部配置类(不被@ComponentScan扫描)
public class FeignLocalConfig {
    @Bean
    public RequestInterceptor feignLocalInterceptor() {
        return template -> {
            template.header("X-Service-Name", "user-service"); // 仅为当前服务添加的头信息
        };
    }
}

// Feign客户端引用局部配置
@FeignClient(
    name = "USER-SERVICE",
    configuration = FeignLocalConfig.class
)
public interface UserClient { ... }

常见使用场景与最佳实践

1. 传递认证令牌(Token)

在分布式系统中,用户登录后的 Token 需要在服务间传递,通过拦截器可自动将 Token 添加到 Feign 请求头:

@Override
public void apply(RequestTemplate template) {
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    if (attributes != null) {
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("Authorization");
        if (token != null) {
            template.header("Authorization", token); // 传递Token
        }
    }
}

2. 统一添加日志追踪 ID

为便于分布式追踪,为每个请求添加唯一的X-Trace-Id,若上游已存在则复用,否则生成新 ID:

@Override
public void apply(RequestTemplate template) {
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    String traceId;
    if (attributes != null) {
        HttpServletRequest request = attributes.getRequest();
        traceId = request.getHeader("X-Trace-Id");
    }
    // 若上游无TraceId,生成新的UUID
    if (traceId == null || traceId.isEmpty()) {
        traceId = UUID.randomUUID().toString();
    }
    template.header("X-Trace-Id", traceId);
}

3. 记录 Feign 请求日志

在拦截器中记录请求 URL、方法、参数等信息,辅助调试:

@Override
public void apply(RequestTemplate template) {
    // 记录请求信息(生产环境建议使用日志框架,如SLF4J)
    System.out.println("Feign请求URL:" + template.url());
    System.out.println("请求方法:" + template.method());
    System.out.println("请求参数:" + template.queryLine());
}

注意事项

  1. 线程安全问题RequestInterceptorapply方法可能被多线程调用,需确保拦截器无状态(如不使用成员变量存储请求相关信息)。
  2. RequestContextHolder 的局限性RequestContextHolder依赖 ThreadLocal,仅在 Web 环境(如 Tomcat 线程)中有效。若 Feign 调用发生在异步线程(如@Async方法),需手动传递上下文信息。
  3. 避免循环依赖:拦截器中若注入 Feign 客户端,可能导致循环依赖(Feign 客户端依赖拦截器,拦截器又依赖 Feign 客户端),需谨慎设计。
  4. 请求头冲突:若拦截器添加的头信息与 Feign 接口方法中定义的头信息冲突,后者会覆盖前者(方法级配置优先级更高)。

总结

Feign 拦截器是实现请求统一处理的核心组件,通过它可便捷地传递上下文、添加通用头信息、记录日志等,显著提升微服务间通信的灵活性和可维护性。在实际使用中,需根据业务场景选择全局或局部注册方式,并注意线程安全和上下文传递的局限性,确保拦截器逻辑可靠高效