跳到主要内容

实战案例:电商微服务

本章通过一个电商系统的实际案例,演示如何在实际项目中使用 OpenFeign 进行服务间调用。我们将构建一个完整的微服务调用链路,涵盖用户服务、商品服务、订单服务等核心模块。

场景概述

业务背景

假设我们需要实现一个电商系统的"下单"功能,涉及以下服务:

  • 用户服务(user-service):管理用户信息
  • 商品服务(product-service):管理商品信息和库存
  • 订单服务(order-service):处理订单逻辑
  • 优惠券服务(coupon-service):管理优惠券

下单流程需要调用多个服务:

  1. 验证用户信息
  2. 查询商品信息
  3. 扣减库存
  4. 验证并使用优惠券
  5. 创建订单

项目结构

ecommerce-platform/
├── api-common/ # 公共 API 定义
│ └── src/main/java/com/example/api/
│ ├── user/
│ │ └── UserApi.java
│ ├── product/
│ │ └── ProductApi.java
│ └── order/
│ └── OrderApi.java

├── user-service/ # 用户服务
│ └── src/main/java/com/example/user/
│ ├── controller/
│ │ └── UserController.java
│ └── service/
│ └── UserService.java

├── product-service/ # 商品服务
│ └── src/main/java/com/example/product/
│ ├── controller/
│ │ └── ProductController.java
│ └── service/
│ └── ProductService.java

├── order-service/ # 订单服务
│ └── src/main/java/com/example/order/
│ ├── client/
│ │ ├── UserClient.java
│ │ ├── ProductClient.java
│ │ └── CouponClient.java
│ ├── config/
│ │ └── FeignConfig.java
│ ├── controller/
│ │ └── OrderController.java
│ ├── service/
│ │ └── OrderService.java
│ └── fallback/
│ └── ClientFallbackFactory.java

└── coupon-service/ # 优惠券服务
└── src/main/java/com/example/coupon/
└── ...

公共 API 定义

为了实现服务端和客户端共享接口定义,我们创建一个公共模块:

用户 API

// api-common/src/main/java/com/example/api/user/UserApi.java
package com.example.api.user;

import com.example.api.dto.UserDTO;
import org.springframework.web.bind.annotation.*;

import java.util.List;

public interface UserApi {

String BASE_PATH = "/api/v1/users";

@GetMapping(BASE_PATH + "/{userId}")
UserDTO getUserById(@PathVariable("userId") Long userId);

@GetMapping(BASE_PATH + "/batch")
List<UserDTO> getUsersByIds(@RequestParam("ids") List<Long> ids);

@GetMapping(BASE_PATH + "/check")
boolean checkUserExists(@RequestParam("userId") Long userId);

@GetMapping(BASE_PATH + "/{userId}/address/{addressId}")
AddressDTO getUserAddress(
@PathVariable("userId") Long userId,
@PathVariable("addressId") Long addressId
);
}

商品 API

// api-common/src/main/java/com/example/api/product/ProductApi.java
package com.example.api.product;

import com.example.api.dto.ProductDTO;
import com.example.api.dto.StockDTO;
import org.springframework.web.bind.annotation.*;

import java.util.List;

public interface ProductApi {

String BASE_PATH = "/api/v1/products";

@GetMapping(BASE_PATH + "/{productId}")
ProductDTO getProductById(@PathVariable("productId") Long productId);

@GetMapping(BASE_PATH + "/batch")
List<ProductDTO> getProductsByIds(@RequestParam("ids") List<Long> ids);

@GetMapping(BASE_PATH + "/{productId}/stock")
StockDTO getStock(@PathVariable("productId") Long productId);

@PostMapping(BASE_PATH + "/{productId}/stock/deduct")
StockDTO deductStock(
@PathVariable("productId") Long productId,
@RequestParam("quantity") Integer quantity
);

@PostMapping(BASE_PATH + "/stock/batch-deduct")
void batchDeductStock(@RequestBody List<StockDeductionRequest> requests);

@PostMapping(BASE_PATH + "/{productId}/stock/restore")
void restoreStock(
@PathVariable("productId") Long productId,
@RequestParam("quantity") Integer quantity
);
}

public record StockDeductionRequest(Long productId, Integer quantity) {}

数据传输对象

// api-common/src/main/java/com/example/api/dto/UserDTO.java
package com.example.api.dto;

