Spring Cloud Feign 进阶详解:契约测试、负载均衡、文件上传与原生API

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在四个方面的深入应用:

  1. 与Spring Cloud Contract集成:展示了如何通过契约测试确保微服务之间的API兼容性,包括生产者端和消费者端的完整配置、契约定义、测试生成和CI/CD集成。

  2. 负载均衡器高级配置:深入探讨了Spring Cloud LoadBalancer的各种配置选项,包括自定义负载均衡策略、区域感知、健康检查、重试机制以及与熔断器的集成。

  3. Feign文件上传:提供了完整的文件上传解决方案,包括单文件上传、多文件上传、大文件分片上传、断点续传、安全验证等高级特性。

  4. 原生Feign API使用:展示了如何在非Spring环境中使用原生Feign API,包括客户端构建、自定义编解码器、拦截器、服务发现、负载均衡、监控和测试等完整实现。

相关推荐
wellc10 分钟前
SpringBoot集成Flowable
java·spring boot·后端
Hui Baby1 小时前
springAi+MCP三种
java
hsjcjh1 小时前
【MySQL】C# 连接MySQL
java
敖正炀1 小时前
LinkedBlockingDeque详解
java
wangyadong3171 小时前
datagrip 链接mysql 报错
java
untE EADO1 小时前
Tomcat的server.xml配置详解
xml·java·tomcat
ictI CABL1 小时前
Tomcat 乱码问题彻底解决
java·tomcat
敖正炀1 小时前
DelayQueue 详解
java
敖正炀2 小时前
PriorityBlockingQueue 详解
java
shark22222222 小时前
Spring 的三种注入方式?
java·数据库·spring