第四章:Feign客户端

理论讲解

什么是Feign?
Feign就像是微服务世界里的"智能电话",你只需要告诉它你想给谁打电话(定义接口),它就会自动帮你拨号、通话、处理所有通信细节。

相比RestClient需要手动构造请求,Feign让你像调用本地方法一样调用远程服务。

应用场景举例:
想象一下公司内部的部门协作:

  • 财务部提供报销API,销售部需要调用
  • 使用Feign,销售部就像调用自己部门的方法一样调用财务部的方法
  • 不需要关心HTTP请求的构造、URL拼接、参数序列化等细节
  • 即使财务部换了办公室(服务地址变化),销售部也无需修改代码

项目结构

chapter-04-feign/
├── commons-service/        # 公共基础服务
├── eureka-server/          # 注册中心服务端
├── user-service/           # 用户服务
├── product-service/        # 商品服务
├── order-service/          # 订单服务(使用Feign)
└── payment-service/        # 支付服务(新增)

完整代码实现

1. 父工程pom.xml更新

在父工程中添加OpenFeign依赖管理:


2. Order Service订单服务(使用Feign)

pom.xml更新
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.lihaozhe</groupId>
    <artifactId>springcloud-course</artifactId>
    <version>1.0.0</version>
    <relativePath>../../pom.xml</relativePath>
  </parent>

  <artifactId>order-service</artifactId>

  <properties>
    <maven.compiler.source>25</maven.compiler.source>
    <maven.compiler.target>25</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- 引出 commons-service 公共基础服务 -->
    <dependency>
      <groupId>com.lihaozhe</groupId>
      <artifactId>commons-service</artifactId>
      <version>1.0.0</version>
    </dependency>
    <!-- Spring Cloud loadbalancer 依赖管理 -->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    <!-- Spring Cloud openfeign 依赖管理 -->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <dependency>
      <groupId>org.apache.httpcomponents.client5</groupId>
      <artifactId>httpclient5</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <!-- 替换为你的启动类全路径 -->
          <mainClass>com.lihaozhe.orderservice.OrderServiceApplication</mainClass>
        </configuration>
        <!-- 可选:如果需要打包为可执行jar,添加此配置 -->
        <executions>
          <execution>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

</project>

应用启动类更新
package com.lihaozhe.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;

/**
 * 订单服务应用启动类 - Feign版本
 * 使用@EnableFeignClients注解启用Feign客户端功能
 * Feign会自动扫描@FeignClient注解的接口并创建实现类
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderServiceApplication {
  public static void main(String[] args) {
    SpringApplication.run(OrderServiceApplication.class, args);
  }
}

Feign客户端接口定义
UserServiceClient - 用户服务Feign客户端
package com.lihaozhe.orderservice.service;

import com.lihaozhe.userservice.dto.UserDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.List;

/**
 * 用户服务Feign客户端接口
 * <p>
 * FeignClient注解说明:
 * - name: 指定要调用的服务名称(在Eureka中注册的服务名)
 * - path: 可选的请求路径前缀
 * - configuration: 自定义配置类
 * <p>
 * 工作原理:
 * 1. Spring启动时会为这个接口创建动态代理
 * 2. 当调用接口方法时,Feign会构造HTTP请求
 * 3. 通过服务发现找到对应的服务实例
 * 4. 发送HTTP请求并解析响应
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@FeignClient(name = "user-service", path = "/api/users")
public interface UserServiceClient {

  /**
   * 根据用户ID获取用户信息
   * 对应 user-service 的 GET /api/users/{id}
   *
   * @param id 用户ID
   * @return 用户信息
   */
  @GetMapping("/{id}")
  UserDTO getUserById(@PathVariable("id") Long id);

  /**
   * 获取所有用户列表
   * 对应 user-service 的 GET /api/users
   *
   * @return 用户列表
   */
  @GetMapping
  List<UserDTO> getAllUsers();

  /**
   * 创建新用户
   * 对应 user-service 的 POST /api/users
   *
   * @param user 用户信息
   * @return 创建的用户
   */
  @PostMapping
  UserDTO createUser(@RequestBody UserDTO user);

  /**
   * 健康检查
   * 对应 user-service 的 GET /api/users/health
   *
   * @return 健康状态
   */
  @GetMapping("/health")
  String healthCheck();
}

