SpringCloud 服务调用与负载均衡:OpenFeign 极简使用教程

🎯 本文适合人群 :SpringCloud 初学者、微服务开发者

⏱️ 阅读时长 :30 分钟

📌 你将收获:掌握 OpenFeign 声明式调用、负载均衡策略、超时重试配置


📖 目录


一、OpenFeign 快速认识

1.1 什么是 OpenFeign?

OpenFeign = 声明式的 HTTP 客户端

核心思想

  • 定义接口 + 注解
  • OpenFeign 自动生成实现类
  • 像调用本地方法一样调用远程服务

1.2 为什么需要 OpenFeign?

传统方式(RestTemplate)

java 复制代码
// ❌ 繁琐的方式
@Autowired
private RestTemplate restTemplate;

public User getUserById(Long userId) {
    // 1. 拼接 URL
    String url = "http://user-service/user/" + userId;
    
    // 2. 设置请求头
    HttpHeaders headers = new HttpHeaders();
    headers.set("Authorization", "Bearer token");
    HttpEntity<String> entity = new HttpEntity<>(headers);
    
    // 3. 发起请求
    ResponseEntity<User> response = restTemplate.exchange(
        url, HttpMethod.GET, entity, User.class
    );
    
    // 4. 获取结果
    return response.getBody();
}

使用 OpenFeign

java 复制代码
// ✅ 简洁的方式
@FeignClient(name = "user-service")
public interface UserClient {
    @GetMapping("/user/{id}")
    User getUserById(@PathVariable("id") Long userId);
}

// 使用
@Autowired
private UserClient userClient;

User user = userClient.getUserById(1001L);  // 一行搞定!

对比

维度 RestTemplate OpenFeign
代码量
可读性
负载均衡 需手动配置 自动集成
熔断降级 需手动实现 自动集成
维护成本

1.3 OpenFeign 工作原理

复制代码
┌──────────────────────────────────────────────────┐
│          OpenFeign 调用流程                       │
├──────────────────────────────────────────────────┤
│                                                  │
│  ① 定义 FeignClient 接口                         │
│     @FeignClient("user-service")                 │
│     interface UserClient { ... }                 │
│     ↓                                            │
│  ② Spring 启动时扫描接口                          │
│     FeignClientFactoryBean 创建代理对象           │
│     ↓                                            │
│  ③ 调用接口方法                                   │
│     userClient.getUserById(1001L)                │
│     ↓                                            │
│  ④ 代理对象拦截调用                                │
│     ReflectiveFeign.invoke()                     │
│     ↓                                            │
│  ⑤ 解析注解,构造请求                              │
│     - @GetMapping → GET 方法                     │
│     - @PathVariable → 路径参数                   │
│     - 服务名 → 从 Nacos 获取实例列表              │
│     ↓                                            │
│  ⑥ LoadBalancer 选择实例                         │
│     [192.168.1.101:8081, 192.168.1.102:8081]     │
│     选中:192.168.1.101:8081                     │
│     ↓                                            │
│  ⑦ 发送 HTTP 请求                                 │
│     GET http://192.168.1.101:8081/user/1001     │
│     ↓                                            │
│  ⑧ 反序列化响应                                   │
│     JSON → User 对象                             │
│     ↓                                            │
│  ⑨ 返回结果                                       │
│     return user;                                 │
└──────────────────────────────────────────────────┘

二、OpenFeign 快速入门

2.1 环境搭建

项目结构

复制代码
microservice-demo/
├── user-service/      # 服务提供者(用户服务)
├── order-service/     # 服务消费者(订单服务)
└── common/            # 公共模块(实体类)

2.2 服务提供者(用户服务)

步骤 1:pom.xml
xml 复制代码
<dependencies>
    <!-- SpringBoot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Nacos 服务发现 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
</dependencies>
步骤 2:application.yml
yaml 复制代码
server:
  port: 8081

spring:
  application:
    name: user-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
步骤 3:实体类
java 复制代码
package com.example.userservice.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 用户实体
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long id;
    private String username;
    private Integer age;
    private String email;
}
步骤 4:控制器
java 复制代码
package com.example.userservice.controller;

import com.example.userservice.entity.User;
import org.springframework.web.bind.annotation.*;

