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 声明式调用优势
- 代码简洁:只需定义接口
- 易于维护:统一管理 API
- 集成简单:与 Spring MVC 注解兼容
- 扩展方便:支持自定义配置
三、代码示例
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 使用规范
- 接口定义:统一管理 API 接口
- 配置管理:合理配置超时和重试
- 熔断降级:配置 Fallback 策略
- 日志监控:开启适当的日志级别
最佳实践
- 接口分层:单独模块管理 Feign Client
- 异常处理:统一处理 Feign 调用异常
- 性能优化:合理配置连接池和超时
- 安全传输:配置 TLS 和认证
OpenFeign 是微服务调用的核心组件,掌握其使用方式和最佳实践,能够构建出简洁、高效、可靠的微服务系统。