26-Spring Cloud 服务调用详解

3 阅读7分钟

Spring Cloud 服务调用详解

一、知识概述

在微服务架构中,服务之间的调用是核心需求。Spring Cloud 提供了多种服务调用方式,包括 RestTemplate、WebClient 和 OpenFeign。其中 OpenFeign 是最常用的声明式 HTTP 客户端,它使得编写 Web 服务客户端变得简单。

服务调用的核心需求:

  • 声明式调用:像调用本地方法一样调用远程服务
  • 负载均衡:自动分发请求到多个实例
  • 熔断降级:服务不可用时提供降级策略
  • 重试机制:失败后自动重试

理解服务调用的原理和最佳实践,是构建微服务系统的关键技能。

二、知识点详细讲解

2.1 服务调用方式对比

方式类型特点适用场景
RestTemplate同步阻塞简单直接简单场景
WebClient异步非阻塞响应式高并发
OpenFeign声明式简洁优雅推荐使用

2.2 OpenFeign 核心概念

工作原理
Feign Client 接口
       ↓
动态代理生成实现类
       ↓
解析注解(@RequestMapping 等)
       ↓
构建 HTTP 请求
       ↓
负载均衡选择实例
       ↓
发送请求
       ↓
解析响应
核心组件
  • @FeignClient:声明 Feign 客户端
  • Encoder:请求参数编码
  • Decoder:响应结果解码
  • Contract:注解契约
  • Client:HTTP 客户端

2.3 Feign 配置

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic
      user-service:
        connectTimeout: 10000
        readTimeout: 10000
  compression:
    request:
      enabled: true
      mime-types: text/xml,application/xml,application/json
    response:
      enabled: true

2.4 声明式调用优势

  1. 代码简洁:只需定义接口
  2. 易于维护:统一管理 API
  3. 集成简单:与 Spring MVC 注解兼容
  4. 扩展方便:支持自定义配置

三、代码示例

3.1 基础 Feign Client

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

@FeignClient(name = "user-service", path = "/api/users")
public interface UserClient {
    
    @GetMapping("/{id}")
    UserDTO getUserById(@PathVariable("id") Long id);
    
    @GetMapping
    List<UserDTO> getAllUsers();
    
    @PostMapping
    UserDTO createUser(@RequestBody UserDTO user);
    
    @PutMapping("/{id}")
    UserDTO updateUser(@PathVariable("id") Long id, @RequestBody UserDTO user);
    
    @DeleteMapping("/{id}")
    void deleteUser(@PathVariable("id") Long id);
}
// 启用 Feign
@SpringBootApplication
@EnableFeignClients
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
// 使用 Feign Client
@Service
public class OrderService {
    
    @Autowired
    private UserClient userClient;
    
    public OrderDTO createOrder(Long userId) {
        // 像调用本地方法一样调用远程服务
        UserDTO user = userClient.getUserById(userId);
        
        OrderDTO order = new OrderDTO();
        order.setUserId(userId);
        order.setUserName(user.getName());
        
        return order;
    }
}

3.2 Feign 配置

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

@Configuration
public class FeignConfig {
    
    // 自定义日志级别
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
    
    // 自定义重试策略
    @Bean
    public Retryer feignRetryer() {
        // 最大重试次数 3,初始间隔 100ms,最大间隔 1s
        return new Retryer.Default(100, 1000, 3);
    }
    
    // 自定义请求拦截器
    @Bean
    public RequestInterceptor authInterceptor() {
        return template -> {
            // 添加认证头
            String token = getCurrentToken();
            template.header("Authorization", "Bearer " + token);
        };
    }
    
    // 自定义超时配置
    @Bean
    public Request.Options requestOptions() {
        return new Request.Options(
            5000,  // 连接超时 5s
            10000  // 读取超时 10s
        );
    }
    
    private String getCurrentToken() {
        // 从上下文获取当前 Token
        return SecurityContextHolder.getContext()
            .getAuthentication()
            .getCredentials()
            .toString();
    }
}

3.3 指定服务配置

// 为特定服务配置
@FeignClient(
    name = "user-service",
    path = "/api/users",
    configuration = UserServiceFeignConfig.class
)
public interface UserClient {
    // ...
}