/**
 * 用户控制器
 */
@RestController
@RequestMapping("/user")
public class UserController {

    /**
     * 根据 ID 查询用户
     */
    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        // 模拟查询数据库
        return new User(id, "用户" + id, 25, "user" + id + "@example.com");
    }

    /**
     * 根据用户名查询用户
     */
    @GetMapping("/by-name")
    public User getUserByName(@RequestParam String username) {
        // 模拟查询
        return new User(1001L, username, 30, username + "@example.com");
    }

    /**
     * 创建用户
     */
    @PostMapping
    public User createUser(@RequestBody User user) {
        // 模拟保存到数据库
        user.setId(System.currentTimeMillis());
        return user;
    }
}

2.3 服务消费者(订单服务)

步骤 1:pom.xml
xml 复制代码
<dependencies>
    <!-- SpringBoot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Nacos 服务发现 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    
    <!-- OpenFeign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
    <!-- LoadBalancer(负载均衡) -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
</dependencies>
步骤 2:application.yml
yaml 复制代码
server:
  port: 8082

spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
步骤 3:启动类
java 复制代码
package com.example.orderservice;

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

/**
 * 订单服务启动类
 * 
 * @EnableFeignClients:开启 Feign 客户端扫描
 * - 扫描 @FeignClient 注解
 * - 生成代理对象
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients  // 开启 Feign 客户端
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}
步骤 4:定义 Feign 客户端
java 复制代码
package com.example.orderservice.client;

import com.example.orderservice.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

/**
 * 用户服务 Feign 客户端
 * 
 * @FeignClient:声明这是一个 Feign 客户端
 * - name/value:服务名(必须)
 * - path:统一路径前缀(可选)
 */
@FeignClient(name = "user-service", path = "/user")
public interface UserClient {

    /**
     * 根据 ID 查询用户
     * 
     * @PathVariable:路径参数
     * - value/name:参数名(必须与接口定义一致)
     */
    @GetMapping("/{id}")
    User getUserById(@PathVariable("id") Long id);

    /**
     * 根据用户名查询用户
     * 
     * @RequestParam:查询参数
     */
    @GetMapping("/by-name")
    User getUserByName(@RequestParam("username") String username);

    /**
     * 创建用户
     * 
     * @RequestBody:请求体参数
     */
    @PostMapping
    User createUser(@RequestBody User user);
}
步骤 5:使用 Feign 客户端
java 复制代码
package com.example.orderservice.controller;

import com.example.orderservice.client.UserClient;
import com.example.orderservice.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * 订单控制器
 */
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private UserClient userClient;  // 注入 Feign 客户端

    /**
     * 创建订单
     * 
     * 业务流程:
     * 1. 调用用户服务,验证用户
     * 2. 创建订单
     */
    @PostMapping("/create")
    public String createOrder(@RequestParam Long userId) {
        // ① 调用用户服务(像调用本地方法一样简单!)
        User user = userClient.getUserById(userId);
        
        // ② 验证用户
        if (user == null) {
            return "用户不存在,订单创建失败";
        }
        
        // ③ 创建订单(模拟)
        String orderId = "ORDER-" + System.currentTimeMillis();
        
        // ④ 返回结果
        return String.format(
            "订单创建成功!\n订单ID:%s\n用户:%s(%s岁)",
            orderId, user.getUsername(), user.getAge()
        );
    }

    /**
     * 测试 Feign 调用
     */
    @GetMapping("/test-feign")
    public String testFeign() {
        // 测试不同类型的调用
        
        // 1. 路径参数
        User user1 = userClient.getUserById(1001L);
        
        // 2. 查询参数
        User user2 = userClient.getUserByName("张三");
        
        // 3. 请求体参数
        User newUser = new User(null, "李四", 28, "lisi@example.com");
        User user3 = userClient.createUser(newUser);
        
        return "Feign 调用测试成功!\n" +
               "用户1:" + user1 + "\n" +
               "用户2:" + user2 + "\n" +
               "用户3:" + user3;
    }
}

2.4 启动测试

启动顺序

  1. 启动 Nacos
  2. 启动 user-service(8081)
  3. 启动 order-service(8082)

测试接口