ProductServiceClient - 商品服务Feign客户端
package com.lihaozhe.orderservice.service;

import com.lihaozhe.orderservice.config.FeignConfig;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.Map;

/**
 * 商品服务Feign客户端接口
 * <p>
 * Feign的特点:
 * - 声明式调用:只需定义接口,无需实现
 * - 自动服务发现:基于服务名发现实例
 * - 负载均衡:自动集成LoadBalancer
 * - 错误处理:可配置降级逻辑
 * </p>
 * @author 李昊哲
 * @version 1.0.0
 */
@FeignClient(
    name = "product-service",
    path = "/api/products",
    configuration = FeignConfig.class
)
public interface ProductServiceClient {

  /**
   * 根据商品ID获取商品信息
   * 对应 product-service 的 GET /api/products/{id}
   *
   * @param id 商品ID
   * @return 商品信息
   */
  @GetMapping("/{id}")
  Map<String, Object> getProductById(@PathVariable("id") Long id);

  /**
   * 获取所有商品
   * 对应 product-service 的 GET /api/products
   *
   * @return 商品列表
   */
  @GetMapping
  Map<String, Object> getAllProducts();

  /**
   * 根据分类获取商品
   * 对应 product-service 的 GET /api/products/category/{category}
   *
   * @param category 商品分类
   * @return 分类商品列表
   */
  @GetMapping("/category/{category}")
  Map<String, Object> getProductsByCategory(@PathVariable("category") String category);

  /**
   * 更新商品库存
   * 对应 product-service 的 PUT /api/products/{id}/stock
   *
   * @param id      商品ID
   * @param request 库存更新请求
   * @return 更新结果
   */
  @PutMapping("/{id}/stock")
  Map<String, Object> updateStock(
      @PathVariable("id") Long id,
      @RequestBody Map<String, Integer> request
  );

  /**
   * 获取实例信息
   * 对应 product-service 的 GET /api/products/instance-info
   *
   * @return 实例信息
   */
  @GetMapping("/instance-info")
  Map<String, Object> getInstanceInfo();
}

PaymentServiceClient - 支付服务Feign客户端
package com.lihaozhe.orderservice.service;

import com.lihaozhe.orderservice.feign.PaymentServiceFallback;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.Map;

/**
 * 支付服务Feign客户端接口
 * <p>
 * 演示更复杂的Feign使用场景:
 * - 请求体参数
 * - 响应处理
 * - 错误处理
 * </p>
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@FeignClient(
    name = "payment-service",
    path = "/api/payments",
    fallback = PaymentServiceFallback.class  // 降级处理类
)
public interface PaymentServiceClient {

  /**
   * 创建支付
   * 对应 payment-service 的 POST /api/payments
   *
   * @param paymentRequest 支付请求
   * @return 支付结果
   */
  @PostMapping
  Map<String, Object> createPayment(@RequestBody Map<String, Object> paymentRequest);

  /**
   * 查询支付状态
   * 对应 payment-service 的 GET /api/payments/{paymentId}/status
   *
   * @return 支付状态
   */
  @PostMapping("/{paymentId}/status")
  Map<String, Object> getPaymentStatus( @RequestBody Map<String, Object> request);

  /**
   * 退款
   * 对应 payment-service 的 POST /api/payments/refund
   *
   * @param refundRequest 退款请求
   * @return 退款结果
   */
  @PostMapping("/refund")
  Map<String, Object> refund(@RequestBody Map<String, Object> refundRequest);
}

Feign配置类
package com.lihaozhe.orderservice.config;

import feign.Logger;
import feign.codec.ErrorDecoder;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.util.Timeout;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.web.client.RestClient;

import java.net.http.HttpClient;
import java.time.Duration;