@Configuration
public class UserServiceFeignConfig {
    
    @Bean
    public Logger.Level userLoggerLevel() {
        return Logger.Level.FULL;
    }
    
    @Bean
    public Request.Options userRequestOptions() {
        return new Request.Options(3000, 5000);
    }
}

3.4 请求拦截器

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

// 认证拦截器
@Component
public class AuthRequestInterceptor implements RequestInterceptor {
    
    @Override
    public void apply(RequestTemplate template) {
        // 从请求上下文获取 Token
        ServletRequestAttributes attributes = (ServletRequestAttributes) 
            RequestContextHolder.getRequestAttributes();
        
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            String token = request.getHeader("Authorization");
            
            if (token != null) {
                template.header("Authorization", token);
            }
        }
    }
}

// 链路追踪拦截器
@Component
public class TraceRequestInterceptor implements RequestInterceptor {
    
    @Override
    public void apply(RequestTemplate template) {
        // 添加链路追踪头
        String traceId = MDC.get("traceId");
        if (traceId != null) {
            template.header("X-Trace-Id", traceId);
        }
        
        String spanId = MDC.get("spanId");
        if (spanId != null) {
            template.header("X-Span-Id", spanId);
        }
    }
}

// 日志拦截器
@Component
public class LoggingRequestInterceptor implements RequestInterceptor {
    
    private static final Logger log = LoggerFactory.getLogger(LoggingRequestInterceptor.class);
    
    @Override
    public void apply(RequestTemplate template) {
        log.info("Feign Request: {} {} headers={}", 
            template.method(), 
            template.url(),
            template.headers());
    }
}

3.5 Feign 继承支持

// 公共 API 接口
public interface UserService {
    
    @GetMapping("/api/users/{id}")
    UserDTO getUserById(@PathVariable("id") Long id);
    
    @GetMapping("/api/users")
    List<UserDTO> getAllUsers();
    
    @PostMapping("/api/users")
    UserDTO createUser(@RequestBody UserDTO user);
}

// 服务端实现
@RestController
public class UserController implements UserService {
    
    @Autowired
    private UserService userService;
    
    @Override
    public UserDTO getUserById(Long id) {
        return userService.getById(id);
    }
    
    @Override
    public List<UserDTO> getAllUsers() {
        return userService.listAll();
    }
    
    @Override
    public UserDTO createUser(UserDTO user) {
        return userService.save(user);
    }
}

// Feign 客户端继承
@FeignClient(name = "user-service")
public interface UserClient extends UserService {
    // 自动继承所有方法定义
}

3.6 Feign 日志配置

# application.yml
feign:
  client:
    config:
      default:
        loggerLevel: basic
      user-service:
        loggerLevel: full

logging:
  level:
    com.example.client.UserClient: DEBUG
// 日志级别说明
public enum Level {
    NONE,       // 无日志
    BASIC,      // 请求方法、URL、响应状态码、执行时间
    HEADERS,    // BASIC + 请求和响应头
    FULL        // HEADERS + 请求和响应体
}

3.7 Feign 熔断降级

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;

// 带 Fallback 的 Feign Client
@FeignClient(
    name = "user-service",
    path = "/api/users",
    fallback = UserClientFallback.class
)
public interface UserClient {
    
    @GetMapping("/{id}")
    UserDTO getUserById(@PathVariable("id") Long id);
    
    @GetMapping
    List<UserDTO> getAllUsers();
}

// 降级实现
@Component
public class UserClientFallback implements UserClient {
    
    @Override
    public UserDTO getUserById(Long id) {
        // 返回默认值
        UserDTO defaultUser = new UserDTO();
        defaultUser.setId(id);
        defaultUser.setName("默认用户");
        defaultUser.setStatus("unknown");
        return defaultUser;
    }
    
    @Override
    public List<UserDTO> getAllUsers() {
        // 返回空列表
        return Collections.emptyList();
    }
}

// 带 Fallback 工厂的 Feign Client(可获取异常信息)
@FeignClient(
    name = "user-service",
    path = "/api/users",
    fallbackFactory = UserClientFallbackFactory.class
)
public interface UserClient {
    // ...
}