bash 复制代码
# 创建订单
curl -X POST "http://localhost:8082/order/create?userId=1001"

# 返回:
订单创建成功!
订单ID:ORDER-1711770123456
用户:用户1001(25岁)

三、OpenFeign 参数传递

3.1 路径参数(@PathVariable)

java 复制代码
/**
 * 单个路径参数
 */
@GetMapping("/user/{id}")
User getUserById(@PathVariable("id") Long id);

/**
 * 多个路径参数
 */
@GetMapping("/user/{id}/order/{orderId}")
String getOrderDetail(
    @PathVariable("id") Long userId,
    @PathVariable("orderId") Long orderId
);

调用示例

java 复制代码
User user = userClient.getUserById(1001L);
// GET http://user-service/user/1001

String detail = userClient.getOrderDetail(1001L, 2001L);
// GET http://user-service/user/1001/order/2001

3.2 查询参数(@RequestParam)

java 复制代码
/**
 * 单个查询参数
 */
@GetMapping("/user/search")
User searchUser(@RequestParam("name") String name);

/**
 * 多个查询参数
 */
@GetMapping("/user/list")
List<User> listUsers(
    @RequestParam("page") Integer page,
    @RequestParam("size") Integer size
);

/**
 * 可选参数(required = false)
 */
@GetMapping("/user/filter")
List<User> filterUsers(
    @RequestParam(value = "age", required = false) Integer age,
    @RequestParam(value = "city", required = false) String city
);

调用示例

java 复制代码
User user = userClient.searchUser("张三");
// GET http://user-service/user/search?name=张三

List<User> users = userClient.listUsers(1, 10);
// GET http://user-service/user/list?page=1&size=10

3.3 请求体参数(@RequestBody)

java 复制代码
/**
 * POST 请求
 */
@PostMapping("/user")
User createUser(@RequestBody User user);

/**
 * PUT 请求
 */
@PutMapping("/user/{id}")
User updateUser(
    @PathVariable("id") Long id,
    @RequestBody User user
);

/**
 * DELETE 请求
 */
@DeleteMapping("/user/{id}")
void deleteUser(@PathVariable("id") Long id);

调用示例

java 复制代码
User newUser = new User(null, "王五", 30, "wangwu@example.com");
User created = userClient.createUser(newUser);
// POST http://user-service/user
// Body: {"username":"王五","age":30,"email":"wangwu@example.com"}

3.4 请求头参数(@RequestHeader)

java 复制代码
/**
 * 单个请求头
 */
@GetMapping("/user/{id}")
User getUserById(
    @PathVariable("id") Long id,
    @RequestHeader("Authorization") String token
);

/**
 * 多个请求头
 */
@GetMapping("/user/info")
User getUserInfo(
    @RequestHeader("Authorization") String token,
    @RequestHeader("X-Request-Id") String requestId
);

调用示例

java 复制代码
User user = userClient.getUserById(1001L, "Bearer token123");
// GET http://user-service/user/1001
// Header: Authorization: Bearer token123

3.5 表单参数(@SpringQueryMap)

java 复制代码
/**
 * Map 参数(自动转为查询参数)
 */
@GetMapping("/user/search")
List<User> searchUsers(@SpringQueryMap Map<String, Object> params);

调用示例

java 复制代码
Map<String, Object> params = new HashMap<>();
params.put("name", "张三");
params.put("age", 25);
params.put("city", "北京");

List<User> users = userClient.searchUsers(params);
// GET http://user-service/user/search?name=张三&age=25&city=北京

四、负载均衡策略

4.1 默认策略(轮询)

OpenFeign 集成了 Spring Cloud LoadBalancer ,默认使用轮询策略

复制代码
用户服务 3 个实例:
- 192.168.1.101:8081
- 192.168.1.102:8081
- 192.168.1.103:8081

第 1 次请求 → 192.168.1.101:8081
第 2 次请求 → 192.168.1.102:8081
第 3 次请求 → 192.168.1.103:8081
第 4 次请求 → 192.168.1.101:8081 ↻

4.2 自定义负载均衡策略

方式 1:全局配置(所有服务)
java 复制代码
package com.example.orderservice.config;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

/**
 * 全局负载均衡配置
 */
@Configuration
public class LoadBalancerConfig {