/**
 * Feign全局配置类
 * 可以配置连接超时、日志级别、错误处理等
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@Configuration
public class FeignConfig {

  /**
   * 配置Feign日志级别
   * - NONE: 不记录任何日志(默认)
   * - BASIC: 仅记录请求方法、URL、响应状态码和执行时间
   * - HEADERS: 记录BASIC级别的基础上,还记录请求和响应的头信息
   * - FULL: 记录请求和响应的所有信息
   */
  @Bean
  Logger.Level feignLoggerLevel() {
    return Logger.Level.FULL;
  }

  /**
   * 自定义错误解码器
   * 用于处理Feign调用失败时的错误信息
   */
  @Bean
  public ErrorDecoder errorDecoder() {
    return (methodKey, response) -> {
      HttpStatus status = HttpStatus.valueOf(response.status());
      return switch (status) {
        case NOT_FOUND -> new RuntimeException("服务未找到: " + methodKey);
        case BAD_REQUEST -> new RuntimeException("请求参数错误: " + methodKey);
        case SERVICE_UNAVAILABLE -> new RuntimeException("服务暂时不可用: " + methodKey);
        default -> new RuntimeException("服务调用失败: " + methodKey + ", 状态码: " + status);
      };
    };
  }
}

Feign降级处理类
package com.lihaozhe.orderservice.feign;

import com.lihaozhe.orderservice.service.PaymentServiceClient;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 *
 * 支付服务降级处理类
 * 当支付服务不可用时,Feign会调用这个类的方法作为fallback
 * <p>
 * 降级策略:
 * - 返回默认值
 * - 返回缓存数据
 * - 记录日志并抛出业务异常
 * - 触发备用流程
 * </p>
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@Component
public class PaymentServiceFallback implements PaymentServiceClient {

  /**
   * 支付服务不可用时的降级处理
   */
  @Override
  public Map<String, Object> createPayment(Map<String, Object> paymentRequest) {
    System.err.println("支付服务不可用,使用降级处理");

    return Map.of(
        "success", false,
        "message", "支付服务暂时不可用,请稍后重试",
        "fallback", true,
        "paymentId", null
    );
  }

  /**
   * 查询支付状态的降级处理
   */
  @Override
  public Map<String, Object> getPaymentStatus(Map<String, Object> request) {
    System.err.println("支付服务不可用,无法查询支付状态");

    return Map.of(
        "status", "UNKNOWN",
        "message", "支付服务不可用,无法获取支付状态",
        "fallback", true
    );
  }

  /**
   * 退款服务的降级处理
   */
  @Override
  public Map<String, Object> refund(Map<String, Object> refundRequest) {
    System.err.println("支付服务不可用,无法处理退款");

    return Map.of(
        "success", false,
        "message", "支付服务不可用,退款请求已记录,稍后处理",
        "fallback", true,
        "refundId", "FALLBACK_" + System.currentTimeMillis()
    );
  }
}

订单服务类(使用Feign)
package com.lihaozhe.orderservice.service;

import com.lihaozhe.orderservice.dto.OrderDTO;
import com.lihaozhe.orderservice.dto.OrderItemDTO;
import com.lihaozhe.productservice.dto.ProductDTO;
import com.lihaozhe.userservice.dto.UserDTO;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 订单服务类 - Feign版本
 * 使用Feign客户端调用其他微服务,代码更简洁
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@Service
public class OrderService {

  private final UserServiceClient userServiceClient;
  private final ProductServiceClient productServiceClient;
  private final PaymentServiceClient paymentServiceClient;

  private final List<OrderDTO> orders = new ArrayList<>();
  private Long orderIdCounter = 1L;

  /**
   * 构造函数,注入Feign客户端
   */
  public OrderService(UserServiceClient userServiceClient,
                      ProductServiceClient productServiceClient,
                      PaymentServiceClient paymentServiceClient) {
    this.userServiceClient = userServiceClient;
    this.productServiceClient = productServiceClient;
    this.paymentServiceClient = paymentServiceClient;
  }