@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
    
    @Override
    public UserClient create(Throwable cause) {
        return new UserClient() {
            @Override
            public UserDTO getUserById(Long id) {
                log.error("调用 user-service 失败: {}", cause.getMessage());
                
                // 根据异常类型返回不同结果
                if (cause instanceof FeignException.NotFound) {
                    throw new RuntimeException("用户不存在");
                }
                
                // 返回默认值
                UserDTO defaultUser = new UserDTO();
                defaultUser.setId(id);
                defaultUser.setName("降级用户");
                return defaultUser;
            }
            
            @Override
            public List<UserDTO> getAllUsers() {
                log.error("调用 user-service 失败: {}", cause.getMessage());
                return Collections.emptyList();
            }
        };
    }
}

3.8 多参数调用

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

@FeignClient(name = "user-service", path = "/api/users")
public interface UserClient {
    
    // 多个查询参数
    @GetMapping("/search")
    List<UserDTO> searchUsers(
        @RequestParam("name") String name,
        @RequestParam("status") String status,
        @RequestParam("page") int page,
        @RequestParam("size") int size
    );
    
    // 使用 Map 传递参数
    @GetMapping("/query")
    List<UserDTO> queryUsers(@RequestParam Map<String, Object> params);
    
    // 表单提交
    @PostMapping(value = "/login", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    LoginResult login(
        @RequestParam("username") String username,
        @RequestParam("password") String password
    );
}

// 使用示例
@Service
public class UserQueryService {
    
    @Autowired
    private UserClient userClient;
    
    public List<UserDTO> search(String name, String status) {
        // 方式1:直接传参
        return userClient.searchUsers(name, status, 1, 10);
    }
    
    public List<UserDTO> query(String name, Integer age, String status) {
        // 方式2:Map 传参
        Map<String, Object> params = new HashMap<>();
        params.put("name", name);
        params.put("age", age);
        params.put("status", status);
        return userClient.queryUsers(params);
    }
}

3.9 文件上传下载

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@FeignClient(name = "file-service", path = "/api/files")
public interface FileClient {
    
    // 文件上传
    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    FileResult upload(@RequestPart("file") MultipartFile file);
    
    // 多文件上传
    @PostMapping(value = "/upload-batch", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    List<FileResult> uploadBatch(@RequestPart("files") MultipartFile[] files);
    
    // 文件下载
    @GetMapping("/download/{id}")
    ResponseEntity<Resource> download(@PathVariable("id") String id);
    
    // 带参数上传
    @PostMapping(value = "/upload-with-meta", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    FileResult uploadWithMeta(
        @RequestPart("file") MultipartFile file,
        @RequestParam("userId") Long userId,
        @RequestParam("description") String description
    );
}

// 使用示例
@Service
public class FileUploadService {
    
    @Autowired
    private FileClient fileClient;
    
    public FileResult uploadFile(MultipartFile file) {
        return fileClient.upload(file);
    }
    
    public void downloadFile(String id, HttpServletResponse response) {
        ResponseEntity<Resource> entity = fileClient.download(id);
        
        try {
            Resource resource = entity.getBody();
            InputStream inputStream = resource.getInputStream();
            
            response.setContentType(entity.getHeaders().getContentType().toString());
            response.setHeader("Content-Disposition", 
                entity.getHeaders().getContentDisposition().toString());
            
            IOUtils.copy(inputStream, response.getOutputStream());
            response.flushBuffer();
        } catch (IOException e) {
            throw new RuntimeException("文件下载失败", e);
        }
    }
}

3.10 自定义编解码器

import feign.codec.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.fasterxml.jackson.databind.ObjectMapper;

@Configuration
public class FeignCodecConfig {
    
    // 自定义编码器
    @Bean
    public Encoder feignEncoder(ObjectMapper objectMapper) {
        return new CustomEncoder(objectMapper);
    }
    
    // 自定义解码器
    @Bean
    public Decoder feignDecoder(ObjectMapper objectMapper) {
        return new CustomDecoder(objectMapper);
    }
    
    // 自定义错误解码器
    @Bean
    public ErrorDecoder feignErrorDecoder() {
        return new CustomErrorDecoder();
    }
}

class CustomEncoder extends SpringEncoder {
    
    public CustomEncoder(ObjectMapper objectMapper) {
        super(new SpringEncoderDelegate(objectMapper));
    }
    
    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) {
        // 自定义编码逻辑
        if (object instanceof String) {
            template.body((String) object);
        } else {
            super.encode(object, bodyType, template);
        }
    }
}

class CustomDecoder extends SpringDecoder {
    
