1. 与 Spring Cloud Contract 集成进行消费者驱动契约测试
1.1 消费者驱动契约测试(CDC)基础理论
1.1.1 什么是CDC
消费者驱动契约测试是一种微服务测试方法,通过定义服务间的契约来确保API的兼容性。它解决了微服务架构中服务独立部署和演进的难题。
核心概念:
-
生产者(Provider): 提供API服务的应用程序
-
消费者(Consumer): 使用API服务的应用程序
-
契约(Contract): 描述API请求和响应格式的规范文件
-
存根(Stub): 基于契约生成的模拟服务,用于消费者测试
1.2 Spring Cloud Contract 架构解析
xml
<!-- Maven 依赖配置 -->
<dependencies>
<!-- 生产者端依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
<!-- 消费者端依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
<!-- 可选:使用WireMock作为存根服务器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
1.3 生产者端配置与实现
1.3.1 基础配置
yaml
# application.yml
spring:
cloud:
contract:
verifier:
# 用于生成测试的基础类
base-class-for-tests: com.example.contract.BaseTestClass
# 生成的测试包名
base-package-for-tests: com.example.contract
# 存储契约的目录
contractsDslDir: src/test/resources/contracts
# 生成的测试输出目录
generatedTestSourcesDir: ${project.build.directory}/generated-test-sources/contracts
1.3.2 契约定义示例
groovy
// src/test/resources/contracts/userService/shouldReturnUser.groovy
package contracts.userService
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "根据用户ID查询用户信息"
request {
method GET()
url "/api/users/123"
headers {
contentType(applicationJson())
}
}
response {
status 200
headers {
contentType(applicationJson())
}
body([
id: 123,
name: "张三",
email: "zhangsan@example.com",
age: value(consumer(25), producer(25))
])
}
}
1.3.3 高级契约特性
groovy
// 复杂的契约示例
Contract.make {
description "创建新用户"
request {
method POST()
url "/api/users"
headers {
contentType(applicationJson())
header("X-Request-ID", regex('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}'))
}
body([
name: $(consumer(regex('[a-zA-Z\\s]{3,50}')), producer('李四')),
email: $(consumer(regex(email())), producer('lisi@example.com')),
age: $(consumer(optional(18)), producer(18))
])
}
response {
status 201
headers {
contentType(applicationJson())
header('Location', $(consumer('/api/users/456'), producer(regex('/api/users/[0-9]+'))))
}
body([
id: $(producer(456), consumer(anyNumber())),
name: fromRequest().body('$.name'),
email: fromRequest().body('$.email'),
createdAt: $(producer(regex('[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}')), consumer(anyIso8601WithOffset()))
])
}
}
1.3.4 生成并运行生产者测试
java
// 生成的测试基类
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
@Import(ContractVerifierConfig.class)
public abstract class BaseTestClass {
@Autowired
private WebApplicationContext context;
@Autowired
private ObjectMapper objectMapper;
@Before
public void setup() {
RestAssuredMockMvc.webAppContextSetup(this.context);
}
// 可选:设置数据库状态
@Before
public void setupDb() {
// 准备测试数据
}
}
// 生成的测试类
public class UserServiceContractTest extends BaseTestClass {
@Test
public void validate_shouldReturnUser() throws Exception {
// 自动生成的测试代码
given()
.when()
.get("/api/users/123")
.then()
.statusCode(200)
.body("id", equalTo(123))
.body("name", equalTo("张三"))
.body("email", equalTo("zhangsan@example.com"));
}
}
1.4 消费者端配置与实现
1.4.1 消费者端配置
yaml
# application-test.yml
spring:
cloud:
contract:
stubrunner:
ids: 'com.example:user-service:+:stubs:8080'
repositoryRoot: http://artifactory.example.com/libs-release-local
stubs-per-consumer: true
consumer-name: 'order-service'
1.4.2 消费者测试示例
java
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureStubRunner(
ids = "com.example:user-service:+:stubs:8080",
stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
@ActiveProfiles("test")
public class UserServiceConsumerTest {
@Autowired
private UserServiceClient userServiceClient;
@Test
public void shouldGetUserById() {
// 使用存根服务器进行测试
User user = userServiceClient.getUserById(123L);
assertThat(user).isNotNull();
assertThat(user.getId()).isEqualTo(123L);
assertThat(user.getName()).isEqualTo("张三");
assertThat(user.getEmail()).isEqualTo("zhangsan@example.com");
}
@Test
public void shouldCreateUser() {
CreateUserRequest request = new CreateUserRequest();
request.setName("李四");
request.setEmail("lisi@example.com");
request.setAge(25);
User createdUser = userServiceClient.createUser(request);
assertThat(createdUser).isNotNull();
assertThat(createdUser.getId()).isNotNull();
assertThat(createdUser.getName()).isEqualTo("李四");
assertThat(createdUser.getEmail()).isEqualTo("lisi@example.com");
}
}
1.5 进阶特性与最佳实践
1.5.1 契约版本管理
groovy
// 版本化的契约定义
package contracts.userService.v1
Contract.make {
name "user_service_v1"
request {
method GET()
urlPath "/api/v1/users/123"
}
response {
// v1响应格式
}
}
// v2契约
package contracts.userService.v2
Contract.make {
name "user_service_v2"
request {
method GET()
urlPath "/api/v2/users/123"
}
response {
// v2响应格式(扩展字段)
}
}
1.5.2 使用Contract DSL扩展
groovy
// 自定义DSL扩展
class CustomContractDsl extends Contract {
// 自定义方法
static Map<String, Object> paginatedResponse(Map params = [:]) {
return [
content: params.content ?: [],
page: params.page ?: 0,
size: params.size ?: 20,
totalElements: params.totalElements ?: 0,
totalPages: params.totalPages ?: 1
]
}
}
// 在契约中使用自定义DSL
Contract.make {
request {
method GET()
url "/api/users"
queryParameters {
parameter("page", "0")
parameter("size", "20")
}
}
response {
status 200
body(
CustomContractDsl.paginatedResponse(
content: [
[id: 1, name: "User1"],
[id: 2, name: "User2"]
],
totalElements: 50
)
)
}
}
1.5.3 异步消息契约
groovy
// 消息契约示例
Contract.make {
description "用户创建事件"
// 输入消息(消费者发送)
input {
// 触发消息
triggeredBy('userCreated()')
}
// 输出消息(生产者发送)
outputMessage {
// 发送到哪个目的地
sentTo('user-created-events')
// 消息体
body([
userId: $(producer(anyUuid()), consumer('123e4567-e89b-12d3-a456-426614174000')),
userName: $(producer(anyNonBlankString()), consumer('张三')),
eventType: 'USER_CREATED',
timestamp: $(producer(anyIso8601WithOffset()), consumer('2024-01-01T00:00:00Z'))
])
// 消息头
headers {
header('contentType', applicationJson())
header('messageId', $(anyUuid()))
}
}
}
1.5.4 集成测试策略
java
// 分层测试策略
public class ContractTestingStrategy {
// 1. 单元测试层 - 快速反馈
@Test
public void unitTest() {
// 测试业务逻辑,不涉及外部依赖
}
// 2. 契约测试层 - 验证接口兼容性
@Test
public void contractTest() {
// 使用Spring Cloud Contract验证接口契约
}
// 3. 集成测试层 - 端到端验证
@Test
public void integrationTest() {
// 使用WireMock或Testcontainers进行集成测试
}
// 4. 组件测试层 - 验证组件功能
@Test
public void componentTest() {
// 测试完整的业务场景
}
}
// 使用Testcontainers进行集成测试
@Testcontainers
@SpringBootTest
public class IntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13");
@Container
@ServiceConnection
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine");
@Test
public void testCompleteFlow() {
// 使用真实容器进行测试
}
}
1.5.5 CI/CD流水线集成
yaml
# GitLab CI配置示例
stages:
- build
- contract-test
- integration-test
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
cache:
paths:
- .m2/repository/
build:
stage: build
script:
- mvn clean compile -DskipTests
contract-tests-producer:
stage: contract-test
script:
- mvn clean test -Dtest=*ContractTest -Pcontract-tests
- mvn spring-cloud-contract:convert
- mvn spring-cloud-contract:generateStubs
artifacts:
paths:
- target/stubs/
expire_in: 1 week
contract-tests-consumer:
stage: contract-test
script:
- mvn clean test -Dtest=*ConsumerTest -Pcontract-tests
dependencies:
- contract-tests-producer
integration-tests:
stage: integration-test
services:
- postgres:13
- redis:7-alpine
script:
- mvn verify -Pintegration-tests
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy-staging:
stage: deploy
script:
- mvn deploy -DskipTests -Pstaging
environment:
name: staging
rules:
- if: $CI_COMMIT_BRANCH == "main"
1.5.6 监控与报告
java
// 契约测试报告扩展
@Configuration
public class ContractReportingConfig {
@Bean
public ContractVerifierListener contractVerifierListener() {
return new ContractVerifierListener() {
@Override
public void after(File contractFile, ContractVerifierConfig config) {
// 生成自定义报告
generateCustomReport(contractFile);
}
@Override
public void afterAll() {
// 生成聚合报告
generateAggregateReport();
}
};
}
private void generateCustomReport(File contractFile) {
// 实现自定义报告生成逻辑
Map<String, Object> reportData = new HashMap<>();
reportData.put("contractName", contractFile.getName());
reportData.put("testTimestamp", Instant.now());
reportData.put("status", "SUCCESS");
// 保存到数据库或发送到监控系统
saveToMonitoringSystem(reportData);
}
}
// 使用Micrometer监控契约测试
@Configuration
public class ContractMetricsConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config()
.commonTags("application", "contract-verifier");
}
}
2. 更深入的负载均衡器(Spring Cloud LoadBalancer)配置示例
2.1 Spring Cloud LoadBalancer 架构深度解析
2.1.1 核心组件
java
// LoadBalancer的核心接口
public interface ReactorLoadBalancer<T> {
Mono<Response<T>> choose(Request request);
default Mono<Response<T>> choose() {
return choose(Request.EMPTY);
}
}
// 服务实例选择器
public interface ServiceInstanceListSupplier extends Supplier<Flux<List<ServiceInstance>>> {
String getServiceId();
default Flux<List<ServiceInstance>> get() {
return get(Request.EMPTY);
}
Flux<List<ServiceInstance>> get(Request request);
}
// 负载均衡客户端
public interface LoadBalancerClient {
ServiceInstance choose(String serviceId, Request request);
<T> T execute(String serviceId, ServiceInstance instance,
LoadBalancerRequest<T> request) throws IOException;
URI reconstructURI(ServiceInstance instance, URI original);
}
2.2 基础配置详解
2.2.1 配置文件方式
yaml
# application.yml
spring:
cloud:
loadbalancer:
# 全局配置
cache:
enabled: true
ttl: 30s
capacity: 256
# 服务特定配置
clients:
user-service:
# 负载均衡算法
nfloadbalancer:
rule: round-robin
# 健康检查配置
health-check:
initial-delay: 5s
interval: 30s
enabled: true
# 重试配置
retry:
enabled: true
max-attempts: 3
backoff:
initial-interval: 100ms
max-interval: 1s
multiplier: 2.0
order-service:
nfloadbalancer:
rule: random
health-check:
enabled: false
2.2.2 Java配置方式
java
@Configuration
@LoadBalancerClient(
name = "user-service",
configuration = UserServiceLoadBalancerConfig.class
)
public class UserServiceLoadBalancerConfig {
@Bean
@ConditionalOnMissingBean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RoundRobinLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name
);
}
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withCaching()
.withHealthChecks()
.build(context);
}
@Bean
public LoadBalancerProperties loadBalancerProperties() {
LoadBalancerProperties properties = new LoadBalancerProperties();
properties.setStickySession(true);
properties.setStickySessionCookieName("LB_SESSION_ID");
return properties;
}
}
2.3 高级负载均衡策略实现
2.3.1 自定义权重负载均衡器
java
@Slf4j
public class WeightedLoadBalancer implements ReactorLoadBalancer<ServiceInstance> {
private final String serviceId;
private final ObjectProvider<ServiceInstanceListSupplier> supplierProvider;
private final WeightedLoadBalancerConfig config;
// 权重配置
private final Map<String, Integer> instanceWeights = new ConcurrentHashMap<>();
private final Random random = new Random();
public WeightedLoadBalancer(
ObjectProvider<ServiceInstanceListSupplier> supplierProvider,
String serviceId,
WeightedLoadBalancerConfig config) {
this.supplierProvider = supplierProvider;
this.serviceId = serviceId;
this.config = config;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = supplierProvider.getIfAvailable();
return supplier.get(request).next()
.map(instances -> processInstanceResponse(instances, request))
.doOnError(e -> log.error("Error choosing instance", e));
}
private Response<ServiceInstance> processInstanceResponse(
List<ServiceInstance> instances, Request request) {
if (instances.isEmpty()) {
log.warn("No servers available for service: " + serviceId);
return new EmptyResponse();
}
// 应用权重逻辑
ServiceInstance selectedInstance = selectByWeight(instances, request);
// 记录选择日志
if (log.isDebugEnabled()) {
log.debug("Selected instance {} for service {}",
selectedInstance.getInstanceId(), serviceId);
}
return new DefaultResponse(selectedInstance);
}
private ServiceInstance selectByWeight(
List<ServiceInstance> instances, Request request) {
// 1. 获取实例权重(可从元数据或外部配置读取)
List<WeightedInstance> weightedInstances = instances.stream()
.map(instance -> {
int weight = getInstanceWeight(instance);
return new WeightedInstance(instance, weight);
})
.collect(Collectors.toList());
// 2. 计算总权重
int totalWeight = weightedInstances.stream()
.mapToInt(WeightedInstance::getWeight)
.sum();
// 3. 根据权重随机选择
int randomWeight = random.nextInt(totalWeight);
int currentWeight = 0;
for (WeightedInstance weightedInstance : weightedInstances) {
currentWeight += weightedInstance.getWeight();
if (randomWeight < currentWeight) {
return weightedInstance.getInstance();
}
}
// 回退到第一个实例
return instances.get(0);
}
private int getInstanceWeight(ServiceInstance instance) {
// 从实例元数据获取权重
String weightStr = instance.getMetadata().get("weight");
if (weightStr != null) {
try {
return Integer.parseInt(weightStr);
} catch (NumberFormatException e) {
log.warn("Invalid weight value for instance {}: {}",
instance.getInstanceId(), weightStr);
}
}
// 默认权重
return config.getDefaultWeight();
}
// 权重实例包装类
@Data
@AllArgsConstructor
private static class WeightedInstance {
private ServiceInstance instance;
private int weight;
}
}
// 权重负载均衡器配置
@Configuration
@EnableConfigurationProperties(WeightedLoadBalancerConfig.class)
public class WeightedLoadBalancerConfiguration {
@Bean
@ConditionalOnMissingBean
public ReactorLoadBalancer<ServiceInstance> weightedLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory,
WeightedLoadBalancerConfig config) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new WeightedLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name,
config
);
}
}
// 权重配置属性
@Data
@ConfigurationProperties("spring.cloud.loadbalancer.weighted")
public class WeightedLoadBalancerConfig {
private int defaultWeight = 100;
private boolean enabled = true;
private Map<String, Integer> serviceWeights = new HashMap<>();
}
2.3.2 基于响应时间的自适应负载均衡
java
public class AdaptiveLoadBalancer implements ReactorLoadBalancer<ServiceInstance> {
// 响应时间统计
private final ConcurrentHashMap<String, ResponseTimeStats> statsMap =
new ConcurrentHashMap<>();
// 冷却时间窗口
private final long cooldownWindowMs = 60000; // 1分钟
private final ConcurrentHashMap<String, Long> cooldownMap =
new ConcurrentHashMap<>();
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = supplierProvider.getIfAvailable();
return supplier.get(request).next()
.map(instances -> {
// 过滤掉冷却中的实例
List<ServiceInstance> availableInstances = instances.stream()
.filter(instance -> !isInCooldown(instance.getInstanceId()))
.collect(Collectors.toList());
if (availableInstances.isEmpty()) {
return new EmptyResponse();
}
// 根据响应时间选择实例
ServiceInstance selected = selectByResponseTime(availableInstances);
return new DefaultResponse(selected);
});
}
private ServiceInstance selectByResponseTime(List<ServiceInstance> instances) {
// 获取各实例的响应时间统计
List<ScoredInstance> scoredInstances = instances.stream()
.map(instance -> {
double score = calculateScore(instance.getInstanceId());
return new ScoredInstance(instance, score);
})
.sorted(Comparator.comparingDouble(ScoredInstance::getScore).reversed())
.collect(Collectors.toList());
// 使用softmax选择
return selectBySoftmax(scoredInstances);
}
private double calculateScore(String instanceId) {
ResponseTimeStats stats = statsMap.get(instanceId);
if (stats == null || stats.getRequestCount() < 10) {
return 1.0; // 新实例默认分数
}
// 计算分数:响应时间越短,分数越高
double avgResponseTime = stats.getAverageResponseTime();
double successRate = stats.getSuccessRate();
// 加权计算分数
return (successRate * 0.7) + (1.0 / Math.log1p(avgResponseTime) * 0.3);
}
// 记录响应时间
public void recordResponseTime(String instanceId, long responseTime, boolean success) {
statsMap.compute(instanceId, (key, stats) -> {
if (stats == null) {
stats = new ResponseTimeStats();
}
stats.record(responseTime, success);
return stats;
});
}
// 标记实例进入冷却
public void markForCooldown(String instanceId) {
cooldownMap.put(instanceId, System.currentTimeMillis());
}
private boolean isInCooldown(String instanceId) {
Long cooldownStart = cooldownMap.get(instanceId);
if (cooldownStart == null) {
return false;
}
long elapsed = System.currentTimeMillis() - cooldownStart;
if (elapsed > cooldownWindowMs) {
cooldownMap.remove(instanceId);
return false;
}
return true;
}
// 响应时间统计类
@Data
private static class ResponseTimeStats {
private double averageResponseTime = 0;
private int requestCount = 0;
private int successCount = 0;
public synchronized void record(long responseTime, boolean success) {
// 指数加权移动平均
averageResponseTime = 0.9 * averageResponseTime + 0.1 * responseTime;
requestCount++;
if (success) {
successCount++;
}
}
public double getSuccessRate() {
return requestCount == 0 ? 1.0 : (double) successCount / requestCount;
}
}
}
2.4 区域感知负载均衡
2.4.1 区域感知配置
java
@Configuration
public class ZoneAwareLoadBalancerConfig {
@Bean
public ServiceInstanceListSupplier zoneAwareServiceInstanceListSupplier(
ConfigurableApplicationContext context,
DiscoveryClient discoveryClient,
@Value("${spring.cloud.loadbalancer.zone:default}") String currentZone) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withCaching()
.withZoneAffinity(currentZone) // 区域亲和性
.withHealthChecks()
.withRequestBasedStickySession() // 基于请求的粘性会话
.build(context);
}
@Bean
public ReactorLoadBalancer<ServiceInstance> zoneAwareLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new ZoneAwareLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name,
loadBalancerClientFactory
);
}
}
// 区域感知负载均衡器实现
public class ZoneAwareLoadBalancer implements ReactorLoadBalancer<ServiceInstance> {
private final String serviceId;
private final ObjectProvider<ServiceInstanceListSupplier> supplierProvider;
private final LoadBalancerClientFactory clientFactory;
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
return supplierProvider.getIfAvailable()
.get(request)
.next()
.map(instances -> {
// 按区域分组
Map<String, List<ServiceInstance>> zoneInstances = instances.stream()
.collect(Collectors.groupingBy(
instance -> instance.getMetadata().getOrDefault("zone", "default")
));
// 优先选择本区域实例
String localZone = getLocalZone();
List<ServiceInstance> localInstances = zoneInstances.getOrDefault(localZone,
Collections.emptyList());
if (!localInstances.isEmpty()) {
// 本区域有可用实例
return chooseFromZone(localInstances, request);
}
// 本区域无实例,选择其他区域
for (Map.Entry<String, List<ServiceInstance>> entry : zoneInstances.entrySet()) {
if (!entry.getValue().isEmpty()) {
return chooseFromZone(entry.getValue(), request);
}
}
return new EmptyResponse();
});
}
private Response<ServiceInstance> chooseFromZone(
List<ServiceInstance> instances, Request request) {
// 应用负载均衡策略(如轮询、随机等)
LoadBalancerProperties properties = clientFactory.getProperties(serviceId);
String rule = properties.getRule();
switch (rule) {
case "round-robin":
return roundRobinChoose(instances);
case "random":
return randomChoose(instances);
case "weighted":
return weightedChoose(instances);
default:
return roundRobinChoose(instances);
}
}
private String getLocalZone() {
// 从配置或环境变量获取当前区域
return System.getenv("ZONE") != null ?
System.getenv("ZONE") : "default";
}
}
2.4.2 跨区域故障转移
yaml
# application.yml
spring:
cloud:
loadbalancer:
zone-aware:
enabled: true
# 首选区域
preferred-zones:
- ${ZONE:default}
- zone-b
- zone-c
# 跨区域故障转移策略
failover:
enabled: true
# 最大跨区域调用比例(0.0-1.0)
max-cross-zone-ratio: 0.3
# 是否启用延迟感知
latency-aware: true
# 最大额外延迟容忍度(毫秒)
max-latency-tolerance: 100
2.5 负载均衡与熔断器集成
2.5.1 Resilience4j集成
java
@Configuration
public class LoadBalancerResilienceConfig {
@Bean
public Customizer<ReactiveResilience4JCircuitBreakerFactory> circuitBreakerFactoryCustomizer() {
return factory -> factory.configureDefault(id ->
Resilience4JConfigBuilder.of(id)
.circuitBreakerConfig(CircuitBreakerConfig.custom()
.slidingWindowSize(100)
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.permittedNumberOfCallsInHalfOpenState(10)
.slowCallDurationThreshold(Duration.ofSeconds(2))
.slowCallRateThreshold(50)
.build())
.timeLimiterConfig(TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(5))
.build())
.build());
}
@Bean
public LoadBalancedExchangeFilterFunction loadBalancedExchangeFilterFunction(
LoadBalancerClientFactory clientFactory,
ReactiveResilience4JCircuitBreakerFactory circuitBreakerFactory) {
return new LoadBalancedExchangeFilterFunction(clientFactory,
new LoadBalancerRetryPolicy() {
@Override
public boolean retryOnSameServiceInstance(
LoadBalancerRetryContext context) {
// 在相同实例上重试的条件
return context.getRetryCount() < 2 &&
isRetryableException(context.getException());
}
@Override
public boolean retryOnNextServiceInstance(
LoadBalancerRetryContext context) {
// 切换到下一个实例重试的条件
return context.getRetryCount() < 3;
}
private boolean isRetryableException(Throwable exception) {
return exception instanceof IOException ||
exception instanceof TimeoutException ||
exception instanceof SocketException;
}
},
circuitBreakerFactory);
}
}
// 使用负载均衡的WebClient
@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder(
LoadBalancedExchangeFilterFunction lbFunction) {
return WebClient.builder()
.filter(lbFunction)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10))
)
));
}
2.5.2 请求级别的负载均衡策略
java
@Aspect
@Component
public class RequestAwareLoadBalancerAspect {
private final ThreadLocal<RequestContext> requestContext =
new ThreadLocal<>();
@Before("@annotation(RequestAwareLB)")
public void captureRequestContext(JoinPoint joinPoint) {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
RequestContext context = new RequestContext();
context.setRequestId(request.getHeader("X-Request-ID"));
context.setUserId(request.getHeader("X-User-ID"));
context.setClientIp(request.getRemoteAddr());
context.setRequestPath(request.getRequestURI());
requestContext.set(context);
}
@After("@annotation(RequestAwareLB)")
public void clearRequestContext() {
requestContext.remove();
}
@Component
public static class RequestAwareServiceInstanceListSupplier
implements ServiceInstanceListSupplier {
@Override
public Flux<List<ServiceInstance>> get(Request request) {
return delegate.get(request)
.map(instances -> filterInstancesByRequest(instances, request));
}
private List<ServiceInstance> filterInstancesByRequest(
List<ServiceInstance> instances, Request request) {
RequestContext context = requestContext.get();
if (context == null) {
return instances;
}
// 基于请求上下文过滤实例
return instances.stream()
.filter(instance ->
matchesRequestRequirements(instance, context))
.collect(Collectors.toList());
}
private boolean matchesRequestRequirements(
ServiceInstance instance, RequestContext context) {
Map<String, String> metadata = instance.getMetadata();
// 示例:根据用户ID路由到特定实例组
if (context.getUserId() != null) {
String userGroup = getUserGroup(context.getUserId());
String instanceGroup = metadata.get("user-group");
return userGroup.equals(instanceGroup);
}
return true;
}
private String getUserGroup(String userId) {
// 根据业务逻辑确定用户分组
int hash = userId.hashCode();
return hash % 2 == 0 ? "group-a" : "group-b";
}
}
}
// 自定义请求上下文
@Data
public class RequestContext {
private String requestId;
private String userId;
private String clientIp;
private String requestPath;
private Map<String, String> customAttributes = new HashMap<>();
}
2.6 监控与指标收集
2.6.1 Micrometer指标集成
java
@Configuration
public class LoadBalancerMetricsConfig {
@Bean
public LoadBalancerMetricsRecorder loadBalancerMetricsRecorder(
MeterRegistry meterRegistry) {
return new LoadBalancerMetricsRecorder(meterRegistry);
}
@Bean
public LoadBalancerPropertiesCustomizer metricsPropertiesCustomizer(
LoadBalancerMetricsRecorder recorder) {
return properties -> {
properties.getMetrics().setEnabled(true);
properties.getMetrics().setRecorder(recorder);
};
}
}
@Component
public class LoadBalancerMetricsRecorder {
private final MeterRegistry meterRegistry;
private final Map<String, Timer> requestTimers = new ConcurrentHashMap<>();
private final Map<String, Counter> errorCounters = new ConcurrentHashMap<>();
public LoadBalancerMetricsRecorder(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
public void recordRequest(String serviceId, String instanceId,
long duration, boolean success) {
String timerName = "loadbalancer.requests.duration";
Timer timer = requestTimers.computeIfAbsent(serviceId + "." + instanceId,
key -> Timer.builder(timerName)
.tag("service", serviceId)
.tag("instance", instanceId)
.description("Load balancer request duration")
.register(meterRegistry));
timer.record(duration, TimeUnit.MILLISECONDS);
// 记录成功/失败计数
String counterName = success ?
"loadbalancer.requests.success" : "loadbalancer.requests.failed";
Counter counter = errorCounters.computeIfAbsent(counterName,
key -> Counter.builder(counterName)
.tag("service", serviceId)
.tag("instance", instanceId)
.description("Load balancer request outcomes")
.register(meterRegistry));
counter.increment();
}
public void recordInstanceStatus(String serviceId, String instanceId,
boolean healthy) {
Gauge.builder("loadbalancer.instance.health",
() -> healthy ? 1 : 0)
.tag("service", serviceId)
.tag("instance", instanceId)
.description("Service instance health status")
.register(meterRegistry);
}
}
2.6.2 Grafana监控仪表板配置
json
{
"dashboard": {
"title": "Spring Cloud LoadBalancer Metrics",
"panels": [
{
"title": "Request Duration by Service",
"type": "graph",
"targets": [{
"expr": "rate(loadbalancer_requests_duration_seconds_sum[5m]) / rate(loadbalancer_requests_duration_seconds_count[5m])",
"legendFormat": "{{service}}"
}]
},
{
"title": "Instance Health Status",
"type": "heatmap",
"targets": [{
"expr": "loadbalancer_instance_health",
"legendFormat": "{{instance}}"
}]
},
{
"title": "Error Rate",
"type": "singlestat",
"targets": [{
"expr": "rate(loadbalancer_requests_failed_total[5m]) / rate(loadbalancer_requests_total[5m]) * 100",
"format": "percent"
}]
}
]
}
}
3. 使用 Feign 进行文件上传
3.1 Feign 文件上传基础
3.1.1 依赖配置
xml
<!-- pom.xml 依赖 -->
<dependencies>
<!-- Spring Cloud OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 文件上传支持 -->
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.8.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.8.0</version>
</dependency>
<!-- Spring Web(提供Multipart支持) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 可选:Apache HttpClient -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
</dependencies>
3.1.2 基础配置类
java
@Configuration
@EnableFeignClients
public class FeignFileUploadConfig {
@Bean
public Encoder feignFormEncoder() {
return new SpringFormEncoder(new SpringEncoder(new ObjectFactory<>() {
@Override
public Object getObject() {
return new HttpMessageConverters(
new ByteArrayHttpMessageConverter(),
new StringHttpMessageConverter(),
new ResourceHttpMessageConverter(),
new SourceHttpMessageConverter<>(),
new AllEncompassingFormHttpMessageConverter()
);
}
}));
}
@Bean
public feign.Logger.Level feignLoggerLevel() {
return feign.Logger.Level.FULL;
}
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(100, 1000, 3);
}
// 配置HTTP客户端(可选)
@Bean
public Client feignClient() {
return new ApacheHttpClient();
}
}
3.2 单文件上传实现
3.2.1 服务端接口
java
@RestController
@RequestMapping("/api/files")
@Slf4j
public class FileUploadController {
@PostMapping("/upload")
public ResponseEntity<UploadResult> uploadFile(
@RequestPart("file") MultipartFile file,
@RequestParam(value = "description", required = false) String description,
@RequestHeader(value = "X-User-ID", required = false) String userId) {
try {
// 验证文件
validateFile(file);
// 保存文件
String fileId = saveFile(file, userId);
// 记录元数据
FileMetadata metadata = FileMetadata.builder()
.fileId(fileId)
.originalName(file.getOriginalFilename())
.size(file.getSize())
.contentType(file.getContentType())
.description(description)
.uploadedBy(userId)
.uploadedAt(Instant.now())
.build();
saveMetadata(metadata);
return ResponseEntity.ok(UploadResult.success(fileId, metadata));
} catch (FileValidationException e) {
log.error("文件验证失败", e);
return ResponseEntity.badRequest()
.body(UploadResult.error("VALIDATION_ERROR", e.getMessage()));
} catch (StorageException e) {
log.error("文件存储失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(UploadResult.error("STORAGE_ERROR", "文件存储失败"));
}
}
private void validateFile(MultipartFile file) {
if (file.isEmpty()) {
throw new FileValidationException("文件不能为空");
}
// 文件大小限制(10MB)
if (file.getSize() > 10 * 1024 * 1024) {
throw new FileValidationException("文件大小不能超过10MB");
}
// 文件类型验证
String contentType = file.getContentType();
List<String> allowedTypes = Arrays.asList(
"image/jpeg", "image/png", "image/gif",
"application/pdf", "text/plain"
);
if (!allowedTypes.contains(contentType)) {
throw new FileValidationException("不支持的文件类型");
}
}
private String saveFile(MultipartFile file, String userId) {
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String extension = getFileExtension(originalFilename);
String fileId = UUID.randomUUID().toString();
String filename = fileId + "." + extension;
// 存储路径(可按用户ID分目录)
Path storagePath = Paths.get("uploads",
userId != null ? userId : "anonymous",
filename);
try {
Files.createDirectories(storagePath.getParent());
Files.copy(file.getInputStream(), storagePath,
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new StorageException("文件保存失败", e);
}
return fileId;
}
private String getFileExtension(String filename) {
if (filename == null || !filename.contains(".")) {
return "";
}
return filename.substring(filename.lastIndexOf(".") + 1);
}
// 数据类
@Data
@Builder
public static class UploadResult {
private boolean success;
private String fileId;
private FileMetadata metadata;
private String errorCode;
private String errorMessage;
public static UploadResult success(String fileId, FileMetadata metadata) {
return UploadResult.builder()
.success(true)
.fileId(fileId)
.metadata(metadata)
.build();
}
public static UploadResult error(String errorCode, String errorMessage) {
return UploadResult.builder()
.success(false)
.errorCode(errorCode)
.errorMessage(errorMessage)
.build();
}
}
@Data
@Builder
public static class FileMetadata {
private String fileId;
private String originalName;
private Long size;
private String contentType;
private String description;
private String uploadedBy;
private Instant uploadedAt;
}
}
3.2.2 客户端Feign接口
java
@FeignClient(
name = "file-service",
url = "${feign.client.file-service.url}",
configuration = FileServiceClient.FileServiceConfig.class
)
public interface FileServiceClient {
@PostMapping(value = "/api/files/upload",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
UploadResult uploadFile(
@RequestPart("file") MultipartFile file,
@RequestParam(value = "description", required = false) String description,
@RequestHeader(value = "X-User-ID", required = false) String userId);
// 支持直接上传字节数组
@PostMapping(value = "/api/files/upload-bytes",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
UploadResult uploadFileBytes(
@RequestPart("file") byte[] fileBytes,
@RequestParam("filename") String filename,
@RequestParam(value = "contentType", required = false) String contentType,
@RequestParam(value = "description", required = false) String description,
@RequestHeader(value = "X-User-ID", required = false) String userId);
// 支持上传Resource
@PostMapping(value = "/api/files/upload-resource",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
UploadResult uploadFileResource(
@RequestPart("file") Resource fileResource,
@RequestParam("filename") String filename,
@RequestParam(value = "contentType", required = false) String contentType,
@RequestParam(value = "description", required = false) String description,
@RequestHeader(value = "X-User-ID", required = false) String userId);
// 配置类
@Configuration
class FileServiceConfig {
@Bean
@Primary
@Scope("prototype")
public Encoder feignFormEncoder() {
List<HttpMessageConverter<?>> converters =
new ArrayList<>(new SpringEncoder(getMessageConverters()).getConverters());
converters.add(new ByteArrayHttpMessageConverter());
converters.add(new StringHttpMessageConverter());
converters.add(new ResourceHttpMessageConverter());
converters.add(new SourceHttpMessageConverter<>());
converters.add(new AllEncompassingFormHttpMessageConverter());
FormHttpMessageConverter formConverter = new FormHttpMessageConverter();
formConverter.setMultipartCharset(StandardCharsets.UTF_8);
MultipartHttpMessageConverter multipartConverter =
new MultipartHttpMessageConverter();
multipartConverter.setMultipartCharset(StandardCharsets.UTF_8);
converters.add(formConverter);
converters.add(multipartConverter);
return new SpringFormEncoder(new SpringEncoder(
() -> new HttpMessageConverters(converters)
));
}
@Bean
public Decoder feignDecoder() {
ObjectFactory<HttpMessageConverters> messageConverters =
() -> new HttpMessageConverters(
new MappingJackson2HttpMessageConverter()
);
return new ResponseEntityDecoder(new SpringDecoder(messageConverters));
}
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public RequestInterceptor requestIdInterceptor() {
return template -> {
String requestId = UUID.randomUUID().toString();
template.header("X-Request-ID", requestId);
template.header("X-Caller-Service", "your-service-name");
};
}
@Bean
public ErrorDecoder fileUploadErrorDecoder() {
return new FileUploadErrorDecoder();
}
}
// 自定义错误解码器
class FileUploadErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultDecoder = new Default();
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() == 400 || response.status() == 413) {
try {
// 尝试解析错误响应体
String body = Util.toString(response.body().asReader(StandardCharsets.UTF_8));
UploadResult errorResult = objectMapper.readValue(body, UploadResult.class);
return new FileUploadException(
"文件上传失败: " + errorResult.getErrorMessage(),
errorResult.getErrorCode(),
response.status()
);
} catch (IOException e) {
// 解析失败,返回默认异常
}
}
return defaultDecoder.decode(methodKey, response);
}
}
// 自定义异常
class FileUploadException extends RuntimeException {
private final String errorCode;
private final int statusCode;
public FileUploadException(String message, String errorCode, int statusCode) {
super(message);
this.errorCode = errorCode;
this.statusCode = statusCode;
}
}
}
3.2.3 客户端使用示例
java
@Service
@Slf4j
public class FileUploadService {
private final FileServiceClient fileServiceClient;
private final ObjectMapper objectMapper;
public FileUploadService(FileServiceClient fileServiceClient,
ObjectMapper objectMapper) {
this.fileServiceClient = fileServiceClient;
this.objectMapper = objectMapper;
}
public String uploadSingleFile(MultipartFile file, String userId,
String description) {
try {
log.info("开始上传文件: {}, 大小: {} bytes",
file.getOriginalFilename(), file.getSize());
// 调用Feign客户端
UploadResult result = fileServiceClient.uploadFile(
file, description, userId
);
if (result.isSuccess()) {
log.info("文件上传成功, fileId: {}", result.getFileId());
// 记录上传日志
logUploadSuccess(result.getFileId(), file, userId);
return result.getFileId();
} else {
log.error("文件上传失败: {} - {}",
result.getErrorCode(), result.getErrorMessage());
throw new BusinessException("文件上传失败: " + result.getErrorMessage());
}
} catch (FeignException e) {
log.error("Feign调用失败", e);
handleFeignException(e);
throw new BusinessException("文件服务调用失败");
}
}
public String uploadFileFromPath(Path filePath, String userId,
String description) {
try {
// 读取文件内容
byte[] fileBytes = Files.readAllBytes(filePath);
String filename = filePath.getFileName().toString();
// 获取文件类型
String contentType = Files.probeContentType(filePath);
if (contentType == null) {
contentType = "application/octet-stream";
}
// 使用字节数组上传
UploadResult result = fileServiceClient.uploadFileBytes(
fileBytes, filename, contentType, description, userId
);
if (result.isSuccess()) {
return result.getFileId();
} else {
throw new BusinessException(result.getErrorMessage());
}
} catch (IOException e) {
throw new BusinessException("读取文件失败", e);
}
}
public String uploadFileFromUrl(String fileUrl, String userId,
String description) {
try {
// 从URL下载文件
URL url = new URL(fileUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
if (connection.getResponseCode() != 200) {
throw new BusinessException("无法下载文件: " + fileUrl);
}
// 获取文件名和类型
String filename = getFileNameFromUrl(fileUrl);
String contentType = connection.getContentType();
// 读取文件内容
byte[] fileBytes;
try (InputStream inputStream = connection.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
fileBytes = outputStream.toByteArray();
}
// 上传文件
return uploadFileBytes(fileBytes, filename, contentType,
description, userId);
} catch (IOException e) {
throw new BusinessException("从URL下载文件失败", e);
}
}
private String getFileNameFromUrl(String url) {
try {
URL parsedUrl = new URL(url);
String path = parsedUrl.getPath();
return path.substring(path.lastIndexOf('/') + 1);
} catch (Exception e) {
return "downloaded-file";
}
}
private void logUploadSuccess(String fileId, MultipartFile file, String userId) {
// 记录上传日志到数据库或消息队列
Map<String, Object> logData = new HashMap<>();
logData.put("fileId", fileId);
logData.put("originalName", file.getOriginalFilename());
logData.put("size", file.getSize());
logData.put("contentType", file.getContentType());
logData.put("userId", userId);
logData.put("timestamp", Instant.now());
// 异步记录日志
CompletableFuture.runAsync(() -> {
try {
// 保存到数据库或发送到Kafka
log.info("Upload log: {}", objectMapper.writeValueAsString(logData));
} catch (JsonProcessingException e) {
log.error("Failed to serialize upload log", e);
}
});
}
private void handleFeignException(FeignException e) {
if (e.status() == 400) {
log.warn("文件验证失败: {}", e.getMessage());
} else if (e.status() == 413) {
log.warn("文件过大: {}", e.getMessage());
} else if (e.status() == 504) {
log.warn("文件上传超时: {}", e.getMessage());
} else {
log.error("文件上传服务异常, status: {}", e.status(), e);
}
}
}
3.3 多文件上传实现
3.3.1 服务端多文件接口
java
@RestController
@RequestMapping("/api/files")
public class MultiFileUploadController {
@PostMapping("/upload-multiple")
public ResponseEntity<BatchUploadResult> uploadMultipleFiles(
@RequestPart("files") MultipartFile[] files,
@RequestParam(value = "category", required = false) String category,
@RequestParam(value = "tags", required = false) List<String> tags,
@RequestHeader(value = "X-User-ID") String userId) {
List<FileUploadResult> results = new ArrayList<>();
List<String> errors = new ArrayList<>();
for (int i = 0; i < files.length; i++) {
MultipartFile file = files[i];
try {
// 验证并保存单个文件
String fileId = processSingleFile(file, userId, category, tags);
results.add(FileUploadResult.success(
fileId,
file.getOriginalFilename(),
file.getSize(),
file.getContentType()
));
} catch (Exception e) {
errors.add(String.format("文件 %s 上传失败: %s",
file.getOriginalFilename(), e.getMessage()));
results.add(FileUploadResult.error(
file.getOriginalFilename(),
e.getMessage()
));
}
}
BatchUploadResult batchResult = BatchUploadResult.builder()
.totalFiles(files.length)
.successfulUploads((int) results.stream()
.filter(FileUploadResult::isSuccess).count())
.failedUploads((int) results.stream()
.filter(r -> !r.isSuccess()).count())
.results(results)
.errors(errors)
.build();
if (errors.isEmpty()) {
return ResponseEntity.ok(batchResult);
} else {
return ResponseEntity.status(HttpStatus.MULTI_STATUS)
.body(batchResult);
}
}
@PostMapping("/upload-multipart")
public ResponseEntity<BatchUploadResult> uploadWithMetadata(
@RequestPart("files") MultipartFile[] files,
@RequestPart("metadata") FileUploadMetadata metadata) {
// 处理带元数据的批量上传
return uploadMultipleFiles(
files,
metadata.getCategory(),
metadata.getTags(),
metadata.getUserId()
);
}
// 数据类
@Data
@Builder
public static class FileUploadMetadata {
private String userId;
private String category;
private List<String> tags;
private Map<String, String> customAttributes;
private Instant uploadTime;
private Integer expiryDays;
}
@Data
@Builder
public static class BatchUploadResult {
private int totalFiles;
private int successfulUploads;
private int failedUploads;
private List<FileUploadResult> results;
private List<String> errors;
private String batchId;
private Instant completedAt;
}
@Data
@Builder
public static class FileUploadResult {
private boolean success;
private String fileId;
private String filename;
private Long size;
private String contentType;
private String errorMessage;
public static FileUploadResult success(String fileId, String filename,
Long size, String contentType) {
return FileUploadResult.builder()
.success(true)
.fileId(fileId)
.filename(filename)
.size(size)
.contentType(contentType)
.build();
}
public static FileUploadResult error(String filename, String errorMessage) {
return FileUploadResult.builder()
.success(false)
.filename(filename)
.errorMessage(errorMessage)
.build();
}
}
}
3.3.2 客户端多文件Feign接口
java
@FeignClient(name = "file-service",
configuration = MultiFileServiceClient.MultiFileConfig.class)
public interface MultiFileServiceClient {
@PostMapping(value = "/api/files/upload-multiple",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
BatchUploadResult uploadMultipleFiles(
@RequestPart("files") MultipartFile[] files,
@RequestParam(value = "category", required = false) String category,
@RequestParam(value = "tags", required = false) List<String> tags,
@RequestHeader("X-User-ID") String userId);
@PostMapping(value = "/api/files/upload-multipart",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
BatchUploadResult uploadWithMetadata(
@RequestPart("files") MultipartFile[] files,
@RequestPart("metadata") FileUploadMetadata metadata);
// 流式上传接口
@PostMapping(value = "/api/files/upload-stream",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
ResponseEntity<Void> uploadStream(
@RequestPart("metadata") FileUploadMetadata metadata,
@RequestPart("file") InputStream fileStream,
@RequestParam("filename") String filename,
@RequestHeader("Content-Length") long contentLength,
@RequestHeader("X-User-ID") String userId);
// 配置类
@Configuration
class MultiFileConfig {
@Bean
public Encoder feignEncoder(ObjectFactory<HttpMessageConverters> converters) {
return new SpringFormEncoder(new SpringEncoder(converters) {
@Override
public void encode(Object object, Type bodyType,
RequestTemplate template) {
if (object instanceof MultipartFile[]) {
// 处理多文件数组
encodeMultipartFiles((MultipartFile[]) object, template);
} else if (object instanceof MultiFileRequest) {
// 处理自定义多文件请求
encodeMultiFileRequest((MultiFileRequest) object, template);
} else {
super.encode(object, bodyType, template);
}
}
private void encodeMultipartFiles(MultipartFile[] files,
RequestTemplate template) {
MultipartFormData data = new MultipartFormData();
for (int i = 0; i < files.length; i++) {
MultipartFile file = files[i];
data.addFile("files",
new MultipartFormData.File(
file.getOriginalFilename(),
file.getName(),
getContentType(file),
file
)
);
}
template.body(data, MultipartFormData.class);
}
private String getContentType(MultipartFile file) {
String contentType = file.getContentType();
return contentType != null ? contentType :
"application/octet-stream";
}
});
}
@Bean
public Retryer multipartRetryer() {
return new Retryer.Default(1000, 5000, 5) {
@Override
public void continueOrPropagate(RetryableException e) {
// 对于大文件上传,增加重试间隔
if (e.getMessage().contains("timeout")) {
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
super.continueOrPropagate(e);
}
};
}
@Bean
public RequestInterceptor multipartRequestInterceptor() {
return template -> {
// 为大文件上传添加特殊头信息
if (template.requestBody().asString().contains("multipart/form-data")) {
template.header("Expect", "100-continue");
template.header("X-Upload-Type", "multipart");
}
};
}
}
// 自定义多文件请求封装
@Data
@Builder
public static class MultiFileRequest {
private List<FileItem> files;
private Map<String, String> metadata;
private String userId;
private String category;
private List<String> tags;
@Data
@Builder
public static class FileItem {
private String name;
private byte[] content;
private String contentType;
private String originalFilename;
}
}
}
3.3.3 客户端多文件上传服务
java
@Service
@Slf4j
public class MultiFileUploadService {
private final MultiFileServiceClient fileServiceClient;
private final TaskExecutor taskExecutor;
private final ObjectMapper objectMapper;
public MultiFileUploadService(MultiFileServiceClient fileServiceClient,
@Qualifier("fileUploadTaskExecutor") TaskExecutor taskExecutor,
ObjectMapper objectMapper) {
this.fileServiceClient = fileServiceClient;
this.taskExecutor = taskExecutor;
this.objectMapper = objectMapper;
}
public BatchUploadResult uploadMultipleFiles(List<MultipartFile> files,
String userId, String category,
List<String> tags) {
log.info("开始批量上传 {} 个文件", files.size());
long startTime = System.currentTimeMillis();
try {
MultipartFile[] fileArray = files.toArray(new MultipartFile[0]);
BatchUploadResult result = fileServiceClient.uploadMultipleFiles(
fileArray, category, tags, userId
);
long duration = System.currentTimeMillis() - startTime;
log.info("批量上传完成, 成功: {}, 失败: {}, 耗时: {}ms",
result.getSuccessfulUploads(),
result.getFailedUploads(),
duration);
// 记录批量上传日志
recordBatchUpload(result, userId, duration);
return result;
} catch (FeignException e) {
log.error("批量上传失败", e);
throw new BusinessException("文件批量上传失败: " + e.getMessage());
}
}
public CompletableFuture<BatchUploadResult> uploadMultipleFilesAsync(
List<MultipartFile> files, String userId, String category,
List<String> tags) {
return CompletableFuture.supplyAsync(() ->
uploadMultipleFiles(files, userId, category, tags),
taskExecutor
);
}
public BatchUploadResult uploadLargeFiles(List<Path> filePaths, String userId,
String category, List<String> tags,
ProgressListener progressListener) {
List<CompletableFuture<FileUploadResult>> futures = new ArrayList<>();
List<FileUploadResult> results = new ArrayList<>();
for (Path filePath : filePaths) {
CompletableFuture<FileUploadResult> future =
CompletableFuture.supplyAsync(() ->
uploadLargeFile(filePath, userId, category, tags, progressListener),
taskExecutor
);
futures.add(future);
}
// 等待所有任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.join();
// 收集结果
for (CompletableFuture<FileUploadResult> future : futures) {
try {
results.add(future.get());
} catch (Exception e) {
log.error("获取文件上传结果失败", e);
}
}
return buildBatchResult(results);
}
private FileUploadResult uploadLargeFile(Path filePath, String userId,
String category, List<String> tags,
ProgressListener progressListener) {
try {
String filename = filePath.getFileName().toString();
long fileSize = Files.size(filePath);
log.info("开始上传大文件: {}, 大小: {} bytes", filename, fileSize);
// 分片上传(如果文件很大)
if (fileSize > 10 * 1024 * 1024) { // 大于10MB
return uploadInChunks(filePath, userId, category, tags,
progressListener);
}
// 直接上传小文件
byte[] fileBytes = Files.readAllBytes(filePath);
String contentType = Files.probeContentType(filePath);
// 创建MultipartFile
MultipartFile multipartFile = new MockMultipartFile(
"file", filename, contentType, fileBytes
);
MultipartFile[] files = new MultipartFile[]{multipartFile};
BatchUploadResult batchResult = fileServiceClient.uploadMultipleFiles(
files, category, tags, userId
);
if (!batchResult.getResults().isEmpty()) {
return batchResult.getResults().get(0);
}
return FileUploadResult.error(filename, "上传结果为空");
} catch (IOException e) {
log.error("上传大文件失败: {}", filePath, e);
return FileUploadResult.error(
filePath.getFileName().toString(),
"文件读取失败: " + e.getMessage()
);
}
}
private FileUploadResult uploadInChunks(Path filePath, String userId,
String category, List<String> tags,
ProgressListener progressListener) {
try {
String filename = filePath.getFileName().toString();
String contentType = Files.probeContentType(filePath);
long fileSize = Files.size(filePath);
// 初始化分片上传
String uploadId = initChunkedUpload(filename, fileSize,
contentType, userId);
// 分片上传
int chunkSize = 5 * 1024 * 1024; // 5MB每片
long uploadedBytes = 0;
try (InputStream inputStream = Files.newInputStream(filePath)) {
byte[] buffer = new byte[chunkSize];
int chunkIndex = 0;
while (true) {
int bytesRead = inputStream.read(buffer);
if (bytesRead == -1) break;
// 上传分片
uploadChunk(uploadId, chunkIndex,
Arrays.copyOf(buffer, bytesRead),
bytesRead == chunkSize);
uploadedBytes += bytesRead;
chunkIndex++;
// 更新进度
if (progressListener != null) {
double progress = (double) uploadedBytes / fileSize * 100;
progressListener.onProgress(filename, progress);
}
log.debug("已上传分片 {}: {}/{} bytes",
chunkIndex, uploadedBytes, fileSize);
}
}
// 完成上传
return completeChunkedUpload(uploadId);
} catch (IOException e) {
log.error("分片上传失败", e);
return FileUploadResult.error(
filePath.getFileName().toString(),
"分片上传失败: " + e.getMessage()
);
}
}
private String initChunkedUpload(String filename, long fileSize,
String contentType, String userId) {
// 调用服务端初始化分片上传接口
Map<String, Object> request = new HashMap<>();
request.put("filename", filename);
request.put("fileSize", fileSize);
request.put("contentType", contentType);
request.put("userId", userId);
request.put("chunkSize", 5 * 1024 * 1024);
// 这里需要实现具体的分片上传初始化逻辑
return UUID.randomUUID().toString();
}
private void uploadChunk(String uploadId, int chunkIndex,
byte[] chunkData, boolean isFullChunk) {
// 上传单个分片
// 实现分片上传逻辑
}
private FileUploadResult completeChunkedUpload(String uploadId) {
// 完成分片上传
// 实现完成逻辑
return FileUploadResult.success(uploadId, "chunked-file", 0L,
"application/octet-stream");
}
private void recordBatchUpload(BatchUploadResult result,
String userId, long duration) {
try {
Map<String, Object> logData = new HashMap<>();
logData.put("batchId", result.getBatchId());
logData.put("userId", userId);
logData.put("totalFiles", result.getTotalFiles());
logData.put("successful", result.getSuccessfulUploads());
logData.put("failed", result.getFailedUploads());
logData.put("duration", duration);
logData.put("timestamp", Instant.now());
if (result.getErrors() != null && !result.getErrors().isEmpty()) {
logData.put("errors", result.getErrors());
}
// 异步保存日志
CompletableFuture.runAsync(() -> {
log.info("Batch upload completed: {}",
objectMapper.writeValueAsString(logData));
});
} catch (JsonProcessingException e) {
log.error("Failed to serialize batch upload log", e);
}
}
public interface ProgressListener {
void onProgress(String filename, double progress);
}
}
3.4 高级文件上传特性
3.4.1 断点续传实现
java
@Service
@Slf4j
public class ResumableUploadService {
private final FileServiceClient fileServiceClient;
private final UploadStateRepository uploadStateRepository;
public ResumableUploadService(FileServiceClient fileServiceClient,
UploadStateRepository uploadStateRepository) {
this.fileServiceClient = fileServiceClient;
this.uploadStateRepository = uploadStateRepository;
}
public ResumableUploadSession initResumableUpload(String filename,
long fileSize,
String userId) {
String sessionId = UUID.randomUUID().toString();
ResumableUploadSession session = ResumableUploadSession.builder()
.sessionId(sessionId)
.filename(filename)
.fileSize(fileSize)
.userId(userId)
.chunkSize(5 * 1024 * 1024) // 5MB每片
.createdAt(Instant.now())
.expiresAt(Instant.now().plusHours(24))
.build();
// 保存会话状态
uploadStateRepository.save(session);
return session;
}
public UploadChunkResult uploadChunk(String sessionId, int chunkIndex,
byte[] chunkData, boolean isLastChunk) {
// 获取上传会话
ResumableUploadSession session = uploadStateRepository.findById(sessionId)
.orElseThrow(() -> new BusinessException("上传会话不存在或已过期"));
// 验证分片索引
if (chunkIndex < 0 || chunkIndex > session.getTotalChunks()) {
throw new BusinessException("无效的分片索引");
}
// 上传分片
try {
// 调用Feign接口上传分片
ChunkUploadRequest request = ChunkUploadRequest.builder()
.sessionId(sessionId)
.chunkIndex(chunkIndex)
.chunkData(chunkData)
.isLastChunk(isLastChunk)
.build();
ChunkUploadResponse response = fileServiceClient.uploadChunk(request);
// 更新会话状态
session.getUploadedChunks().add(chunkIndex);
session.setUploadedBytes(session.getUploadedBytes() + chunkData.length);
session.setLastActivity(Instant.now());
if (isLastChunk) {
session.setStatus(UploadStatus.COMPLETED);
session.setCompletedAt(Instant.now());
// 完成上传
completeUpload(sessionId);
}
uploadStateRepository.save(session);
return UploadChunkResult.success(chunkIndex, response.getFileId());
} catch (FeignException e) {
log.error("分片上传失败: session={}, chunk={}", sessionId, chunkIndex, e);
return UploadChunkResult.error(chunkIndex, e.getMessage());
}
}
public ResumableUploadStatus getUploadStatus(String sessionId) {
ResumableUploadSession session = uploadStateRepository.findById(sessionId)
.orElseThrow(() -> new BusinessException("上传会话不存在"));
return ResumableUploadStatus.builder()
.sessionId(sessionId)
.filename(session.getFilename())
.fileSize(session.getFileSize())
.uploadedBytes(session.getUploadedBytes())
.chunkSize(session.getChunkSize())
.uploadedChunks(session.getUploadedChunks())
.status(session.getStatus())
.progress((double) session.getUploadedBytes() / session.getFileSize() * 100)
.createdAt(session.getCreatedAt())
.lastActivity(session.getLastActivity())
.build();
}
public boolean resumeUpload(String sessionId, Path filePath,
ProgressListener progressListener) {
ResumableUploadSession session = uploadStateRepository.findById(sessionId)
.orElseThrow(() -> new BusinessException("上传会话不存在"));
try {
long fileSize = Files.size(filePath);
if (fileSize != session.getFileSize()) {
throw new BusinessException("文件大小不匹配");
}
int chunkSize = session.getChunkSize();
Set<Integer> uploadedChunks = session.getUploadedChunks();
try (InputStream inputStream = Files.newInputStream(filePath)) {
for (int chunkIndex = 0; chunkIndex < session.getTotalChunks(); chunkIndex++) {
// 跳过已上传的分片
if (uploadedChunks.contains(chunkIndex)) {
inputStream.skip(chunkSize);
continue;
}
// 读取分片数据
byte[] buffer = new byte[chunkSize];
int bytesRead = inputStream.read(buffer);
if (bytesRead == -1) break;
// 上传分片
boolean isLastChunk = (chunkIndex == session.getTotalChunks() - 1);
byte[] chunkData = bytesRead == chunkSize ?
buffer : Arrays.copyOf(buffer, bytesRead);
UploadChunkResult result = uploadChunk(sessionId, chunkIndex,
chunkData, isLastChunk);
if (!result.isSuccess()) {
log.error("续传分片失败: chunk={}, error={}",
chunkIndex, result.getErrorMessage());
return false;
}
// 更新进度
if (progressListener != null) {
double progress = (double) session.getUploadedBytes() /
session.getFileSize() * 100;
progressListener.onProgress(session.getFilename(), progress);
}
}
}
return true;
} catch (IOException e) {
log.error("续传失败", e);
return false;
}
}
// 数据类
@Data
@Builder
public static class ResumableUploadSession {
private String sessionId;
private String filename;
private long fileSize;
private String userId;
private int chunkSize;
private Set<Integer> uploadedChunks = new HashSet<>();
private long uploadedBytes;
private UploadStatus status;
private Instant createdAt;
private Instant lastActivity;
private Instant expiresAt;
private Instant completedAt;
public int getTotalChunks() {
return (int) Math.ceil((double) fileSize / chunkSize);
}
}
public enum UploadStatus {
INITIALIZED,
UPLOADING,
COMPLETED,
FAILED,
CANCELLED
}
}
3.4.2 安全特性实现
java
@Component
public class FileUploadSecurityService {
private final AntivirusScanner antivirusScanner;
private final FileTypeValidator fileTypeValidator;
private final RateLimiter rateLimiter;
public FileUploadSecurityService(AntivirusScanner antivirusScanner,
FileTypeValidator fileTypeValidator,
RateLimiter rateLimiter) {
this.antivirusScanner = antivirusScanner;
this.fileTypeValidator = fileTypeValidator;
this.rateLimiter = rateLimiter;
}
public SecurityCheckResult validateFile(MultipartFile file, String userId) {
SecurityCheckResult result = new SecurityCheckResult();
try {
// 1. 检查文件大小
if (!checkFileSize(file)) {
result.addViolation("FILE_SIZE_LIMIT_EXCEEDED",
"文件大小超过限制");
}
// 2. 检查文件类型
if (!fileTypeValidator.isSafeType(file)) {
result.addViolation("UNSAFE_FILE_TYPE",
"不支持的文件类型或潜在危险文件");
}
// 3. 检查文件名
if (!validateFilename(file.getOriginalFilename())) {
result.addViolation("INVALID_FILENAME",
"文件名包含非法字符");
}
// 4. 检查上传频率
if (!rateLimiter.tryAcquire(userId)) {
result.addViolation("RATE_LIMIT_EXCEEDED",
"上传频率过高,请稍后再试");
}
// 5. 病毒扫描
ScanResult scanResult = antivirusScanner.scan(file.getBytes());
if (!scanResult.isClean()) {
result.addViolation("VIRUS_DETECTED",
"文件包含病毒或恶意代码: " + scanResult.getThreatName());
}
// 6. 内容检查(如敏感信息检测)
if (containsSensitiveInfo(file)) {
result.addViolation("SENSITIVE_CONTENT_DETECTED",
"文件包含敏感信息");
}
result.setPassed(result.getViolations().isEmpty());
} catch (IOException e) {
result.addViolation("FILE_PROCESSING_ERROR",
"文件处理失败: " + e.getMessage());
result.setPassed(false);
}
return result;
}
public SecurityCheckResult validateBatch(List<MultipartFile> files, String userId) {
SecurityCheckResult batchResult = new SecurityCheckResult();
batchResult.setPassed(true);
for (MultipartFile file : files) {
SecurityCheckResult fileResult = validateFile(file, userId);
if (!fileResult.isPassed()) {
batchResult.getFileResults().put(
file.getOriginalFilename(), fileResult);
batchResult.setPassed(false);
}
}
return batchResult;
}
public String generateSecureFilename(String originalFilename) {
// 移除路径信息
String safeName = Paths.get(originalFilename).getFileName().toString();
// 移除特殊字符
safeName = safeName.replaceAll("[^a-zA-Z0-9._-]", "_");
// 添加时间戳和随机数防止重名
String timestamp = Instant.now().toString()
.replaceAll("[^0-9]", "").substring(0, 14);
String random = UUID.randomUUID().toString().substring(0, 8);
String extension = "";
int dotIndex = safeName.lastIndexOf('.');
if (dotIndex > 0 && dotIndex < safeName.length() - 1) {
extension = safeName.substring(dotIndex);
safeName = safeName.substring(0, dotIndex);
}
return String.format("%s_%s_%s%s",
safeName, timestamp, random, extension);
}
public byte[] encryptFile(byte[] fileData, String keyId) {
// 实现文件加密
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
// ... 加密逻辑
return fileData;
} catch (Exception e) {
throw new SecurityException("文件加密失败", e);
}
}
public String calculateFileHash(byte[] fileData) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(fileData);
return Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new SecurityException("计算文件哈希失败", e);
}
}
@Data
public static class SecurityCheckResult {
private boolean passed;
private List<Violation> violations = new ArrayList<>();
private Map<String, SecurityCheckResult> fileResults = new HashMap<>();
private Instant checkedAt = Instant.now();
public void addViolation(String code, String message) {
violations.add(new Violation(code, message));
}
@Data
@AllArgsConstructor
public static class Violation {
private String code;
private String message;
}
}
@Component
public static class FileTypeValidator {
private static final Set<String> SAFE_EXTENSIONS = Set.of(
"jpg", "jpeg", "png", "gif", "pdf", "txt", "doc", "docx",
"xls", "xlsx", "ppt", "pptx"
);
private static final Map<String, String> MIME_TYPE_MAP = Map.of(
"image/jpeg", "jpg",
"image/png", "png",
"image/gif", "gif",
"application/pdf", "pdf",
"text/plain", "txt"
);
public boolean isSafeType(MultipartFile file) {
// 1. 检查MIME类型
String mimeType = file.getContentType();
if (mimeType == null) {
return false;
}
// 2. 检查文件扩展名
String originalFilename = file.getOriginalFilename();
if (originalFilename == null) {
return false;
}
String extension = getExtension(originalFilename).toLowerCase();
if (!SAFE_EXTENSIONS.contains(extension)) {
return false;
}
// 3. 验证MIME类型与扩展名是否匹配
String expectedExtension = MIME_TYPE_MAP.get(mimeType.toLowerCase());
if (expectedExtension != null &&
!expectedExtension.equalsIgnoreCase(extension)) {
return false;
}
// 4. 魔数验证(实际文件类型)
try {
byte[] header = new byte[512];
InputStream inputStream = file.getInputStream();
int bytesRead = inputStream.read(header);
if (!validateMagicNumber(header, bytesRead, extension)) {
return false;
}
} catch (IOException e) {
return false;
}
return true;
}
private String getExtension(String filename) {
int dotIndex = filename.lastIndexOf('.');
return (dotIndex == -1) ? "" : filename.substring(dotIndex + 1);
}
private boolean validateMagicNumber(byte[] header, int length,
String extension) {
// 实现魔数验证逻辑
// 例如:PNG文件头应该是 89 50 4E 47
// PDF文件头应该是 25 50 44 46
if (length < 4) return false;
switch (extension.toLowerCase()) {
case "png":
return header[0] == (byte) 0x89 &&
header[1] == 0x50 &&
header[2] == 0x4E &&
header[3] == 0x47;
case "jpg":
case "jpeg":
return header[0] == (byte) 0xFF &&
header[1] == (byte) 0xD8;
case "gif":
return header[0] == 0x47 &&
header[1] == 0x49 &&
header[2] == 0x46;
case "pdf":
return header[0] == 0x25 &&
header[1] == 0x50 &&
header[2] == 0x44 &&
header[3] == 0x46;
default:
return true; // 对于其他类型,暂时信任扩展名
}
}
}
}
4. 在非Spring环境中使用原生Feign API的完整指南
4.1 原生Feign基础架构
4.1.1 Feign核心组件
java
// Feign的核心构建器
public class NativeFeignClient {
// 1. 基础构建
public static <T> T createBasicClient(Class<T> apiType, String baseUrl) {
return Feign.builder()
.encoder(new GsonEncoder()) // JSON编码器
.decoder(new GsonDecoder()) // JSON解码器
.logger(new Slf4jLogger()) // 日志
.logLevel(Logger.Level.BASIC)
.target(apiType, baseUrl); // 目标接口和URL
}
// 2. 带HTTP客户端的构建
public static <T> T createWithHttpClient(Class<T> apiType, String baseUrl) {
// 使用Apache HttpClient
HttpClient httpClient = HttpClientBuilder.create()
.setMaxConnTotal(200)
.setMaxConnPerRoute(20)
.setDefaultRequestConfig(RequestConfig.custom()
.setConnectTimeout(5000)
.setSocketTimeout(10000)
.build())
.build();
return Feign.builder()
.client(new ApacheHttpClient(httpClient))
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.options(new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true))
.target(apiType, baseUrl);
}
// 3. 使用OkHttp
public static <T> T createWithOkHttp(Class<T> apiType, String baseUrl) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES))
.addInterceptor(new LoggingInterceptor())
.build();
return Feign.builder()
.client(new OkHttpClient(okHttpClient))
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.retryer(new Retryer.Default(100, 1000, 3))
.target(apiType, baseUrl);
}
// 4. 带断路器
public static <T> T createWithCircuitBreaker(Class<T> apiType, String baseUrl) {
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("api-client");
return Feign.builder()
.client(new Resilience4JFeignClient(
circuitBreaker,
new DefaultFeignClient()
))
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(apiType, baseUrl);
}
}
4.1.2 Maven依赖配置
xml
<!-- 原生Feign核心依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>11.10</version>
</dependency>
<!-- HTTP客户端(选择其中一个) -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>11.10</version>
</dependency>
<!-- 或 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>11.10</version>
</dependency>
<!-- 编码器/解码器 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-gson</artifactId>
<version>11.10</version>
</dependency>
<!-- 或 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jackson</artifactId>
<version>11.10</version>
</dependency>
<!-- 其他功能 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-slf4j</artifactId>
<version>11.10</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hystrix</artifactId>
<version>11.10</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
<version>11.10</version>
</dependency>
4.2 完整原生Feign客户端实现
4.2.1 API接口定义
java
// 基础API接口定义
public interface UserServiceApi {
@RequestLine("GET /api/users/{id}")
@Headers({
"Accept: application/json",
"X-Request-ID: {requestId}"
})
User getUserById(@Param("id") Long id, @Param("requestId") String requestId);
@RequestLine("POST /api/users")
@Headers("Content-Type: application/json")
@Body("{user}")
User createUser(@Param("user") User user);
@RequestLine("GET /api/users")
@Headers("Accept: application/json")
List<User> getUsers(
@Param("page") Integer page,
@Param("size") Integer size,
@Param("sort") String sort
);
@RequestLine("PUT /api/users/{id}")
@Headers("Content-Type: application/json")
@Body("{user}")
User updateUser(@Param("id") Long id, @Param("user") User user);
@RequestLine("DELETE /api/users/{id}")
@Headers("Accept: application/json")
void deleteUser(@Param("id") Long id);
// 文件上传接口
@RequestLine("POST /api/users/{id}/avatar")
@Headers({
"Content-Type: multipart/form-data",
"X-User-ID: {userId}"
})
UploadResult uploadAvatar(
@Param("id") Long userId,
@Param("avatar") File avatar,
@Param("description") String description
);
// 流式响应
@RequestLine("GET /api/users/export")
@Headers("Accept: text/csv")
Response exportUsers(@Param("format") String format);
// 自定义请求头
@RequestLine("GET /api/users/profile")
@Headers({
"Authorization: Bearer {token}",
"X-Client-Version: {version}",
"X-Client-Platform: {platform}"
})
UserProfile getProfile(
@Param("token") String token,
@Param("version") String version,
@Param("platform") String platform
);
}
// 数据模型
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String username;
private String email;
private String phone;
private Instant createdAt;
private Instant updatedAt;
private UserStatus status;
public enum UserStatus {
ACTIVE, INACTIVE, SUSPENDED
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UploadResult {
private boolean success;
private String fileId;
private String url;
private Long size;
private Instant uploadedAt;
}
4.2.2 高级配置实现
java
public class NativeFeignClientFactory {
private final ObjectMapper objectMapper;
private final MetricRegistry metricRegistry;
private final Cache<String, Object> responseCache;
public NativeFeignClientFactory(ObjectMapper objectMapper,
MetricRegistry metricRegistry) {
this.objectMapper = objectMapper;
this.metricRegistry = metricRegistry;
this.responseCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.build();
}
public <T> T createClient(Class<T> apiType, String baseUrl,
ClientConfig config) {
Feign.Builder builder = Feign.builder();
// 1. 配置HTTP客户端
builder.client(createHttpClient(config));
// 2. 配置编码器/解码器
builder.encoder(createEncoder(config));
builder.decoder(createDecoder(config));
// 3. 配置重试器
builder.retryer(createRetryer(config));
// 4. 配置超时
builder.options(createOptions(config));
// 5. 配置拦截器
config.getInterceptors().forEach(builder::requestInterceptor);
// 6. 配置错误解码器
builder.errorDecoder(createErrorDecoder(config));
// 7. 配置日志
builder.logger(createLogger(config));
builder.logLevel(config.getLogLevel());
// 8. 配置契约(自定义注解处理)
builder.contract(createContract(config));
// 9. 配置指标收集
if (config.isMetricsEnabled()) {
builder.client(new MeteredClient(
builder.client(),
metricRegistry,
apiType.getSimpleName()
));
}
// 10. 配置缓存
if (config.isCacheEnabled()) {
builder.client(new CachingClient(
builder.client(),
responseCache,
config.getCacheConfig()
));
}
// 11. 配置断路器
if (config.isCircuitBreakerEnabled()) {
builder.client(new CircuitBreakerClient(
builder.client(),
config.getCircuitBreakerConfig()
));
}
return builder.target(apiType, baseUrl);
}
private Client createHttpClient(ClientConfig config) {
switch (config.getHttpClientType()) {
case APACHE:
return createApacheHttpClient(config);
case OKHTTP:
return createOkHttpClient(config);
case JDK:
return new Client.Default(null, null);
default:
throw new IllegalArgumentException("Unsupported HTTP client type");
}
}
private Client createApacheHttpClient(ClientConfig config) {
try {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
// 连接池配置
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(config.getMaxConnections());
connectionManager.setDefaultMaxPerRoute(config.getMaxConnectionsPerRoute());
httpClientBuilder.setConnectionManager(connectionManager);
// 请求配置
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(config.getConnectTimeout())
.setSocketTimeout(config.getSocketTimeout())
.setConnectionRequestTimeout(config.getConnectionRequestTimeout())
.build();
httpClientBuilder.setDefaultRequestConfig(requestConfig);
// 重试策略
httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(
config.getRetryCount(), true
));
// 禁用SSL验证(仅测试环境)
if (config.isDisableSslValidation()) {
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial((chain, authType) -> true)
.build();
httpClientBuilder.setSSLContext(sslContext);
httpClientBuilder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
}
return new ApacheHttpClient(httpClientBuilder.build());
} catch (Exception e) {
throw new RuntimeException("Failed to create Apache HTTP client", e);
}
}
private Encoder createEncoder(ClientConfig config) {
switch (config.getEncoderType()) {
case JACKSON:
return new JacksonEncoder(objectMapper);
case GSON:
return new GsonEncoder();
case JSON:
return new JsonEncoder();
case STRING:
return new Encoder.Default();
default:
throw new IllegalArgumentException("Unsupported encoder type");
}
}
private Decoder createDecoder(ClientConfig config) {
switch (config.getDecoderType()) {
case JACKSON:
return new JacksonDecoder(objectMapper);
case GSON:
return new GsonDecoder();
case JSON:
return new JsonDecoder();
case STRING:
return new Decoder.Default();
default:
throw new IllegalArgumentException("Unsupported decoder type");
}
}
// 配置类
@Data
@Builder
public static class ClientConfig {
private HttpClientType httpClientType;
private EncoderType encoderType;
private DecoderType decoderType;
private LogLevel logLevel;
// HTTP配置
private int connectTimeout;
private int socketTimeout;
private int connectionRequestTimeout;
private int maxConnections;
private int maxConnectionsPerRoute;
// 重试配置
private int retryCount;
private long retryInterval;
private long maxRetryInterval;
private double backoffMultiplier;
// 高级功能
private boolean metricsEnabled;
private boolean cacheEnabled;
private boolean circuitBreakerEnabled;
private boolean disableSslValidation;
private List<RequestInterceptor> interceptors;
private CacheConfig cacheConfig;
private CircuitBreakerConfig circuitBreakerConfig;
public enum HttpClientType {
APACHE, OKHTTP, JDK
}
public enum EncoderType {
JACKSON, GSON, JSON, STRING
}
public enum DecoderType {
JACKSON, GSON, JSON, STRING
}
}
}
// 自定义拦截器示例
public class CustomRequestInterceptor implements RequestInterceptor {
private final String apiKey;
private final String clientId;
public CustomRequestInterceptor(String apiKey, String clientId) {
this.apiKey = apiKey;
this.clientId = clientId;
}
@Override
public void apply(RequestTemplate template) {
// 添加认证头
template.header("X-API-Key", apiKey);
template.header("X-Client-ID", clientId);
// 添加请求ID
template.header("X-Request-ID", UUID.randomUUID().toString());
// 添加时间戳
template.header("X-Timestamp", Instant.now().toString());
// 记录请求日志
logRequest(template);
// 修改请求体(如添加签名)
if (template.body() != null) {
String signature = calculateSignature(template.body());
template.header("X-Signature", signature);
}
}
private void logRequest(RequestTemplate template) {
System.out.printf("[Feign Request] %s %s%n",
template.method(), template.url());
}
private String calculateSignature(byte[] body) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(body);
return Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Failed to calculate signature", e);
}
}
}
// 自定义错误解码器
public class CustomErrorDecoder implements ErrorDecoder {
private final ObjectMapper objectMapper;
private final ErrorDecoder defaultDecoder = new Default();
public CustomErrorDecoder(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public Exception decode(String methodKey, Response response) {
try {
// 尝试解析错误响应体
if (response.body() != null) {
String body = Util.toString(response.body().asReader(StandardCharsets.UTF_8));
// 尝试解析为已知错误结构
try {
ApiError apiError = objectMapper.readValue(body, ApiError.class);
return new ApiException(
apiError.getMessage(),
apiError.getCode(),
response.status(),
apiError.getDetails()
);
} catch (JsonProcessingException e) {
// 如果不是JSON格式,返回原始错误
}
}
// 根据状态码返回特定异常
switch (response.status()) {
case 400:
return new BadRequestException("Bad request");
case 401:
return new UnauthorizedException("Unauthorized");
case 403:
return new ForbiddenException("Forbidden");
case 404:
return new NotFoundException("Not found");
case 429:
return new RateLimitException("Rate limit exceeded");
case 500:
return new ServerException("Server error");
case 502:
case 503:
case 504:
return new ServiceUnavailableException("Service unavailable");
default:
return defaultDecoder.decode(methodKey, response);
}
} catch (IOException e) {
return new FeignException("Error decoding response", e);
}
}
}
// 自定义异常类
public class ApiException extends RuntimeException {
private final String errorCode;
private final int statusCode;
private final Map<String, Object> details;
public ApiException(String message, String errorCode,
int statusCode, Map<String, Object> details) {
super(message);
this.errorCode = errorCode;
this.statusCode = statusCode;
this.details = details != null ? details : new HashMap<>();
}
}
4.2.3 完整使用示例
java
public class NativeFeignExample {
public static void main(String[] args) {
// 1. 创建对象映射器
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 2. 创建度量注册表
MetricRegistry metricRegistry = new MetricRegistry();
// 3. 创建工厂
NativeFeignClientFactory factory = new NativeFeignClientFactory(
objectMapper, metricRegistry
);
// 4. 配置客户端
ClientConfig config = ClientConfig.builder()
.httpClientType(ClientConfig.HttpClientType.APACHE)
.encoderType(ClientConfig.EncoderType.JACKSON)
.decoderType(ClientConfig.DecoderType.JACKSON)
.logLevel(Logger.Level.FULL)
.connectTimeout(5000)
.socketTimeout(10000)
.maxConnections(100)
.maxConnectionsPerRoute(20)
.retryCount(3)
.metricsEnabled(true)
.cacheEnabled(true)
.circuitBreakerEnabled(true)
.interceptors(Arrays.asList(
new CustomRequestInterceptor("your-api-key", "your-client-id"),
new LoggingInterceptor()
))
.build();
// 5. 创建客户端
UserServiceApi userService = factory.createClient(
UserServiceApi.class,
"https://api.example.com",
config
);
// 6. 使用客户端
try {
// 获取用户
User user = userService.getUserById(123L, "req-123");
System.out.println("User: " + user.getUsername());
// 创建用户
User newUser = new User();
newUser.setUsername("newuser");
newUser.setEmail("newuser@example.com");
User createdUser = userService.createUser(newUser);
System.out.println("Created user ID: " + createdUser.getId());
// 批量获取用户
List<User> users = userService.getUsers(0, 20, "createdAt,desc");
System.out.println("Total users: " + users.size());
// 文件上传
File avatar = new File("avatar.jpg");
UploadResult uploadResult = userService.uploadAvatar(
createdUser.getId(), avatar, "Profile avatar"
);
System.out.println("File uploaded: " + uploadResult.getUrl());
// 导出用户
Response exportResponse = userService.exportUsers("csv");
try (InputStream is = exportResponse.body().asInputStream()) {
// 处理流式响应
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
// 处理数据
}
}
} catch (ApiException e) {
System.err.println("API Error: " + e.getErrorCode() + " - " + e.getMessage());
e.getDetails().forEach((key, value) ->
System.err.println(key + ": " + value));
} catch (Exception e) {
System.err.println("Unexpected error: " + e.getMessage());
e.printStackTrace();
}
// 7. 监控指标
reportMetrics(metricRegistry);
}
private static void reportMetrics(MetricRegistry metricRegistry) {
// 输出请求计数
Counter requestCounter = metricRegistry.counter("feign.requests.total");
System.out.println("Total requests: " + requestCounter.getCount());
// 输出响应时间直方图
Histogram responseTimeHistogram = metricRegistry.histogram("feign.response.time");
Snapshot snapshot = responseTimeHistogram.getSnapshot();
System.out.printf("Response time - Avg: %.2f, 95th: %.2f%n",
snapshot.getMean(),
snapshot.get95thPercentile());
}
}
// 异步客户端实现
public class AsyncFeignClient<T> {
private final T syncClient;
private final ExecutorService executorService;
public AsyncFeignClient(Class<T> apiType, String baseUrl, ClientConfig config) {
NativeFeignClientFactory factory = new NativeFeignClientFactory(
new ObjectMapper(), new MetricRegistry()
);
this.syncClient = factory.createClient(apiType, baseUrl, config);
this.executorService = Executors.newFixedThreadPool(10);
}
public CompletableFuture<User> getUserAsync(Long id, String requestId) {
return CompletableFuture.supplyAsync(() -> {
UserServiceApi api = (UserServiceApi) syncClient;
return api.getUserById(id, requestId);
}, executorService);
}
public CompletableFuture<List<User>> getUsersAsync(Integer page, Integer size, String sort) {
return CompletableFuture.supplyAsync(() -> {
UserServiceApi api = (UserServiceApi) syncClient;
return api.getUsers(page, size, sort);
}, executorService);
}
public CompletableFuture<User> createUserAsync(User user) {
return CompletableFuture.supplyAsync(() -> {
UserServiceApi api = (UserServiceApi) syncClient;
return api.createUser(user);
}, executorService);
}
// 批量异步操作
public CompletableFuture<List<User>> batchCreateUsersAsync(List<User> users) {
List<CompletableFuture<User>> futures = users.stream()
.map(this::createUserAsync)
.collect(Collectors.toList());
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList()));
}
// 带超时的异步操作
public CompletableFuture<User> getUserWithTimeout(Long id, String requestId,
long timeout, TimeUnit unit) {
CompletableFuture<User> future = getUserAsync(id, requestId);
return future.orTimeout(timeout, unit)
.exceptionally(ex -> {
if (ex instanceof TimeoutException) {
throw new RuntimeException("Request timeout", ex);
}
throw new RuntimeException("Request failed", ex);
});
}
public void shutdown() {
executorService.shutdown();
try {
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
4.3 原生Feign高级特性
4.3.1 自定义编解码器
java
// 自定义Protobuf编码器
public class ProtobufEncoder implements Encoder {
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) {
if (object instanceof Message) {
Message message = (Message) object;
template.body(message.toByteArray(), StandardCharsets.UTF_8);
template.header("Content-Type", "application/x-protobuf");
} else {
throw new EncodeException("Object must be a Protocol Buffers Message");
}
}
}
// 自定义Protobuf解码器
public class ProtobufDecoder implements Decoder {
private final ExtensionRegistry extensionRegistry;
public ProtobufDecoder() {
this.extensionRegistry = ExtensionRegistry.newInstance();
}
@Override
public Object decode(Response response, Type type) throws IOException {
if (response.body() == null) {
return null;
}
if (type instanceof Class && Message.class.isAssignableFrom((Class<?>) type)) {
@SuppressWarnings("unchecked")
Class<Message> messageClass = (Class<Message>) type;
Message.Builder builder = getBuilder(messageClass);
builder.mergeFrom(response.body().asInputStream(), extensionRegistry);
return builder.build();
}
throw new DecodeException(response.status(),
"Type is not a Protocol Buffers Message", response.request());
}
private Message.Builder getBuilder(Class<Message> messageClass) {
try {
Method method = messageClass.getMethod("newBuilder");
return (Message.Builder) method.invoke(null);
} catch (Exception e) {
throw new RuntimeException("Failed to create builder", e);
}
}
}
// 自定义XML编码器
public class JacksonXmlEncoder implements Encoder {
private final XmlMapper xmlMapper;
public JacksonXmlEncoder() {
this.xmlMapper = new XmlMapper();
this.xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) {
try {
String xml = xmlMapper.writeValueAsString(object);
template.body(xml, StandardCharsets.UTF_8);
template.header("Content-Type", "application/xml");
} catch (JsonProcessingException e) {
throw new EncodeException("Failed to encode object as XML", e);
}
}
}
// 多部分表单编码器
public class MultipartFormEncoder implements Encoder {
private final Encoder delegate;
private final String boundary;
public MultipartFormEncoder() {
this.delegate = new feign.form.FormEncoder();
this.boundary = UUID.randomUUID().toString();
}
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) {
if (object instanceof MultipartFormData) {
MultipartFormData formData = (MultipartFormData) object;
encodeMultipart(formData, template);
} else {
delegate.encode(object, bodyType, template);
}
}
private void encodeMultipart(MultipartFormData formData, RequestTemplate template) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
for (MultipartFormData.Part part : formData.getParts()) {
writer.append("--").append(boundary).append("\r\n");
if (part instanceof MultipartFormData.FilePart) {
MultipartFormData.FilePart filePart = (MultipartFormData.FilePart) part;
writer.append("Content-Disposition: form-data; name=\"")
.append(filePart.getName())
.append("\"; filename=\"")
.append(filePart.getFilename())
.append("\"\r\n");
if (filePart.getContentType() != null) {
writer.append("Content-Type: ")
.append(filePart.getContentType())
.append("\r\n");
}
writer.append("\r\n");
writer.flush();
try {
if (filePart.getData() instanceof byte[]) {
outputStream.write((byte[]) filePart.getData());
} else if (filePart.getData() instanceof InputStream) {
InputStream is = (InputStream) filePart.getData();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
} catch (IOException e) {
throw new EncodeException("Failed to write file data", e);
}
writer.append("\r\n");
} else {
writer.append("Content-Disposition: form-data; name=\"")
.append(part.getName())
.append("\"\r\n\r\n")
.append(part.getValue())
.append("\r\n");
}
}
writer.append("--").append(boundary).append("--\r\n");
writer.flush();
template.body(outputStream.toByteArray(), StandardCharsets.UTF_8);
template.header("Content-Type", "multipart/form-data; boundary=" + boundary);
}
}
// 自定义压缩解码器
public class CompressedResponseDecoder implements Decoder {
private final Decoder delegate;
public CompressedResponseDecoder(Decoder delegate) {
this.delegate = delegate;
}
@Override
public Object decode(Response response, Type type) throws IOException {
Response decompressedResponse = decompressResponse(response);
return delegate.decode(decompressedResponse, type);
}
private Response decompressResponse(Response response) {
String contentEncoding = response.headers().get("Content-Encoding");
if (contentEncoding != null && contentEncoding.contains("gzip")) {
try {
byte[] decompressed = decompressGzip(response.body().asInputStream());
return response.toBuilder()
.body(decompressed)
.removeHeader("Content-Encoding")
.build();
} catch (IOException e) {
throw new DecodeException(response.status(),
"Failed to decompress gzip response", response.request(), e);
}
}
return response;
}
private byte[] decompressGzip(InputStream inputStream) throws IOException {
try (GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = gzipInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
return outputStream.toByteArray();
}
}
}
4.3.2 高级拦截器和过滤器
java
// 认证拦截器
public class OAuth2Interceptor implements RequestInterceptor {
private final TokenProvider tokenProvider;
private final TokenCache tokenCache;
public OAuth2Interceptor(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
this.tokenCache = new TokenCache();
}
@Override
public void apply(RequestTemplate template) {
String accessToken = getAccessToken();
template.header("Authorization", "Bearer " + accessToken);
}
private String getAccessToken() {
// 从缓存获取
String cachedToken = tokenCache.getToken();
if (cachedToken != null && !isTokenExpired(cachedToken)) {
return cachedToken;
}
// 获取新token
TokenResponse response = tokenProvider.getToken();
tokenCache.cacheToken(response.getAccessToken(),
response.getExpiresIn());
return response.getAccessToken();
}
private boolean isTokenExpired(String token) {
// 解析token判断是否过期
return false; // 简化实现
}
}
// 日志拦截器
public class FullLoggingInterceptor implements RequestInterceptor {
private final Logger logger;
private final ObjectMapper objectMapper;
public FullLoggingInterceptor(Logger logger) {
this.logger = logger;
this.objectMapper = new ObjectMapper();
this.objectMapper.setVisibility(
PropertyAccessor.FIELD,
JsonAutoDetect.Visibility.ANY
);
}
@Override
public void apply(RequestTemplate template) {
logRequest(template);
}
private void logRequest(RequestTemplate template) {
try {
Map<String, Object> logData = new HashMap<>();
logData.put("timestamp", Instant.now());
logData.put("method", template.method());
logData.put("url", template.url());
logData.put("headers", template.headers());
if (template.body() != null) {
// 尝试解析请求体(可能不是JSON)
try {
String body = new String(template.body(), StandardCharsets.UTF_8);
if (isJson(body)) {
logData.put("body", objectMapper.readValue(body, Object.class));
} else {
logData.put("body", "[binary or non-JSON data]");
}
} catch (Exception e) {
logData.put("body", "[unparseable data]");
}
}
logger.info("Feign Request: {}", objectMapper.writeValueAsString(logData));
} catch (JsonProcessingException e) {
logger.error("Failed to log request", e);
}
}
private boolean isJson(String str) {
try {
objectMapper.readTree(str);
return true;
} catch (JsonProcessingException e) {
return false;
}
}
}
// 重试拦截器
public class SmartRetryInterceptor implements RequestInterceptor, Retryer {
private final Map<String, Integer> retryCounts = new ConcurrentHashMap<>();
private final long maxRetryInterval = 30000; // 30秒
private final double backoffMultiplier = 1.5;
@Override
public void apply(RequestTemplate template) {
// 为每个请求生成唯一ID用于重试跟踪
String requestId = UUID.randomUUID().toString();
template.header("X-Retry-ID", requestId);
retryCounts.put(requestId, 0);
}
@Override
public void continueOrPropagate(RetryableException e) {
String requestId = extractRequestId(e);
int retryCount = retryCounts.getOrDefault(requestId, 0);
if (retryCount >= 3) {
retryCounts.remove(requestId);
throw e;
}
// 指数退避
long waitTime = (long) (100 * Math.pow(backoffMultiplier, retryCount));
waitTime = Math.min(waitTime, maxRetryInterval);
try {
Thread.sleep(waitTime);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw e;
}
retryCounts.put(requestId, retryCount + 1);
}
@Override
public Retryer clone() {
return new SmartRetryInterceptor();
}
private String extractRequestId(RetryableException e) {
// 从异常中提取请求ID
return e.getMessage(); // 简化实现
}
}
// 指标收集拦截器
public class MetricsInterceptor implements RequestInterceptor {
private final MeterRegistry meterRegistry;
private final Map<String, Timer.Sample> activeRequests = new ConcurrentHashMap<>();
public MetricsInterceptor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
public void apply(RequestTemplate template) {
String requestKey = template.method() + " " + template.url();
// 开始计时
Timer.Sample sample = Timer.start(meterRegistry);
activeRequests.put(requestKey, sample);
// 记录请求计数
Counter.builder("feign.requests")
.tag("method", template.method())
.tag("endpoint", extractEndpoint(template.url()))
.register(meterRegistry)
.increment();
}
public void recordResponse(String requestKey, int statusCode, boolean success) {
Timer.Sample sample = activeRequests.remove(requestKey);
if (sample != null) {
sample.stop(Timer.builder("feign.response.time")
.tag("status", String.valueOf(statusCode))
.tag("success", String.valueOf(success))
.register(meterRegistry));
}
// 记录响应状态
Counter.builder("feign.responses")
.tag("status", String.valueOf(statusCode))
.register(meterRegistry)
.increment();
}
private String extractEndpoint(String url) {
// 从URL提取端点路径
try {
URI uri = new URI(url);
return uri.getPath();
} catch (URISyntaxException e) {
return "unknown";
}
}
}
4.3.3 服务发现集成
java
// 基于DNS的服务发现
public class DnsServiceDiscovery implements ServiceDiscovery {
private final DnsResolver dnsResolver;
private final Map<String, List<ServiceInstance>> cache = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler;
public DnsServiceDiscovery() {
this.dnsResolver = new DnsResolver();
this.scheduler = Executors.newScheduledThreadPool(1);
// 定期刷新缓存
scheduler.scheduleAtFixedRate(this::refreshAll, 30, 30, TimeUnit.SECONDS);
}
@Override
public List<ServiceInstance> getInstances(String serviceName) {
return cache.computeIfAbsent(serviceName, this::resolveInstances);
}
private List<ServiceInstance> resolveInstances(String serviceName) {
try {
InetAddress[] addresses = InetAddress.getAllByName(serviceName);
return Arrays.stream(addresses)
.map(address -> new SimpleServiceInstance(
serviceName,
address.getHostAddress(),
80, // 默认端口
Map.of("hostname", address.getHostName())
))
.collect(Collectors.toList());
} catch (UnknownHostException e) {
throw new ServiceDiscoveryException(
"Failed to resolve service: " + serviceName, e);
}
}
private void refreshAll() {
for (String serviceName : cache.keySet()) {
try {
List<ServiceInstance> instances = resolveInstances(serviceName);
cache.put(serviceName, instances);
} catch (Exception e) {
// 记录错误但继续运行
System.err.println("Failed to refresh service: " + serviceName);
}
}
}
public void shutdown() {
scheduler.shutdown();
}
}
// 基于Consul的服务发现
public class ConsulServiceDiscovery implements ServiceDiscovery {
private final ConsulClient consulClient;
private final Map<String, ServiceCache<ConsulService>> caches = new ConcurrentHashMap<>();
public ConsulServiceDiscovery(String consulHost, int consulPort) {
this.consulClient = Consul.newClient(consulHost, consulPort);
}
@Override
public List<ServiceInstance> getInstances(String serviceName) {
ServiceCache<ConsulService> cache = caches.computeIfAbsent(
serviceName, this::createCache);
return cache.getInstances().stream()
.map(this::toServiceInstance)
.collect(Collectors.toList());
}
private ServiceCache<ConsulService> createCache(String serviceName) {
ServiceCache<ConsulService> cache = ServiceCache.newCache(
consulClient.agentClient(),
serviceName
);
cache.addListener(new ServiceCacheListener<ConsulService>() {
@Override
public void cacheChanged() {
System.out.println("Service cache updated: " + serviceName);
}
});
cache.start();
return cache;
}
private ServiceInstance toServiceInstance(ConsulService consulService) {
return new SimpleServiceInstance(
consulService.getServiceName(),
consulService.getAddress(),
consulService.getPort(),
consulService.getMetadata()
);
}
public void shutdown() {
caches.values().forEach(ServiceCache::stop);
}
}
// 负载均衡客户端
public class LoadBalancedClient implements Client {
private final Client delegate;
private final ServiceDiscovery serviceDiscovery;
private final LoadBalancer loadBalancer;
private final Map<String, List<ServiceInstance>> instanceCache = new ConcurrentHashMap<>();
public LoadBalancedClient(Client delegate, ServiceDiscovery serviceDiscovery) {
this.delegate = delegate;
this.serviceDiscovery = serviceDiscovery;
this.loadBalancer = new RoundRobinLoadBalancer();
}
@Override
public Response execute(Request request, Request.Options options) throws IOException {
// 解析服务名称
String serviceName = extractServiceName(request.url());
// 获取服务实例
List<ServiceInstance> instances = getServiceInstances(serviceName);
if (instances.isEmpty()) {
throw new IOException("No instances available for service: " + serviceName);
}
// 选择实例
ServiceInstance instance = loadBalancer.choose(instances);
// 重写URL
Request balancedRequest = rewriteRequest(request, instance);
// 执行请求
try {
return delegate.execute(balancedRequest, options);
} catch (IOException e) {
// 标记实例为不健康
loadBalancer.markFailure(instance);
throw e;
}
}
private List<ServiceInstance> getServiceInstances(String serviceName) {
return instanceCache.computeIfAbsent(serviceName,
key -> serviceDiscovery.getInstances(key));
}
private String extractServiceName(String url) {
// 从URL中提取服务名称
// 例如:http://user-service/api/users -> user-service
try {
URI uri = new URI(url);
String host = uri.getHost();
return host != null ? host : "unknown";
} catch (URISyntaxException e) {
return "unknown";
}
}
private Request rewriteRequest(Request original, ServiceInstance instance) {
try {
URI originalUri = new URI(original.url());
URI newUri = new URI(
originalUri.getScheme(),
null, // 用户信息
instance.getHost(),
instance.getPort(),
originalUri.getPath(),
originalUri.getQuery(),
originalUri.getFragment()
);
return Request.create(
original.method(),
newUri.toString(),
original.headers(),
original.body(),
original.charset(),
original.requestTemplate()
);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid URL: " + original.url(), e);
}
}
}
// 服务发现接口
public interface ServiceDiscovery {
List<ServiceInstance> getInstances(String serviceName);
}
// 服务实例接口
public interface ServiceInstance {
String getServiceName();
String getHost();
int getPort();
Map<String, String> getMetadata();
}
// 负载均衡器接口
public interface LoadBalancer {
ServiceInstance choose(List<ServiceInstance> instances);
void markSuccess(ServiceInstance instance);
void markFailure(ServiceInstance instance);
}
// 轮询负载均衡器实现
public class RoundRobinLoadBalancer implements LoadBalancer {
private final ConcurrentMap<String, AtomicInteger> counters = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Set<ServiceInstance>> healthyInstances = new ConcurrentHashMap<>();
@Override
public ServiceInstance choose(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
throw new IllegalStateException("No instances available");
}
String serviceName = instances.get(0).getServiceName();
// 获取健康实例
Set<ServiceInstance> healthy = healthyInstances.computeIfAbsent(
serviceName, k -> new ConcurrentHashSet<>());
// 初始化健康实例集合
if (healthy.isEmpty()) {
healthy.addAll(instances);
}
// 从健康实例中选择
List<ServiceInstance> available = new ArrayList<>(healthy);
if (available.isEmpty()) {
available = instances; // 如果没有健康实例,使用所有实例
}
AtomicInteger counter = counters.computeIfAbsent(
serviceName, k -> new AtomicInteger(0));
int index = counter.getAndIncrement() % available.size();
if (index < 0) {
index = 0;
counter.set(0);
}
return available.get(index);
}
@Override
public void markSuccess(ServiceInstance instance) {
String serviceName = instance.getServiceName();
Set<ServiceInstance> healthy = healthyInstances.get(serviceName);
if (healthy != null) {
healthy.add(instance);
}
}
@Override
public void markFailure(ServiceInstance instance) {
String serviceName = instance.getServiceName();
Set<ServiceInstance> healthy = healthyInstances.get(serviceName);
if (healthy != null) {
healthy.remove(instance);
}
}
}
4.4 测试和监控
4.4.1 单元测试
java
public class NativeFeignClientTest {
@Test
public void testUserServiceClient() {
// 创建模拟服务器
try (MockWebServer server = new MockWebServer()) {
// 设置模拟响应
server.enqueue(new MockResponse()
.setResponseCode(200)
.setHeader("Content-Type", "application/json")
.setBody("{\"id\":123,\"username\":\"testuser\",\"email\":\"test@example.com\"}"));
server.start();
// 创建Feign客户端
UserServiceApi client = Feign.builder()
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.target(UserServiceApi.class, server.url("/").toString());
// 执行测试
User user = client.getUserById(123L, "test-request-id");
// 验证结果
assertNotNull(user);
assertEquals(123L, user.getId());
assertEquals("testuser", user.getUsername());
assertEquals("test@example.com", user.getEmail());
// 验证请求
RecordedRequest request = server.takeRequest();
assertEquals("GET", request.getMethod());
assertEquals("/api/users/123", request.getPath());
assertEquals("test-request-id", request.getHeader("X-Request-ID"));
}
}
@Test
public void testErrorHandling() {
try (MockWebServer server = new MockWebServer()) {
// 模拟错误响应
server.enqueue(new MockResponse()
.setResponseCode(404)
.setBody("{\"error\":\"User not found\"}"));
server.start();
UserServiceApi client = Feign.builder()
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.errorDecoder(new CustomErrorDecoder(new ObjectMapper()))
.target(UserServiceApi.class, server.url("/").toString());
try {
client.getUserById(999L, "test");
fail("Expected exception");
} catch (ApiException e) {
assertEquals(404, e.getStatusCode());
assertEquals("User not found", e.getMessage());
}
}
}
@Test
public void testRetryMechanism() {
try (MockWebServer server = new MockWebServer()) {
// 第一次失败,第二次成功
server.enqueue(new MockResponse().setResponseCode(503));
server.enqueue(new MockResponse()
.setResponseCode(200)
.setBody("{\"id\":123,\"username\":\"retryuser\"}"));
server.start();
UserServiceApi client = Feign.builder()
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.retryer(new Retryer.Default(100, 1000, 3))
.target(UserServiceApi.class, server.url("/").toString());
User user = client.getUserById(123L, "retry-test");
assertNotNull(user);
assertEquals(123L, user.getId());
// 验证重试次数
assertEquals(2, server.getRequestCount());
}
}
}
// 性能测试
public class NativeFeignPerformanceTest {
@Test
public void testConcurrentRequests() throws InterruptedException {
int threadCount = 10;
int requestsPerThread = 100;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
try (MockWebServer server = new MockWebServer()) {
// 准备响应
for (int i = 0; i < threadCount * requestsPerThread; i++) {
server.enqueue(new MockResponse()
.setResponseCode(200)
.setBody("{\"id\":" + i + ",\"username\":\"user" + i + "\"}"));
}
server.start();
UserServiceApi client = Feign.builder()
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.target(UserServiceApi.class, server.url("/").toString());
CountDownLatch latch = new CountDownLatch(threadCount);
List<CompletableFuture<Void>> futures = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
for (int j = 0; j < requestsPerThread; j++) {
client.getUserById((long) j, "perf-test");
}
} finally {
latch.countDown();
}
}, executor);
futures.add(future);
}
// 等待所有任务完成
latch.await(30, TimeUnit.SECONDS);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
System.out.printf("Completed %d requests in %d ms (%d req/sec)%n",
threadCount * requestsPerThread,
duration,
(threadCount * requestsPerThread * 1000L) / duration);
// 验证所有请求都已完成
for (CompletableFuture<Void> future : futures) {
assertTrue(future.isDone());
}
assertEquals(threadCount * requestsPerThread, server.getRequestCount());
} finally {
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
}
}
}
4.4.2 集成测试
java
public class NativeFeignIntegrationTest {
private static MockWebServer mockServer;
private static UserServiceApi client;
@BeforeAll
public static void setup() {
mockServer = new MockWebServer();
client = Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.options(new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true))
.retryer(new Retryer.Default(100, 1000, 3))
.target(UserServiceApi.class, mockServer.url("/").toString());
}
@AfterAll
public static void teardown() throws IOException {
mockServer.shutdown();
}
@BeforeEach
public void reset() {
mockServer.dispatcher = new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {
return handleRequest(request);
}
};
}
private MockResponse handleRequest(RecordedRequest request) {
String path = request.getPath();
String method = request.getMethod();
if ("GET".equals(method) && path.startsWith("/api/users/")) {
String id = path.substring("/api/users/".length());
try {
Long userId = Long.parseLong(id);
User user = new User();
user.setId(userId);
user.setUsername("testuser" + userId);
user.setEmail("user" + userId + "@example.com");
user.setCreatedAt(Instant.now());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
return new MockResponse()
.setResponseCode(200)
.setHeader("Content-Type", "application/json")
.setBody(mapper.writeValueAsString(user));
} catch (Exception e) {
return new MockResponse().setResponseCode(400);
}
}
return new MockResponse().setResponseCode(404);
}
@Test
public void testGetUser() {
User user = client.getUserById(123L, "test-request");
assertNotNull(user);
assertEquals(123L, user.getId());
assertEquals("testuser123", user.getUsername());
}
@Test
public void testCreateUser() {
// 设置创建用户的响应
mockServer.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {
if ("POST".equals(request.getMethod()) && "/api/users".equals(request.getPath())) {
try {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
User createdUser = mapper.readValue(request.getBody().readUtf8(), User.class);
createdUser.setId(456L);
createdUser.setCreatedAt(Instant.now());
return new MockResponse()
.setResponseCode(201)
.setHeader("Content-Type", "application/json")
.setBody(mapper.writeValueAsString(createdUser));
} catch (Exception e) {
return new MockResponse().setResponseCode(400);
}
}
return new MockResponse().setResponseCode(404);
}
});
User newUser = new User();
newUser.setUsername("newuser");
newUser.setEmail("new@example.com");
User createdUser = client.createUser(newUser);
assertNotNull(createdUser);
assertEquals(456L, createdUser.getId());
assertEquals("newuser", createdUser.getUsername());
}
}
4.4.3 监控和诊断
java
// 健康检查
public class FeignClientHealthCheck extends HealthCheck {
private final UserServiceApi userService;
public FeignClientHealthCheck(UserServiceApi userService) {
this.userService = userService;
}
@Override
protected Result check() throws Exception {
try {
// 执行一个简单的健康检查请求
long startTime = System.nanoTime();
userService.getUserById(1L, "health-check");
long duration = System.nanoTime() - startTime;
// 转换为毫秒
double responseTimeMs = duration / 1_000_000.0;
Map<String, Object> details = new HashMap<>();
details.put("response_time_ms", responseTimeMs);
details.put("timestamp", Instant.now());
if (responseTimeMs > 1000) {
return Result.unhealthy("Response time too high: " + responseTimeMs + "ms")
.withDetails(details);
}
return Result.healthy()
.withDetails(details);
} catch (Exception e) {
return Result.unhealthy("Service unavailable: " + e.getMessage());
}
}
}
// 指标报告
public class FeignMetricsReporter {
private final MetricRegistry metricRegistry;
private final ScheduledExecutorService scheduler;
public FeignMetricsReporter(MetricRegistry metricRegistry) {
this.metricRegistry = metricRegistry;
this.scheduler = Executors.newScheduledThreadPool(1);
}
public void start() {
// 定期报告指标
scheduler.scheduleAtFixedRate(this::reportMetrics, 1, 1, TimeUnit.MINUTES);
}
private void reportMetrics() {
try {
// 收集并报告各种指标
reportRequestMetrics();
reportResponseTimeMetrics();
reportErrorMetrics();
reportConnectionPoolMetrics();
} catch (Exception e) {
System.err.println("Failed to report metrics: " + e.getMessage());
}
}
private void reportRequestMetrics() {
Counter totalRequests = metricRegistry.counter("feign.requests.total");
Counter successfulRequests = metricRegistry.counter("feign.requests.success");
Counter failedRequests = metricRegistry.counter("feign.requests.failed");
System.out.printf("Request Stats - Total: %d, Success: %d, Failed: %d%n",
totalRequests.getCount(),
successfulRequests.getCount(),
failedRequests.getCount());
}
private void reportResponseTimeMetrics() {
Histogram responseTimeHistogram = metricRegistry.histogram("feign.response.time");
Snapshot snapshot = responseTimeHistogram.getSnapshot();
System.out.printf("Response Time - Min: %.2f, Max: %.2f, Mean: %.2f, 95th: %.2f%n",
snapshot.getMin(),
snapshot.getMax(),
snapshot.getMean(),
snapshot.get95thPercentile());
}
public void stop() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
// 分布式追踪
public class TracingInterceptor implements RequestInterceptor {
private final Tracer tracer;
public TracingInterceptor(Tracer tracer) {
this.tracer = tracer;
}
@Override
public void apply(RequestTemplate template) {
Span currentSpan = tracer.currentSpan();
if (currentSpan != null) {
// 添加追踪头
String traceId = currentSpan.context().traceIdString();
String spanId = currentSpan.context().spanIdString();
template.header("X-B3-TraceId", traceId);
template.header("X-B3-SpanId", spanId);
template.header("X-B3-ParentSpanId",
currentSpan.context().parentIdString());
template.header("X-B3-Sampled",
String.valueOf(currentSpan.context().sampled()));
}
// 开始新的子span
Tracer.SpanInScope spanInScope = tracer.nextSpan()
.name("feign-" + template.method() + "-" + template.url())
.kind(Span.Kind.CLIENT)
.start()
.annotate("cs") // client send
.putTag("http.method", template.method())
.putTag("http.url", template.url())
.makeCurrent();
// 将span作用域存储在请求上下文中
template.header("X-Span-Scope", "active");
}
}
// 连接池监控
public class ConnectionPoolMonitor {
private final PoolingHttpClientConnectionManager connectionManager;
private final ScheduledExecutorService monitorService;
public ConnectionPoolMonitor(PoolingHttpClientConnectionManager connectionManager) {
this.connectionManager = connectionManager;
this.monitorService = Executors.newScheduledThreadPool(1);
}
public void startMonitoring() {
monitorService.scheduleAtFixedRate(this::monitor, 30, 30, TimeUnit.SECONDS);
}
private void monitor() {
PoolStats totalStats = connectionManager.getTotalStats();
Map<String, PoolStats> routeStats = connectionManager.getRoutes();
System.out.println("=== Connection Pool Stats ===");
System.out.printf("Total Connections: %d%n", totalStats.getAvailable());
System.out.printf("Total Leased: %d%n", totalStats.getLeased());
System.out.printf("Total Pending: %d%n", totalStats.getPending());
System.out.printf("Max Total: %d%n", totalStats.getMax());
routeStats.forEach((route, stats) -> {
System.out.printf("Route %s: Available=%d, Leased=%d, Pending=%d%n",
route, stats.getAvailable(), stats.getLeased(), stats.getPending());
});
// 检查是否有泄漏的连接
connectionManager.getRoutes().forEach((route, stats) -> {
if (stats.getLeased() > stats.getMax() * 0.8) {
System.err.printf("WARNING: High connection usage on route %s: %d/%d%n",
route, stats.getLeased(), stats.getMax());
}
});
}
public void shutdown() {
monitorService.shutdown();
}
}
总结
本文详细介绍了Spring Cloud Feign在四个方面的深入应用:
-
与Spring Cloud Contract集成:展示了如何通过契约测试确保微服务之间的API兼容性,包括生产者端和消费者端的完整配置、契约定义、测试生成和CI/CD集成。
-
负载均衡器高级配置:深入探讨了Spring Cloud LoadBalancer的各种配置选项,包括自定义负载均衡策略、区域感知、健康检查、重试机制以及与熔断器的集成。
-
Feign文件上传:提供了完整的文件上传解决方案,包括单文件上传、多文件上传、大文件分片上传、断点续传、安全验证等高级特性。
-
原生Feign API使用:展示了如何在非Spring环境中使用原生Feign API,包括客户端构建、自定义编解码器、拦截器、服务发现、负载均衡、监控和测试等完整实现。