  /**
   * 创建订单 - 使用Feign调用其他服务
   *
   * @param orderRequest 订单请求
   * @return 订单创建结果
   */
  public Map<String, Object> createOrder(Map<String, Object> orderRequest) {
    Long userId = Long.valueOf(orderRequest.get("userId").toString());
    Long productId = Long.valueOf(orderRequest.get("productId").toString());
    Integer quantity = Integer.valueOf(orderRequest.get("quantity").toString());

    System.out.println("使用Feign创建订单 - 用户ID: " + userId + ", 商品ID: " + productId);

    try {
      // 1. 使用Feign调用用户服务 - 像调用本地方法一样简单
      UserDTO user = userServiceClient.getUserById(userId);
      if (user == null) {
        return Map.of("success", false, "message", "用户不存在");
      }

      // 2. 使用Feign调用商品服务
      Map<String, Object> productResponse = productServiceClient.getProductById(productId);
      if (productResponse == null || productResponse.get("product") == null) {
        return Map.of("success", false, "message", "商品不存在");
      }

      // 3. 提取商品信息
      Map<String, Object> productMap = (Map<String, Object>) productResponse.get("product");
      ProductDTO product = mapToProductDTO(productMap);

      // 4. 检查库存
      if (product.getStock() < quantity) {
        return Map.of("success", false, "message", "库存不足");
      }

      // 5. 创建订单
      OrderItemDTO orderItem = new OrderItemDTO(productId, product.getName(), quantity, product.getPrice());
      List<OrderItemDTO> items = List.of(orderItem);
      Double totalAmount = product.getPrice() * quantity;

      OrderDTO order = new OrderDTO(orderIdCounter++, userId, items, totalAmount, "CREATED");
      orders.add(order);

      // 6. 更新库存
      Map<String, Integer> stockUpdate = Map.of("quantity", -quantity);
      productServiceClient.updateStock(productId, stockUpdate);

      // 7. 调用支付服务
      Map<String, Object> paymentResult = processPayment(order, user);

      // 8. 更新订单状态
      if (Boolean.TRUE.equals(paymentResult.get("success"))) {
        order.setStatus("PAID");
      } else {
        order.setStatus("PAYMENT_FAILED");
      }

      return Map.of(
          "success", true,
          "message", "订单创建成功",
          "orderId", order.getId(),
          "totalAmount", totalAmount,
          "user", user.getUsername(),
          "product", product.getName(),
          "payment", paymentResult,
          "instanceInfo", productResponse.get("instanceInfo")
      );

    } catch (Exception e) {
      System.err.println("创建订单失败: " + e.getMessage());
      return Map.of("success", false, "message", "创建订单失败: " + e.getMessage());
    }
  }

  /**
   * 处理支付 - 使用Feign调用支付服务
   */
  private Map<String, Object> processPayment(OrderDTO order, UserDTO user) {
    Map<String, Object> paymentRequest = Map.of(
        "orderId", order.getId(),
        "amount", order.getTotalAmount(),
        "userId", user.getId(),
        "userName", user.getUsername(),
        "paymentMethod", "ALIPAY"  // 模拟支付方式
    );

    // 使用Feign调用支付服务
    return paymentServiceClient.createPayment(paymentRequest);
  }

  /**
   * 获取订单详情
   */
  public OrderDTO getOrder(Long orderId) {
    return orders.stream()
        .filter(o -> o.getId().equals(orderId))
        .findFirst()
        .orElse(null);
  }

  /**
   * 获取用户订单列表
   */
  public List<OrderDTO> getUserOrders(Long userId) {
    // 验证用户是否存在
    try {
      UserDTO user = userServiceClient.getUserById(userId);
      if (user == null) {
        return List.of();
      }
    } catch (Exception e) {
      System.err.println("验证用户失败: " + e.getMessage());
      return List.of();
    }

    return orders.stream()
        .filter(o -> o.getUserId().equals(userId))
        .toList();
  }

  /**
   * 获取所有用户信息 - 演示Feign调用返回列表
   */
  public List<UserDTO> getAllUsers() {
    try {
      return userServiceClient.getAllUsers();
    } catch (Exception e) {
      System.err.println("获取用户列表失败: " + e.getMessage());
      return List.of();
    }
  }

  /**
   * 根据分类获取商品 - 演示带路径参数的Feign调用
   */
  public Map<String, Object> getProductsByCategory(String category) {
    try {
      return productServiceClient.getProductsByCategory(category);
    } catch (Exception e) {
      System.err.println("根据分类获取商品失败: " + e.getMessage());
      return Map.of("products", List.of(), "error", e.getMessage());
    }
  }

  /**
   * 测试Feign负载均衡 - 连续调用观察实例分布
   */
  public Map<String, Object> testFeignLoadBalancing(int callCount) {
    Map<String, Integer> distribution = new java.util.HashMap<>();

    for (int i = 0; i < callCount; i++) {
      try {
        Map<String, Object> instanceInfo = productServiceClient.getInstanceInfo();
        if (instanceInfo != null) {
          String instanceId = (String) instanceInfo.get("instanceId");
          distribution.put(instanceId, distribution.getOrDefault(instanceId, 0) + 1);
        }

        // 短暂延迟
        Thread.sleep(10);
      } catch (Exception e) {
        System.err.println("Feign负载均衡测试调用失败: " + e.getMessage());
      }
    }

    return Map.of(
        "totalCalls", callCount,
        "distribution", distribution,
        "method", "Feign客户端"
    );
  }