    public CustomDecoder(ObjectMapper objectMapper) {
        super(new SpringDecoderDelegate(objectMapper));
    }
    
    @Override
    public Object decode(Response response, Type type) throws IOException {
        // 自定义解码逻辑
        if (type == String.class) {
            return Util.toString(response.body().asReader());
        }
        return super.decode(response, type);
    }
}

class CustomErrorDecoder implements ErrorDecoder {
    
    @Override
    public Exception decode(String methodKey, Response response) {
        int status = response.status();
        
        try {
            String body = Util.toString(response.body().asReader());
            
            if (status >= 400 && status < 500) {
                return new ClientException("客户端错误: " + body);
            } else if (status >= 500) {
                return new ServerException("服务端错误: " + body);
            }
        } catch (IOException e) {
            return new DecodeException("解码错误响应失败", e);
        }
        
        return new Default().decode(methodKey, response);
    }
}

四、实战应用场景

4.1 动态 URL 调用

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

// 动态 URL(不指定服务名)
@FeignClient(name = "dynamicClient", url = "placeholder")
public interface DynamicClient {
    
    @RequestMapping(method = RequestMethod.GET, value = "/")
    String get(@RequestParam("url") String url);
}

// 使用 Feign Builder 动态创建
@Service
public class DynamicFeignService {
    
    @Autowired
    private FeignClientFactory feignClientFactory;
    
    public <T> T createClient(Class<T> clientClass, String url) {
        return Feign.builder()
            .client(new Client.Default(null, null))
            .encoder(new SpringEncoder(new SpringEncoderDelegate(new ObjectMapper())))
            .decoder(new SpringDecoder(new SpringDecoderDelegate(new ObjectMapper())))
            .target(clientClass, url);
    }
    
    public String callExternalApi(String url) {
        DynamicClient client = createClient(DynamicClient.class, url);
        return client.get(url);
    }
}

4.2 批量调用合并

import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.*;

@Service
public class BatchCallService {
    
    @Autowired
    private UserClient userClient;
    
    private final ExecutorService executor = Executors.newFixedThreadPool(10);
    
    // 并发批量调用
    @Bulkhead(name = "batchCall", type = Bulkhead.Type.THREADPOOL)
    public Map<Long, UserDTO> batchGetUsers(List<Long> userIds) {
        List<Future<Map.Entry<Long, UserDTO>>> futures = new ArrayList<>();
        
        for (Long userId : userIds) {
            futures.add(executor.submit(() -> {
                UserDTO user = userClient.getUserById(userId);
                return Map.entry(userId, user);
            }));
        }
        
        Map<Long, UserDTO> result = new HashMap<>();
        for (Future<Map.Entry<Long, UserDTO>> future : futures) {
            try {
                Map.Entry<Long, UserDTO> entry = future.get(5, TimeUnit.SECONDS);
                result.put(entry.getKey(), entry.getValue());
            } catch (Exception e) {
                // 处理异常
            }
        }
        
        return result;
    }
}

五、总结与最佳实践

Feign 使用规范

  1. 接口定义:统一管理 API 接口
  2. 配置管理:合理配置超时和重试
  3. 熔断降级:配置 Fallback 策略
  4. 日志监控:开启适当的日志级别

最佳实践

  1. 接口分层:单独模块管理 Feign Client
  2. 异常处理:统一处理 Feign 调用异常
  3. 性能优化:合理配置连接池和超时
  4. 安全传输:配置 TLS 和认证

OpenFeign 是微服务调用的核心组件,掌握其使用方式和最佳实践,能够构建出简洁、高效、可靠的微服务系统。