    /**
     * 配置随机策略
     */
    @Bean
    public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
        Environment environment,
        LoadBalancerClientFactory loadBalancerClientFactory) {
        
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        
        return new RandomLoadBalancer(
            loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
            name
        );
    }
}
方式 2:针对单个服务
yaml 复制代码
# application.yml
spring:
  cloud:
    loadbalancer:
      configurations: random  # 全局策略:random 或 round-robin
    
    # 针对单个服务配置
    nacos:
      discovery:
        server-addr: localhost:8848

# 针对 user-service 使用随机策略
user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

4.3 自定义负载均衡规则

java 复制代码
package com.example.orderservice.loadbalancer;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 自定义负载均衡规则:按权重选择
 */
public class WeightedLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
    private final String serviceId;

    public WeightedLoadBalancer(
        ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
        String serviceId) {
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
        this.serviceId = serviceId;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable();
        return supplier.get(request).next().map(this::getInstanceResponse);
    }

    /**
     * 按权重选择实例
     */
    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            return new EmptyResponse();
        }

        // 计算总权重
        int totalWeight = instances.stream()
            .mapToInt(instance -> {
                String weight = instance.getMetadata().get("weight");
                return weight != null ? Integer.parseInt(weight) : 1;
            })
            .sum();

        // 随机数
        int random = ThreadLocalRandom.current().nextInt(totalWeight);

        // 按权重选择
        int currentWeight = 0;
        for (ServiceInstance instance : instances) {
            String weight = instance.getMetadata().get("weight");
            int instanceWeight = weight != null ? Integer.parseInt(weight) : 1;
            currentWeight += instanceWeight;
            if (random < currentWeight) {
                return new DefaultResponse(instance);
            }
        }

        return new DefaultResponse(instances.get(0));
    }
}

五、超时与重试配置

5.1 超时配置

全局配置

yaml 复制代码
# application.yml
feign:
  client:
    config:
      default:  # 默认配置(所有服务)
        connectTimeout: 5000  # 连接超时:5 秒
        readTimeout: 10000    # 读取超时:10 秒

针对单个服务

yaml 复制代码
feign:
  client:
    config:
      user-service:  # 针对 user-service
        connectTimeout: 3000   # 连接超时:3 秒
        readTimeout: 5000      # 读取超时:5 秒
      
      order-service:  # 针对 order-service
        connectTimeout: 10000  # 连接超时:10 秒
        readTimeout: 30000     # 读取超时:30 秒

代码配置

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

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

import java.util.concurrent.TimeUnit;

/**
 * Feign 超时配置
 */
@Configuration
public class FeignConfig {

    @Bean
    public Request.Options requestOptions() {
        return new Request.Options(
            5000, TimeUnit.MILLISECONDS,  // 连接超时
            10000, TimeUnit.MILLISECONDS  // 读取超时
        );
    }
}

5.2 重试配置

默认重试器

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

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

/**
 * Feign 重试配置
 */
@Configuration
public class FeignRetryConfig {

    /**
     * 配置重试器
     * 
     * @return Retryer
     */
    @Bean
    public Retryer feignRetryer() {
        /*
         * Retryer.Default 参数说明:
         * 1. period:初始重试间隔(毫秒)
         * 2. maxPeriod:最大重试间隔(毫秒)
         * 3. maxAttempts:最大重试次数
         * 
         * 重试间隔计算:interval = min(period * 1.5^n, maxPeriod)
         */
        return new Retryer.Default(
            100,    // 初始间隔 100ms
            1000,   // 最大间隔 1s
            3       // 最多重试 3 次
        );
    }
}

禁用重试

java 复制代码
@Bean
public Retryer feignRetryer() {
    return Retryer.NEVER_RETRY;  // 不重试
}

5.3 超时测试

java 复制代码
/**
 * 用户服务:模拟慢接口
 */
@GetMapping("/slow")
public String slowApi() throws InterruptedException {
    Thread.sleep(15000);  // 休眠 15 秒
    return "慢接口响应";
}

/**
 * Feign 客户端
 */
@GetMapping("/slow")
String slowApi();

/**
 * 订单服务:调用慢接口
 */