public record UserDTO(
Long id,
String username,
String nickname,
String email,
String phone,
Integer status,
String level
) {}

// api-common/src/main/java/com/example/api/dto/ProductDTO.java
package com.example.api.dto;

import java.math.BigDecimal;

public record ProductDTO(
Long id,
String name,
String description,
BigDecimal price,
String category,
String imageUrl,
Integer status
) {}

// api-common/src/main/java/com/example/api/dto/StockDTO.java
package com.example.api.dto;

public record StockDTO(
Long productId,
Integer availableStock,
Integer reservedStock,
Integer totalStock
) {}

// api-common/src/main/java/com/example/api/dto/AddressDTO.java
package com.example.api.dto;

public record AddressDTO(
Long id,
Long userId,
String receiverName,
String phone,
String province,
String city,
String district,
String detailAddress,
Boolean isDefault
) {}

Feign 客户端定义

在订单服务中定义 Feign 客户端:

用户客户端

// order-service/src/main/java/com/example/order/client/UserClient.java
package com.example.order.client;

import com.example.api.dto.AddressDTO;
import com.example.api.dto.UserDTO;
import com.example.api.user.UserApi;
import com.example.order.fallback.UserClientFallbackFactory;
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.RequestParam;

import java.util.List;

@FeignClient(
name = "user-service",
path = UserApi.BASE_PATH,
fallbackFactory = UserClientFallbackFactory.class
)
public interface UserClient extends UserApi {

// 继承 UserApi 的所有方法
// 可以在这里添加额外的方法

@GetMapping("/internal/users/{userId}/level")
String getUserLevel(@PathVariable("userId") Long userId);
}

商品客户端

// order-service/src/main/java/com/example/order/client/ProductClient.java
package com.example.order.client;

import com.example.api.dto.ProductDTO;
import com.example.api.dto.StockDTO;
import com.example.api.product.ProductApi;
import com.example.api.product.StockDeductionRequest;
import com.example.order.fallback.ProductClientFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@FeignClient(
name = "product-service",
path = ProductApi.BASE_PATH,
fallbackFactory = ProductClientFallbackFactory.class
)
public interface ProductClient extends ProductApi {
// 继承 ProductApi 的所有方法
}

优惠券客户端

// order-service/src/main/java/com/example/order/client/CouponClient.java
package com.example.order.client;

import com.example.order.dto.CouponDTO;
import com.example.order.dto.CouponValidationResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

@FeignClient(
name = "coupon-service",
path = "/api/v1/coupons",
fallbackFactory = CouponClientFallbackFactory.class
)
public interface CouponClient {

@GetMapping("/{couponId}")
CouponDTO getCouponById(@PathVariable("couponId") Long couponId);

@GetMapping("/user/{userId}/available")
List<CouponDTO> getUserAvailableCoupons(@PathVariable("userId") Long userId);

@PostMapping("/validate")
CouponValidationResult validateCoupon(@RequestBody CouponValidationRequest request);

@PostMapping("/{couponId}/use")
void useCoupon(
@PathVariable("couponId") Long couponId,
@RequestParam("userId") Long userId,
@RequestParam("orderId") Long orderId
);

@PostMapping("/{couponId}/refund")
void refundCoupon(
@PathVariable("couponId") Long couponId,
@RequestParam("orderId") Long orderId
);
}

public record CouponValidationRequest(
Long couponId,
Long userId,
Long productId,
String productCategory,
Long orderAmount
) {}

降级处理

用户服务降级

// order-service/src/main/java/com/example/order/fallback/UserClientFallbackFactory.java
package com.example.order.fallback;

import com.example.api.dto.AddressDTO;
import com.example.api.dto.UserDTO;
import com.example.order.client.UserClient;
import feign.FeignException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;