  /**
   * 将Map转换为ProductDTO
   */
  private ProductDTO mapToProductDTO(Map<String, Object> productMap) {
    ProductDTO product = new ProductDTO();
    product.setId(Long.valueOf(productMap.get("id").toString()));
    product.setName((String) productMap.get("name"));
    product.setCategory((String) productMap.get("category"));
    product.setPrice(Double.valueOf(productMap.get("price").toString()));
    product.setStock(Integer.valueOf(productMap.get("stock").toString()));
    product.setDescription((String) productMap.get("description"));
    return product;
  }
}

订单控制器(使用Feign服务)
package com.lihaozhe.orderservice.controller;

import com.lihaozhe.orderservice.dto.OrderDTO;
import com.lihaozhe.orderservice.service.OrderService;
import com.lihaozhe.userservice.dto.UserDTO;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

/**
 * 订单服务控制器 - Feign版本
 * 演示Feign客户端的各种使用场景
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@RestController
@RequestMapping("/api/orders")
public class OrderController {

  private final OrderService orderService;

  public OrderController(OrderService orderService) {
    this.orderService = orderService;
  }

  /**
   * 创建订单 - Feign版本
   * POST /api/orders
   */
  @PostMapping
  public Map<String, Object> createOrder(@RequestBody Map<String, Object> orderRequest) {
    return orderService.createOrder(orderRequest);
  }

  /**
   * 根据订单ID获取订单详情
   * GET /api/orders/{orderId}
   */
  @GetMapping("/{orderId}")
  public OrderDTO getOrder(@PathVariable("orderId") Long orderId) {
    return orderService.getOrder(orderId);
  }

  /**
   * 获取用户的所有订单
   * GET /api/orders/user/{userId}
   */
  @GetMapping("/user/{userId}")
  public List<OrderDTO> getUserOrders(@PathVariable("userId") Long userId) {
    return orderService.getUserOrders(userId);
  }

  /**
   * 获取所有用户信息 - 演示Feign调用
   * GET /api/orders/users/all
   */
  @GetMapping("/users/all")
  public List<UserDTO> getAllUsers() {
    return orderService.getAllUsers();
  }

  /**
   * 根据分类获取商品 - 演示带参数的Feign调用
   * GET /api/orders/products/category/{category}
   */
  @GetMapping("/products/category/{category}")
  public Map<String, Object> getProductsByCategory(@PathVariable("category") String category) {
    return orderService.getProductsByCategory(category);
  }

  /**
   * Feign负载均衡测试
   * GET /api/orders/feign-loadbalance-test?calls=20
   */
  @GetMapping("/feign-loadbalance-test")
  public Map<String, Object> feignLoadBalanceTest(@RequestParam(name = "calls",defaultValue = "20") int calls) {
    return orderService.testFeignLoadBalancing(calls);
  }

  /**
   * 健康检查端点
   * GET /api/orders/health
   */
  @GetMapping("/health")
  public Map<String, Object> health() {
    return Map.of(
        "status", "UP",
        "service", "order-service",
        "timestamp", System.currentTimeMillis(),
        "totalOrders", orderService.getUserOrders(1L).size(), // 示例调用
        "feignEnabled", true
    );
  }

  /**
   * Feign功能演示端点
   * GET /api/orders/feign-demo
   */
  @GetMapping("/feign-demo")
  public Map<String, Object> feignDemo() {
    try {
      // 演示各种Feign调用
      List<UserDTO> users = orderService.getAllUsers();
      Map<String, Object> phoneProducts = orderService.getProductsByCategory("手机");

      return Map.of(
          "usersCount", users != null ? users.size() : 0,
          "phoneProductsCount", phoneProducts != null && phoneProducts.containsKey("products") ?
              ((List) phoneProducts.get("products")).size() : 0,
          "message", "Feign客户端演示成功",
          "timestamp", System.currentTimeMillis()
      );
    } catch (Exception e) {
      return Map.of(
          "error", e.getMessage(),
          "message", "Feign客户端演示失败"
      );
    }
  }
}