@GetMapping("/test-timeout")
public String testTimeout() {
    try {
        String result = userClient.slowApi();
        return "调用成功:" + result;
    } catch (Exception e) {
        return "调用超时:" + e.getMessage();
    }
}

测试结果

bash 复制代码
# 配置 readTimeout = 10000(10 秒)
curl http://localhost:8082/order/test-timeout

# 10 秒后返回:
调用超时:Read timed out executing GET http://user-service/user/slow

六、日志配置

6.1 日志级别

OpenFeign 日志级别

级别 说明 输出内容
NONE 不记录(默认)
BASIC 基本信息 请求方法、URL、响应状态码、执行时间
HEADERS 基本 + 请求/响应头 BASIC + Headers
FULL 完整信息 BASIC + Headers + Body

6.2 配置日志

方式 1:配置类

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

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

/**
 * Feign 日志配置
 */
@Configuration
public class FeignLogConfig {

    /**
     * 配置日志级别
     */
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;  // 记录完整请求和响应
    }
}

方式 2:配置文件

yaml 复制代码
# application.yml
feign:
  client:
    config:
      default:
        loggerLevel: FULL  # NONE、BASIC、HEADERS、FULL

方式 3:针对单个客户端

java 复制代码
@FeignClient(
    name = "user-service",
    configuration = FeignLogConfig.class  // 指定配置类
)
public interface UserClient { ... }

6.3 开启日志输出

yaml 复制代码
# application.yml
logging:
  level:
    # 开启 Feign 客户端的日志(DEBUG 级别)
    com.example.orderservice.client.UserClient: DEBUG

6.4 日志示例

BASIC 级别

复制代码
2024-03-30 10:00:00 DEBUG UserClient : [UserClient#getUserById] ---> GET http://user-service/user/1001 HTTP/1.1
2024-03-30 10:00:01 DEBUG UserClient : [UserClient#getUserById] <--- HTTP/1.1 200 (1234ms)

FULL 级别

复制代码
2024-03-30 10:00:00 DEBUG UserClient : [UserClient#getUserById] ---> GET http://user-service/user/1001 HTTP/1.1
2024-03-30 10:00:00 DEBUG UserClient : Content-Length: 0
2024-03-30 10:00:00 DEBUG UserClient : ---> END HTTP (0-byte body)
2024-03-30 10:00:01 DEBUG UserClient : [UserClient#getUserById] <--- HTTP/1.1 200 (1234ms)
2024-03-30 10:00:01 DEBUG UserClient : content-type: application/json
2024-03-30 10:00:01 DEBUG UserClient : {"id":1001,"username":"用户1001","age":25,"email":"user1001@example.com"}
2024-03-30 10:00:01 DEBUG UserClient : <--- END HTTP (89-byte body)

七、拦截器与请求头传递

7.1 请求拦截器

应用场景

  • 统一添加请求头(Token、TraceId)
  • 记录请求日志
  • 签名验证

实现方式

java 复制代码
package com.example.orderservice.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;

/**
 * Feign 请求拦截器
 * 
 * 作用:在发送请求前统一处理
 */
@Component
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // ① 添加统一请求头
        template.header("Authorization", "Bearer token123");
        template.header("X-Request-Source", "order-service");
        template.header("X-Request-Id", generateRequestId());
        
        // ② 记录请求日志
        System.out.println("Feign 请求:" + template.method() + " " + template.url());
        System.out.println("请求头:" + template.headers());
        
        // ③ 添加查询参数(可选)
        template.query("from", "order-service");
    }

    /**
     * 生成请求 ID
     */
    private String generateRequestId() {
        return "REQ-" + System.currentTimeMillis();
    }
}

7.2 传递请求头(ThreadLocal)

问题场景

复制代码
用户请求 → Gateway(带 Token)→ 订单服务 → 用户服务

问题:订单服务调用用户服务时,Token 丢失了!

解决方案:使用 ThreadLocal 传递

步骤 1:创建 RequestContextHolder

java 复制代码
package com.example.orderservice.context;

/**
 * 请求上下文
 */
public class RequestContextHolder {

    private static final ThreadLocal<String> TOKEN_HOLDER = new ThreadLocal<>();
    private static final ThreadLocal<String> REQUEST_ID_HOLDER = new ThreadLocal<>();

    /**
     * 设置 Token
     */
    public static void setToken(String token) {
        TOKEN_HOLDER.set(token);
    }

    /**
     * 获取 Token
     */
    public static String getToken() {
        return TOKEN_HOLDER.get();
    }

    /**
     * 设置 RequestId
     */
    public static void setRequestId(String requestId) {
        REQUEST_ID_HOLDER.set(requestId);
    }

    /**
     * 获取 RequestId
     */
    public static String getRequestId() {
        return REQUEST_ID_HOLDER.get();
    }

    /**
     * 清除上下文
     */
    public static void clear() {
        TOKEN_HOLDER.remove();
        REQUEST_ID_HOLDER.remove();
    }
}

步骤 2:创建 Web 拦截器(保存请求头)

java 复制代码
package com.example.orderservice.interceptor;

import com.example.orderservice.context.RequestContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Web 拦截器:保存请求头到 ThreadLocal
 */
@Component
public class WebRequestInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 保存 Token 到 ThreadLocal
        String token = request.getHeader("Authorization");
        if (token != null) {
            RequestContextHolder.setToken(token);
        }

        // 保存 RequestId
        String requestId = request.getHeader("X-Request-Id");
        if (requestId != null) {
            RequestContextHolder.setRequestId(requestId);
        }

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 请求结束,清除 ThreadLocal
        RequestContextHolder.clear();
    }
}