@Slf4j
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {

@Override
public UserClient create(Throwable cause) {
return new UserClient() {

@Override
public UserDTO getUserById(Long userId) {
if (cause instanceof FeignException.NotFound) {
// 用户不存在,返回 null
return null;
}

log.error("获取用户信息失败, userId: {}", userId, cause);
// 用户信息是关键数据,不能返回假数据
throw new ServiceException("用户服务暂时不可用,请稍后重试");
}

@Override
public List<UserDTO> getUsersByIds(List<Long> ids) {
log.error("批量获取用户信息失败, ids: {}", ids, cause);
// 批量查询可以返回空列表
return Collections.emptyList();
}

@Override
public boolean checkUserExists(Long userId) {
log.error("检查用户存在失败, userId: {}", userId, cause);
// 无法确认用户存在,返回 false
return false;
}

@Override
public AddressDTO getUserAddress(Long userId, Long addressId) {
log.error("获取用户地址失败, userId: {}, addressId: {}", userId, addressId, cause);
throw new ServiceException("获取收货地址失败,请稍后重试");
}

@Override
public String getUserLevel(Long userId) {
log.error("获取用户等级失败, userId: {}", userId, cause);
// 降级为普通用户等级
return "NORMAL";
}
};
}
}

商品服务降级

// order-service/src/main/java/com/example/order/fallback/ProductClientFallbackFactory.java
package com.example.order.fallback;

import com.example.api.dto.ProductDTO;
import com.example.api.dto.StockDTO;
import com.example.api.product.StockDeductionRequest;
import com.example.order.client.ProductClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;

@Slf4j
@Component
public class ProductClientFallbackFactory implements FallbackFactory<ProductClient> {

@Override
public ProductClient create(Throwable cause) {
return new ProductClient() {

@Override
public ProductDTO getProductById(Long productId) {
log.error("获取商品信息失败, productId: {}", productId, cause);
throw new ServiceException("商品服务暂时不可用,请稍后重试");
}

@Override
public List<ProductDTO> getProductsByIds(List<Long> ids) {
log.error("批量获取商品信息失败, ids: {}", ids, cause);
return Collections.emptyList();
}

@Override
public StockDTO getStock(Long productId) {
log.error("获取库存信息失败, productId: {}", productId, cause);
// 返回一个表示库存未知的对象
return new StockDTO(productId, 0, 0, 0);
}

@Override
public StockDTO deductStock(Long productId, Integer quantity) {
log.error("扣减库存失败, productId: {}, quantity: {}", productId, quantity, cause);
throw new ServiceException("库存扣减失败,请稍后重试");
}

@Override
public void batchDeductStock(List<StockDeductionRequest> requests) {
log.error("批量扣减库存失败, requests: {}", requests, cause);
throw new ServiceException("库存扣减失败,请稍后重试");
}

@Override
public void restoreStock(Long productId, Integer quantity) {
log.error("恢复库存失败, productId: {}, quantity: {}", productId, quantity, cause);
// 库存恢复失败只记录日志,不影响主流程
}
};
}
}

配置类

// order-service/src/main/java/com/example/order/config/FeignConfig.java
package com.example.order.config;

import feign.Logger;
import feign.Request;
import feign.Retryer;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Slf4j
@Configuration
public class FeignConfig {

/**
* 日志级别配置
*/
@Bean
public Logger.Level feignLoggerLevel() {
// 生产环境使用 BASIC,开发环境可以使用 FULL
return Logger.Level.BASIC;
}

/**
* 请求选项配置
*/
@Bean
public Request.Options requestOptions() {
return new Request.Options(
5, TimeUnit.SECONDS, // 连接超时 5 秒
10, TimeUnit.SECONDS, // 读取超时 10 秒
true // 跟随重定向
);
}

/**
* 重试配置
*/
@Bean
public Retryer retryer() {
// 最多重试 3 次,初始间隔 100ms,最大间隔 1s
return new Retryer.Default(100, 1000, 3);
}

/**
* 错误解码器
*/
@Bean
public ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}
}
// 自定义错误解码器
package com.example.order.config;

import com.example.order.exception.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Slf4j
public class CustomErrorDecoder implements ErrorDecoder {

private final ObjectMapper objectMapper = new ObjectMapper();
private final ErrorDecoder defaultDecoder = new Default();

@Override
public Exception decode(String methodKey, Response response) {
int status = response.status();
String body = readResponseBody(response);

log.error("Feign 调用错误: method={}, status={}, body={}",
methodKey, status, body);

try {
ApiErrorResponse error = objectMapper.readValue(body, ApiErrorResponse.class);
String errorCode = error.code();
String message = error.message();

return switch (status) {
case 400 -> new BadRequestException(message);
case 401 -> new UnauthorizedException(message);
case 403 -> new ForbiddenException(message);
case 404 -> new NotFoundException(message);
case 409 -> new ConflictException(message);
case 429 -> new RateLimitException(message);
default -> {
if (status >= 500) {
yield new ServiceUnavailableException(message);
}
yield defaultDecoder.decode(methodKey, response);
}
};
} catch (Exception e) {
return defaultDecoder.decode(methodKey, response);
}
}

private String readResponseBody(Response response) {
try {
if (response.body() != null) {
return new String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8);
}
} catch (IOException e) {
log.warn("读取响应体失败", e);
}
return "";
}
}