订单服务配置
# application.yml
#=============================================
# 0. 必改/高危配置(上线前一定要检查)
#=============================================
# 订单服务实例端口,集群部署时切忌重复
server:
  port: 8083

#=============================================
# 1. Spring 基础配置
#=============================================
# Eureka 注册名,一旦上线禁止变更(消费者会硬编码)
spring:
  application:
    name: order-service
  cloud:
    loadbalancer:
      # 为所有服务启用 Spring Cloud LoadBalancer
      enabled: true
      # 启用自定义负载均衡配置
      # configurations: custom
      cache:
        # 禁用缓存,每次请求都从注册中心获取最新实例(生产环境不建议)
        ttl: 0
    openfeign:
      # 启用熔断器(为下一章做准备)
      circuit breaker:
        enabled: true
      httpclient:
        # 使 Feign 能够使用 Apache HTTP 客户端 5
        hc5:
          enabled: true
#=============================================
# 2. Eureka 实例级配置
#=============================================
# 在 Eureka 控制台显示的主机名,可读性优先
eureka:
  instance:
    hostname: order-service
    # 注册时优先使用 IP,防止 hostname 解析失败
    prefer-ip-address: true
    # 唯一标识,格式 IP:端口
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
    # 心跳间隔 5s,缩短感知时间
    lease-renewal-interval-in-seconds: 5
    # 10s 内收不到心跳即剔除,开发调试可设短
    lease-expiration-duration-in-seconds: 10

  #=============================================
  # 3. Eureka 客户端行为配置
  #=============================================
  # 是否把自己注册到 Eureka(默认 true,显式写出可提醒运维)
  client:
    register-with-eureka: true
    # 是否拉取注册表(默认 true,同上)
    fetch-registry: true
    service-url:
      # 注册中心地址(带安全认证)
      defaultZone: http://admin:lihaozhe@localhost:8761/eureka/


#=============================================
# 4. Actuator 监控配置
#=============================================
# 按需暴露,生产勿暴露 env/beans
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      # 默认 never,always 方便排查
      show-details: always

#=============================================
# 5. 日志级别
#=============================================
# 业务包日志级别,上线后改为 INFO 或 WARN
logging:
  level:
    com.lihaozhe.orderservice: DEBUG
    org.springframework.cloud.loadbalancer: DEBUG

3. Payment Service支付服务(新增)

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>springcloud-course</artifactId>
    <groupId>com.lihaozhe</groupId>
    <version>1.0.0</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>payment-service</artifactId>
  <properties>
    <maven.compiler.source>25</maven.compiler.source>
    <maven.compiler.target>25</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <!-- 替换为你的启动类全路径 -->
    <start-class>com.lihaozhe.paymentservice.PaymentServiceApplication</start-class>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <!-- 替换为你的启动类全路径 -->
          <mainClass>com.lihaozhe.paymentservice.PaymentServiceApplication</mainClass>
        </configuration>
        <!-- 可选:如果需要打包为可执行jar,添加此配置 -->
        <executions>
          <execution>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>
应用启动类
package com.lihaozhe.paymentservice;

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