步骤 3:注册拦截器

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

import com.example.orderservice.interceptor.WebRequestInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Web MVC 配置
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private WebRequestInterceptor webRequestInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(webRequestInterceptor)
                .addPathPatterns("/**");  // 拦截所有请求
    }
}

步骤 4:Feign 拦截器传递请求头

java 复制代码
package com.example.orderservice.interceptor;

import com.example.orderservice.context.RequestContextHolder;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;

/**
 * Feign 拦截器:传递请求头
 */
@Component
public class FeignHeaderInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 从 ThreadLocal 获取 Token 并传递
        String token = RequestContextHolder.getToken();
        if (token != null) {
            template.header("Authorization", token);
        }

        // 传递 RequestId
        String requestId = RequestContextHolder.getRequestId();
        if (requestId != null) {
            template.header("X-Request-Id", requestId);
        }
    }
}

八、OpenFeign 高级特性

8.1 文件上传

java 复制代码
/**
 * Feign 客户端:文件上传
 */
@FeignClient(name = "file-service", configuration = FeignMultipartConfig.class)
public interface FileClient {

    @PostMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    String uploadFile(@RequestPart("file") MultipartFile file);
}

/**
 * Feign 文件上传配置
 */
@Configuration
public class FeignMultipartConfig {

    @Bean
    public Encoder feignFormEncoder() {
        return new SpringFormEncoder();
    }
}

8.2 响应压缩

yaml 复制代码
# application.yml
feign:
  compression:
    request:
      enabled: true  # 开启请求压缩
      mime-types: text/xml,application/xml,application/json  # 压缩类型
      min-request-size: 2048  # 最小压缩大小(字节)
    
    response:
      enabled: true  # 开启响应压缩

8.3 GZIP 压缩

yaml 复制代码
feign:
  compression:
    request:
      enabled: true
    response:
      enabled: true
      useGzipDecoder: true  # 使用 GZIP 解码器

九、最佳实践

9.1 统一异常处理

java 复制代码
package com.example.orderservice.exception;

import feign.Response;
import feign.codec.ErrorDecoder;
import org.springframework.stereotype.Component;

/**
 * Feign 异常解码器
 */
@Component
public class FeignErrorDecoder implements ErrorDecoder {

    @Override
    public Exception decode(String methodKey, Response response) {
        switch (response.status()) {
            case 400:
                return new IllegalArgumentException("参数错误");
            case 401:
                return new RuntimeException("未授权");
            case 404:
                return new RuntimeException("资源不存在");
            case 500:
                return new RuntimeException("服务器内部错误");
            default:
                return new RuntimeException("未知错误:" + response.status());
        }
    }
}

9.2 Feign 客户端接口抽取

创建公共 API 模块

java 复制代码
// common-api 模块
package com.example.api;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