record ApiErrorResponse(String code, String message) {}

订单服务实现

创建订单请求

// order-service/src/main/java/com/example/order/dto/CreateOrderRequest.java
package com.example.order.dto;

import java.math.BigDecimal;
import java.util.List;

public record CreateOrderRequest(
Long userId,
Long addressId,
List<OrderItemRequest> items,
Long couponId,
String remark
) {}

public record OrderItemRequest(
Long productId,
Integer quantity
) {}

// order-service/src/main/java/com/example/order/dto/CreateOrderResponse.java
package com.example.order.dto;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

public record CreateOrderResponse(
Long orderId,
String orderNo,
Long userId,
BigDecimal totalAmount,
BigDecimal discountAmount,
BigDecimal payAmount,
List<OrderItemResponse> items,
LocalDateTime createTime
) {}

public record OrderItemResponse(
Long productId,
String productName,
BigDecimal price,
Integer quantity,
BigDecimal subtotal
) {}

订单服务实现

// order-service/src/main/java/com/example/order/service/OrderService.java
package com.example.order.service;

import com.example.api.dto.AddressDTO;
import com.example.api.dto.ProductDTO;
import com.example.api.dto.StockDTO;
import com.example.api.dto.UserDTO;
import com.example.order.client.CouponClient;
import com.example.order.client.ProductClient;
import com.example.order.client.UserClient;
import com.example.order.dto.*;
import com.example.order.entity.Order;
import com.example.order.entity.OrderItem;
import com.example.order.exception.*;
import com.example.order.repository.OrderRepository;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {

private final UserClient userClient;
private final ProductClient productClient;
private final CouponClient couponClient;
private final OrderRepository orderRepository;
private final OrderIdGenerator orderIdGenerator;

/**
* 创建订单
*/
@Transactional
@CircuitBreaker(name = "createOrder", fallbackMethod = "createOrderFallback")
public CreateOrderResponse createOrder(CreateOrderRequest request) {
log.info("开始创建订单, userId: {}, items: {}", request.userId(), request.items());

// 1. 验证用户
UserDTO user = validateUser(request.userId());
AddressDTO address = validateAddress(request.userId(), request.addressId());

// 2. 验证商品并获取信息
List<OrderItemRequest> itemRequests = request.items();
Map<Long, ProductDTO> productMap = getProducts(itemRequests);

// 3. 验证库存
validateStock(itemRequests, productMap);

// 4. 扣减库存
deductStock(itemRequests);

try {
// 5. 计算金额
BigDecimal totalAmount = calculateTotalAmount(itemRequests, productMap);
BigDecimal discountAmount = BigDecimal.ZERO;

// 6. 验证并使用优惠券
if (request.couponId() != null) {
discountAmount = validateAndUseCoupon(
request.couponId(),
request.userId(),
totalAmount
);
}

BigDecimal payAmount = totalAmount.subtract(discountAmount);

// 7. 创建订单
Order order = buildOrder(request, user, address, totalAmount, discountAmount, payAmount);
order = orderRepository.save(order);

// 8. 创建订单项
List<OrderItem> orderItems = buildOrderItems(order.getId(), itemRequests, productMap);
orderItemRepository.saveAll(orderItems);

log.info("订单创建成功, orderId: {}, orderNo: {}", order.getId(), order.getOrderNo());

return buildResponse(order, orderItems, productMap);

} catch (Exception e) {
// 订单创建失败,恢复库存
restoreStock(itemRequests);
throw e;
}
}

/**
* 验证用户
*/
private UserDTO validateUser(Long userId) {
UserDTO user = userClient.getUserById(userId);
if (user == null) {
throw new UserNotFoundException("用户不存在: " + userId);
}
if (user.status() != 1) {
throw new UserDisabledException("用户已被禁用: " + userId);
}
return user;
}

/**
* 验证收货地址
*/
private AddressDTO validateAddress(Long userId, Long addressId) {
AddressDTO address = userClient.getUserAddress(userId, addressId);
if (address == null) {
throw new AddressNotFoundException("收货地址不存在: " + addressId);
}
return address;
}

/**
* 批量获取商品信息
*/
private Map<Long, ProductDTO> getProducts(List<OrderItemRequest> items) {
List<Long> productIds = items.stream()
.map(OrderItemRequest::productId)
.distinct()
.collect(Collectors.toList());

List<ProductDTO> products = productClient.getProductsByIds(productIds);

if (products.size() != productIds.size()) {
List<Long> missingIds = productIds.stream()
.filter(id -> products.stream().noneMatch(p -> p.id().equals(id)))
.collect(Collectors.toList());
throw new ProductNotFoundException("商品不存在: " + missingIds);
}

return products.stream()
.collect(Collectors.toMap(ProductDTO::id, Function.identity()));
}

/**
* 验证库存
*/
private void validateStock(List<OrderItemRequest> items, Map<Long, ProductDTO> productMap) {
for (OrderItemRequest item : items) {
StockDTO stock = productClient.getStock(item.productId());
if (stock.availableStock() < item.quantity()) {
ProductDTO product = productMap.get(item.productId());
throw new InsufficientStockException(
"商品【" + product.name() + "】库存不足,当前库存: " + stock.availableStock()
);
}
}
}

/**
* 扣减库存
*/
private void deductStock(List<OrderItemRequest> items) {
List<StockDeductionRequest> requests = items.stream()
.map(item -> new StockDeductionRequest(item.productId(), item.quantity()))
.collect(Collectors.toList());

productClient.batchDeductStock(requests);
}

/**
* 恢复库存
*/
private void restoreStock(List<OrderItemRequest> items) {
for (OrderItemRequest item : items) {
try {
productClient.restoreStock(item.productId(), item.quantity());
} catch (Exception e) {
log.error("恢复库存失败, productId: {}, quantity: {}",
item.productId(), item.quantity(), e);
}
}
}

/**
* 计算订单总金额
*/
private BigDecimal calculateTotalAmount(List<OrderItemRequest> items,
Map<Long, ProductDTO> productMap) {
return items.stream()
.map(item -> {
ProductDTO product = productMap.get(item.productId());
return product.price().multiply(BigDecimal.valueOf(item.quantity()));
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
}

/**
* 验证并使用优惠券
*/
private BigDecimal validateAndUseCoupon(Long couponId, Long userId, BigDecimal orderAmount) {
CouponValidationRequest request = new CouponValidationRequest(
couponId, userId, null, null, orderAmount.longValue()
);

CouponValidationResult result = couponClient.validateCoupon(request);

if (!result.valid()) {
throw new CouponInvalidException(result.message());
}

// 使用优惠券
couponClient.useCoupon(couponId, userId, null); // orderId 稍后更新

return result.discountAmount();
}

/**
* 构建订单实体
*/
private Order buildOrder(CreateOrderRequest request, UserDTO user, AddressDTO address,
BigDecimal totalAmount, BigDecimal discountAmount, BigDecimal payAmount) {
Order order = new Order();
order.setOrderNo(orderIdGenerator.generate());
order.setUserId(request.userId());
order.setUsername(user.username());
order.setAddressId(request.addressId());
order.setReceiverName(address.receiverName());
order.setReceiverPhone(address.phone());
order.setReceiverAddress(buildFullAddress(address));
order.setTotalAmount(totalAmount);
order.setDiscountAmount(discountAmount);
order.setPayAmount(payAmount);
order.setStatus(OrderStatus.CREATED);
order.setRemark(request.remark());
order.setCreateTime(LocalDateTime.now());
return order;
}

/**
* 构建订单项
*/
private List<OrderItem> buildOrderItems(Long orderId, List<OrderItemRequest> items,
Map<Long, ProductDTO> productMap) {
List<OrderItem> orderItems = new ArrayList<>();

for (OrderItemRequest item : items) {
ProductDTO product = productMap.get(item.productId());

OrderItem orderItem = new OrderItem();
orderItem.setOrderId(orderId);
orderItem.setProductId(item.productId());
orderItem.setProductName(product.name());
orderItem.setProductPrice(product.price());
orderItem.setQuantity(item.quantity());
orderItem.setSubtotal(product.price().multiply(BigDecimal.valueOf(item.quantity())));

orderItems.add(orderItem);
}

return orderItems;
}

/**
* 构建响应
*/
private CreateOrderResponse buildResponse(Order order, List<OrderItem> orderItems,
Map<Long, ProductDTO> productMap) {
List<OrderItemResponse> itemResponses = orderItems.stream()
.map(item -> new OrderItemResponse(
item.getProductId(),
item.getProductName(),
item.getProductPrice(),
item.getQuantity(),
item.getSubtotal()
))
.collect(Collectors.toList());

return new CreateOrderResponse(
order.getId(),
order.getOrderNo(),
order.getUserId(),
order.getTotalAmount(),
order.getDiscountAmount(),
order.getPayAmount(),
itemResponses,
order.getCreateTime()
);
}

private String buildFullAddress(AddressDTO address) {
return address.province() + address.city() + address.district() + address.detailAddress();
}

/**
* 创建订单降级方法
*/
public CreateOrderResponse createOrderFallback(CreateOrderRequest request, Throwable t) {
log.error("创建订单降级, request: {}", request, t);
throw new ServiceException("系统繁忙,请稍后重试");
}
}

控制器

// order-service/src/main/java/com/example/order/controller/OrderController.java
package com.example.order.controller;

import com.example.order.dto.CreateOrderRequest;
import com.example.order.dto.CreateOrderResponse;
import com.example.order.service.OrderService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
public class OrderController {

private final OrderService orderService;

@PostMapping
public ResponseEntity<CreateOrderResponse> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
CreateOrderResponse response = orderService.createOrder(request);
return ResponseEntity.ok(response);
}
}

配置文件

# order-service/src/main/resources/application.yml
spring:
application:
name: order-service

cloud:
nacos:
discovery:
server-addr: ${NACOS_SERVER:localhost:8848}
namespace: ${NACOS_NAMESPACE:public}

openfeign:
circuitbreaker:
enabled: true
group:
enabled: true

client:
config:
default:
connectTimeout: 5000
readTimeout: 10000
loggerLevel: basic

user-service:
connectTimeout: 3000
readTimeout: 5000

product-service:
connectTimeout: 3000
readTimeout: 5000

coupon-service:
connectTimeout: 3000
readTimeout: 5000

compression:
request:
enabled: true
response:
enabled: true

httpclient:
hc5:
enabled: true
max-connections: 200
max-connections-per-route: 50

# Resilience4j 熔断配置
resilience4j:
circuitbreaker:
configs:
default:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10s
permittedNumberOfCallsInHalfOpenState: 3
instances:
createOrder:
baseConfig: default
failureRateThreshold: 30

# 日志配置
logging:
level:
com.example.order.client: DEBUG

服务端实现

用户服务实现 UserApi 接口:

// user-service/src/main/java/com/example/user/controller/UserController.java
package com.example.user.controller;

import com.example.api.dto.AddressDTO;
import com.example.api.dto.UserDTO;
import com.example.api.user.UserApi;
import com.example.user.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class UserController implements UserApi {

private final UserService userService;

@Override
public UserDTO getUserById(Long userId) {
return userService.getUserById(userId);
}

@Override
public List<UserDTO> getUsersByIds(List<Long> ids) {
return userService.getUsersByIds(ids);
}

@Override
public boolean checkUserExists(Long userId) {
return userService.checkUserExists(userId);
}

@Override
public AddressDTO getUserAddress(Long userId, Long addressId) {
return userService.getUserAddress(userId, addressId);
}
}

小结

本章通过电商系统的下单场景,演示了 OpenFeign 在实际项目中的应用:

技术点应用场景
接口继承服务端和客户端共享 API 定义
FallbackFactory根据异常类型返回不同的降级结果
批量调用批量获取商品信息、批量扣减库存
熔断保护防止服务故障导致级联失败
事务处理分布式事务补偿(库存恢复)
错误处理统一的异常处理和错误解码

最佳实践总结

  1. 使用公共模块共享 API 定义,避免重复编码
  2. 为关键服务配置合理的降级策略
  3. 批量操作减少网络开销
  4. 使用熔断器保护服务调用
  5. 做好日志记录,便于问题排查
  6. 处理好分布式事务的补偿逻辑