/**
 * 支付服务应用启动类
 * 提供支付相关的业务功能
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentServiceApplication {
  public static void main(String[] args) {
    SpringApplication.run(PaymentServiceApplication.class, args);
  }
}
支付控制器
package com.lihaozhe.paymentservice.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 支付服务控制器
 * 模拟支付处理流程
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@RestController
@RequestMapping("/api/payments")
public class PaymentController {

  // 支付记录存储
  private final Map<String, Map<String, Object>> payments = new ConcurrentHashMap<>();
  private final AtomicLong paymentIdCounter = new AtomicLong(1000);

  @Value("${server.port}")
  private String serverPort;

  /**
   * 创建支付
   * POST /api/payments
   */
  @PostMapping
  public Map<String, Object> createPayment(@RequestBody Map<String, Object> paymentRequest) {
    Long orderId = Long.valueOf(paymentRequest.get("orderId").toString());
    Double amount = Double.valueOf(paymentRequest.get("amount").toString());
    Long userId = Long.valueOf(paymentRequest.get("userId").toString());

    System.out.println("处理支付请求 - 订单ID: " + orderId + ", 金额: " + amount + ", 用户ID: " + userId);

    // 模拟支付处理
    try {
      // 模拟支付处理时间
      Thread.sleep(200);

      // 生成支付ID
      String paymentId = "PAY_" + paymentIdCounter.incrementAndGet();

      // 模拟支付成功(90%成功率)
      boolean success = Math.random() > 0.1;
      String status = success ? "SUCCESS" : "FAILED";

      // 保存支付记录
      Map<String, Object> payment = Map.of(
          "paymentId", paymentId,
          "orderId", orderId,
          "amount", amount,
          "userId", userId,
          "status", status,
          "paymentTime", System.currentTimeMillis(),
          "processedBy", "payment-service:" + serverPort
      );

      payments.put(paymentId, payment);

      return Map.of(
          "success", success,
          "paymentId", paymentId,
          "status", status,
          "message", success ? "支付成功" : "支付失败,请重试",
          "processedBy", "payment-service:" + serverPort
      );

    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      return Map.of(
          "success", false,
          "paymentId", null,
          "status", "FAILED",
          "message", "支付处理被中断"
      );
    } catch (Exception e) {
      return Map.of(
          "success", false,
          "paymentId", null,
          "status", "FAILED",
          "message", "支付处理异常: " + e.getMessage()
      );
    }
  }

  /**
   * 查询支付状态
   * POST /api/payments/{paymentId}/status
   */
  @PostMapping("/{paymentId}/status")
  public Map<String, Object> getPaymentStatus(@RequestBody Map<String, Object> request) {
    String paymentId = request.get("paymentId").toString();

    Map<String, Object> payment = payments.get(paymentId);
    if (payment == null) {
      return Map.of(
          "status", "NOT_FOUND",
          "message", "支付记录不存在",
          "paymentId", paymentId
      );
    }

    return Map.of(
        "status", payment.get("status"),
        "paymentId", paymentId,
        "orderId", payment.get("orderId"),
        "amount", payment.get("amount"),
        "paymentTime", payment.get("paymentTime"),
        "processedBy", payment.get("processedBy")
    );
  }

  /**
   * 退款处理
   * POST /api/payments/refund
   */
  @PostMapping("/refund")
  public Map<String, Object> refund(@RequestBody Map<String, Object> refundRequest) {
    String paymentId = refundRequest.get("paymentId").toString();
    Double refundAmount = Double.valueOf(refundRequest.get("amount").toString());

    System.out.println("处理退款请求 - 支付ID: " + paymentId + ", 退款金额: " + refundAmount);

    // 模拟退款处理
    try {
      Thread.sleep(150);

      Map<String, Object> payment = payments.get(paymentId);
      if (payment == null) {
        return Map.of(
            "success", false,
            "message", "原支付记录不存在",
            "refundId", null
        );
      }

      // 生成退款ID
      String refundId = "REFUND_" + System.currentTimeMillis();

      return Map.of(
          "success", true,
          "refundId", refundId,
          "paymentId", paymentId,
          "refundAmount", refundAmount,
          "message", "退款处理成功",
          "processedBy", "payment-service:" + serverPort
      );

    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      return Map.of(
          "success", false,
          "message", "退款处理被中断"
      );
    }
  }

  /**
   * 获取所有支付记录(管理用)
   * GET /api/payments/all
   */
  @GetMapping("/all")
  public Map<String, Object> getAllPayments() {
    return Map.of(
        "totalPayments", payments.size(),
        "payments", payments.values(),
        "service", "payment-service:" + serverPort
    );
  }

  /**
   * 健康检查
   * GET /api/payments/health
   */
  @GetMapping("/health")
  public Map<String, Object> health() {
    return Map.of(
        "status", "UP",
        "service", "payment-service",
        "port", serverPort,
        "timestamp", System.currentTimeMillis(),
        "totalPayments", payments.size()
    );
  }
}
支付服务配置
# application.yml
#=============================================
# 0. 必改/高危配置(上线前一定要检查)
#=============================================

# 用户服务实例端口,集群部署时切忌重复
server:
  port: 8086

#=============================================
# 1. Spring 基础配置
#=============================================

# Eureka 注册名,一旦上线禁止变更(消费者会硬编码)
spring:
  application:
    name: payment-service

#=============================================
# 2. Eureka 客户端实例配置
#=============================================