@FeignClient(name = "user-service", path = "/user")
public interface UserApi {
    @GetMapping("/{id}")
    User getUserById(@PathVariable("id") Long id);
}

服务提供者实现接口

java 复制代码
// user-service 模块
@RestController
public class UserController implements UserApi {
    @Override
    public User getUserById(Long id) {
        // 实现逻辑
    }
}

服务消费者直接使用

java 复制代码
// order-service 模块
@Autowired
private UserApi userApi;  // 直接注入,无需定义接口

9.3 配置优先级

复制代码
FeignClient 注解配置 > 配置文件针对单个服务 > 配置文件默认配置 > 代码配置

十、常见面试题

面试题 1:OpenFeign 和 Feign 的区别?

参考答案

对比项 Feign OpenFeign
维护者 Netflix SpringCloud 社区
集成 需手动配置 自动集成 SpringBoot
负载均衡 集成 Ribbon 集成 LoadBalancer
熔断降级 需手动集成 自动集成 Sentinel/Hystrix
推荐指数 ⭐⭐⭐ ⭐⭐⭐⭐⭐

面试题 2:OpenFeign 的工作原理是什么?

参考答案

  1. 启动时扫描

    • 扫描 @FeignClient 注解
    • 为每个接口生成代理对象(JDK 动态代理)
  2. 方法调用

    • 拦截接口方法调用
    • 解析注解(@GetMapping、@PathVariable 等)
    • 构造 HTTP 请求
  3. 服务发现

    • 从 Nacos 获取服务实例列表
    • LoadBalancer 选择一个实例
  4. 发送请求

    • 发送 HTTP 请求
    • 接收响应
    • 反序列化为 Java 对象

面试题 3:OpenFeign 如何实现负载均衡?

参考答案

OpenFeign 集成了 Spring Cloud LoadBalancer:

  1. 从 Nacos 获取实例列表

    复制代码
    user-service: [
      {ip: "192.168.1.101", port: 8081},
      {ip: "192.168.1.102", port: 8081}
    ]
  2. LoadBalancer 选择实例

    • 默认策略:轮询(Round Robin)
    • 可配置:随机、加权等
  3. 替换服务名为实际地址

    复制代码
    http://user-service/user/1001
    →
    http://192.168.1.101:8081/user/1001

面试题 4:OpenFeign 超时时间如何配置?

参考答案

方式 1:配置文件(推荐)

yaml 复制代码
feign:
  client:
    config:
      default:  # 全局配置
        connectTimeout: 5000   # 连接超时 5 秒
        readTimeout: 10000     # 读取超时 10 秒
      
      user-service:  # 针对单个服务
        readTimeout: 30000     # 30 秒

方式 2:代码配置

java 复制代码
@Bean
public Request.Options requestOptions() {
    return new Request.Options(5000, 10000);
}

面试题 5:OpenFeign 如何传递请求头?

参考答案

方式 1:在接口方法中声明

java 复制代码
@GetMapping("/user/{id}")
User getUserById(
    @PathVariable("id") Long id,
    @RequestHeader("Authorization") String token
);

方式 2:使用拦截器

java 复制代码
@Component
public class FeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        template.header("Authorization", "Bearer token123");
    }
}

方式 3:ThreadLocal 传递

java 复制代码
// Web 拦截器保存请求头到 ThreadLocal
RequestContextHolder.setToken(token);

// Feign 拦截器从 ThreadLocal 获取并传递
template.header("Authorization", RequestContextHolder.getToken());

面试题 6:OpenFeign 如何处理异常?

参考答案

方式 1:自定义 ErrorDecoder

java 复制代码
@Component
public class FeignErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        switch (response.status()) {
            case 404:
                return new NotFoundException("资源不存在");
            case 500:
                return new ServerException("服务器错误");
            default:
                return new RuntimeException("未知错误");
        }
    }
}

方式 2:使用 try-catch

java 复制代码
try {
    User user = userClient.getUserById(1001L);
} catch (FeignException e) {
    if (e.status() == 404) {
        // 处理 404
    }
}

面试题 7:OpenFeign 日志级别有哪些?

参考答案

级别 输出内容
NONE 不记录(默认)
BASIC 请求方法、URL、响应状态码、执行时间
HEADERS BASIC + 请求/响应头
FULL BASIC + HEADERS + 请求/响应体

配置方式

java 复制代码
@Bean
public Logger.Level feignLoggerLevel() {
    return Logger.Level.FULL;
}

面试题 8:OpenFeign 性能优化有哪些方法?

参考答案

  1. 使用 HTTP 连接池

    xml 复制代码
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-httpclient</artifactId>
    </dependency>
    yaml 复制代码
    feign:
      httpclient:
        enabled: true
        max-connections: 200
        max-connections-per-route: 50
  2. 开启响应压缩

    yaml 复制代码
    feign:
      compression:
        response:
          enabled: true
  3. 合理设置超时时间

    yaml 复制代码
    feign:
      client:
        config:
          default:
            connectTimeout: 2000
            readTimeout: 5000
  4. 禁用不必要的重试

    java 复制代码
    @Bean
    public Retryer feignRetryer() {
        return Retryer.NEVER_RETRY;
    }

面试题 9:OpenFeign 和 RestTemplate 的区别?

参考答案

对比项 OpenFeign RestTemplate
调用方式 声明式(接口 + 注解) 编程式(手动拼接)
代码量
可读性 一般
负载均衡 自动集成 需要 @LoadBalanced
熔断降级 自动集成 需手动实现
学习成本
推荐指数 ⭐⭐⭐⭐⭐ ⭐⭐⭐

面试题 10:OpenFeign 如何实现降级?

参考答案

方式 1:fallback(推荐)

java 复制代码
@FeignClient(
    name = "user-service",
    fallback = UserClientFallback.class  // 降级类
)
public interface UserClient {
    @GetMapping("/user/{id}")
    User getUserById(@PathVariable("id") Long id);
}

@Component
public class UserClientFallback implements UserClient {
    @Override
    public User getUserById(Long id) {
        // 降级逻辑
        return new User(id, "降级用户", 0, "");
    }
}

方式 2:fallbackFactory(可获取异常信息)

java 复制代码
@FeignClient(
    name = "user-service",
    fallbackFactory = UserClientFallbackFactory.class
)
public interface UserClient { ... }

@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
    @Override
    public UserClient create(Throwable cause) {
        return new UserClient() {
            @Override
            public User getUserById(Long id) {
                System.out.println("降级原因:" + cause.getMessage());
                return new User(id, "降级用户", 0, "");
            }
        };
    }
}

总结

本文详细介绍了 OpenFeign 的使用方法:

快速入门 :定义接口 + @FeignClient 注解

参数传递 :@PathVariable、@RequestParam、@RequestBody

负载均衡 :集成 LoadBalancer,支持自定义策略

超时重试 :配置超时时间和重试策略

日志配置 :4 种日志级别,方便调试

拦截器 :统一处理请求头、签名、日志

高级特性 :文件上传、响应压缩、异常处理

最佳实践:接口抽取、统一异常处理

下一篇预告:《SpringCloud 网关、配置与熔断:Gateway+Nacos 配置+Sentinel 入门》


📚 学习建议

  • 先掌握基本用法(@FeignClient、参数传递)
  • 理解负载均衡和超时配置
  • 实践中遇到问题再深入研究拦截器和高级特性
相关推荐
Meepo_haha3 小时前
Maven Spring框架依赖包
java·spring·maven
Flittly3 小时前
【SpringAIAlibaba新手村系列】(5)Prompt 提示词基础与多种消息类型
java·笔记·spring·ai·springboot
杜子不疼.3 小时前
高并发场景下 Spring MVC + 虚拟线程 vs WebFlux 选型对比
java·人工智能·spring·mvc
yyt36304584119 小时前
spring单例bean线程安全问题讨论
java·spring
云烟成雨TD21 小时前
Spring AI 1.x 系列【14】三月双版本连发!Spring AI 最新功能全掌握
java·人工智能·spring
希望永不加班1 天前
SpringBoot 编写第一个 REST 接口(Get/Post/Put/Delete)
java·spring boot·后端·spring
一直都在5721 天前
Java并发面经(二)
java·开发语言·spring
白露与泡影1 天前
Spring Cloud进阶--分布式权限校验OAuth2
分布式·spring cloud·wpf