# 在 Eureka 控制台显示的主机名,可读性优先
eureka:
  instance:
    hostname: payment-service

    # 注册时优先使用 IP,防止 hostname 解析失败
    prefer-ip-address: true

    # 唯一标识,格式 IP:端口
    instance-id: ${spring.cloud.client.ip-address}:${server.port}

    # 心跳间隔 5s,缩短感知时间
    lease-renewal-interval-in-seconds: 5

    # 10s 内收不到心跳即剔除,开发调试可设短
    lease-expiration-duration-in-seconds: 10

  #=============================================
  # 3. Eureka 客户端行为配置
  #=============================================

  # 是否把自己注册到 Eureka(默认 true,显式写出可提醒运维)
  client:
    register-with-eureka: true

    # 是否拉取注册表(默认 true,同上)
    fetch-registry: true

    # 注册中心地址(带安全认证)
    service-url:
      defaultZone: http://admin:lihaozhe@localhost:8761/eureka/

#=============================================
# 4. Actuator 监控配置
#=============================================

# 按需暴露,生产勿暴露 env/beans
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics

  # 默认 never,always 方便排查
  endpoint:
    health:
      show-details: always

#=============================================
# 5. 日志级别
#=============================================

# 业务包日志级别,上线后改为 INFO 或 WARN
logging:
  level:
    com.lihaozhe.paymentservice: DEBUG

开发思路和过程

1. Feign核心概念理解:

  • 声明式HTTP客户端:通过接口定义描述HTTP请求
  • 服务发现集成:自动从Eureka获取服务实例
  • 负载均衡:内置LoadBalancer支持
  • 错误处理:支持降级和容错

2. Feign使用步骤:

  1. 添加spring-cloud-starter-openfeign依赖
  2. 使用@EnableFeignClients启用Feign
  3. 定义Feign客户端接口,使用@FeignClient注解
  4. 在接口中使用Spring MVC注解定义方法
  5. 注入Feign客户端并使用

3. Feign高级特性:

  • 自定义配置:超时时间、日志级别、编码器等
  • 降级处理:实现fallback类处理服务不可用情况
  • 请求拦截器:添加认证头等信息
  • 错误解码器:自定义错误处理逻辑

4. Feign vs RestClient对比:

特性 Feign RestClient
代码简洁性 ⭐⭐⭐⭐⭐ ⭐⭐⭐
配置便利性 ⭐⭐⭐⭐⭐ ⭐⭐⭐
服务发现 自动集成 需要手动处理
负载均衡 自动支持 需要配置LoadBalancer
错误处理 支持降级 需要手动处理异常

运行测试

启动顺序:

  1. 启动Eureka Server (端口8761)
  2. 启动User Service (端口8081)
  3. 启动Product Service实例1 (端口8082)
  4. 启动Product Service实例2 (端口8084)
  5. 启动Payment Service (端口8086)
  6. 启动Order Service (端口8083)

测试步骤:

# 1. 测试Feign基本功能
curl http://localhost:8083/api/orders/feign-demo

# 2. 测试Feign负载均衡
curl "http://localhost:8083/api/orders/feign-loadbalance-test?calls=10"

# 3. 创建订单(使用Feign调用所有服务)
curl -X POST http://localhost:8083/api/orders \
  -H "Content-Type: application/json" \
  -d '{"userId": 1, "productId": 1, "quantity": 1}'

# 4. 获取用户列表(Feign调用)
curl http://localhost:8083/api/orders/users/all

# 5. 根据分类获取商品(Feign调用)
curl http://localhost:8083/api/orders/products/category/手机

# 6. 查看支付记录
curl http://localhost:8086/api/payments/all

# 7. 测试支付服务降级(停止payment-service后重试创建订单)
curl -X POST http://localhost:8083/api/orders \
  -H "Content-Type: application/json" \
  -d '{"userId": 2, "productId": 2, "quantity": 1}'

观察Feign特性:

  • 查看控制台日志,观察Feign的详细请求日志
  • 多次调用负载均衡测试,观察请求分布
  • 停止支付服务,观察降级处理效果
  • 对比Feign代码与之前RestClient代码的简洁性

这一章我们学习了Feign客户端的使用,体验了声明式服务调用的便利性。

在下一章中,我们将学习Resilience4j熔断机制,进一步提升系统的稳定性。

更多推荐