前言
当前已经写了两个demo系统,后面因该还有两个需要写,每个demo系统都需要调用健康系统Api。并且还需要调用第三方系统上的一些接口,并且还需记录请求和响应信息,此时使用Feigin.builder是我们最好的选择了。
什么是Feign
Feign是一个声明式的Web Service客户端,它使得编写HTTP客户端变得简单。使用Feign,只需要创建一个接口并在上面添加注解,即可完成对外部HTTP接口的调用。Feign提供了多种可配置的功能,如编码器、解码器、错误解码器、请求拦截器、重试机制等。
创建
下面我们将创建一个自定义的 Feign 客户端。
在构造函数中有注入了两个依赖:messageConverters
: 管理 HTTP 消息转换器,用于序列化和反序列化请求和响应。apiRequestRepository
: 用于存储和管理 API 请求的信息。
private final ObjectFactory<HttpMessageConverters> messageConverters;
private final ApiRequestRepository apiRequestRepository;
public FeignClientConfig(ObjectFactory<HttpMessageConverters> messageConverters, ApiRequestRepository apiRequestRepository) {
this.messageConverters = messageConverters;
this.apiRequestRepository = apiRequestRepository;
}
public <T> T createClinet(Class<T> type, String url) {
Encoder encoder = new SpringEncoder(this.messageConverters);
Decoder decoder = new FeignResponseInterceptor(new SpringDecoder(this.messageConverters), this.apiRequestRepository);
return Feign.builder()
.encoder(encoder)
.decoder(decoder)
.errorDecoder(new FeignErrorDecoderException(this.apiRequestRepository))
.requestInterceptor(new FeignRequestInterceptor(apiRequestRepository))
.retryer(Retryer.NEVER_RETRY)
.target(type, url);
}
createClient
方法用于创建 Feign 客户端实例:
核心组件
Encoder:编码器,用于对请求数据进行编码。
Decoder:解码器,用于对响应数据进行解码。
ErrorDecoder:用于处理请求过程中发生的错误。
RequestInterceptor:用于在请求发送前对请求进行拦截和处理。
Retryer:用于配置请求的重试策略。
target:指定目标服务的类型和URL。
由于我们请求和响应都需要记录下来,所以定义了拦截器
FeignRequestInterceptor(请求拦截器)
FeignRequestInterceptor类 实现 RequestInterceptor请求拦截器,从写apply方法
主要的目的是:当发起请求之前,apply方法会先执行,用来记录请求的详细信息。
public class FeignRequestInterceptor implements RequestInterceptor {
private final ApiRequestRepository apiRequestRepository;
static Long apiRequestId = null;
public FeignRequestInterceptor(ApiRequestRepository apiRequestRepository) {
this.apiRequestRepository = apiRequestRepository;
}
@Override
public void apply(RequestTemplate requestTemplate) {
ApiRequest apiRequest = new ApiRequest();
apiRequest.setRequestUrl(requestTemplate.feignTarget().url() + requestTemplate.url());
apiRequest.setRequestHeaders(requestTemplate.headers().toString());
apiRequest.setMethod(requestTemplate.method());
// 获取 Content-Type
Collection<String> contentTypes = requestTemplate.headers().get("Content-Type");
if (contentTypes == null || contentTypes.isEmpty() || contentTypes.stream().noneMatch(this::isFileContentType)) {
// 如果不是文件类型请求,保存请求体
if (requestTemplate.body() != null) {
apiRequest.setRequestBody(new String(requestTemplate.body(), StandardCharsets.UTF_8));
}
} else {
try {
saveMultipartMetadata(requestTemplate, apiRequest);
} catch (Exception e) {
throw new RuntimeException("保存附件的Metadata失败" + e.getMessage());
}
}
apiRequest.setRequestTime(new Timestamp(System.currentTimeMillis()));
apiRequestRepository.save(apiRequest);
apiRequestId = apiRequest.getId();
requestTemplate.header("ApiRequestId", apiRequest.getId().toString());
}
FeignResponseInterceptor (响应拦截器)
FeignResponseInterceptor实现了Decoder。
从请求头中提取 ApiRequestId
,根据ApiRequestId
,将响应结果进行保存。
最后调用调用 Spring Decoder 解码响应体为目标类型的对象,并返回该对象。
public class FeignResponseInterceptor implements Decoder {
private final Decoder springDecoder;
private final ApiRequestRepository apiRequestRepository;
public FeignResponseInterceptor(@Lazy Decoder decoder, ApiRequestRepository apiRequestRepository) {
this.springDecoder = decoder;
this.apiRequestRepository = apiRequestRepository;
}
public static Long getApiRequestId(Request request) {
Collection<String> apiRequestIds = request.headers().get("ApiRequestId");
if (apiRequestIds != null && !apiRequestIds.isEmpty()) {
return Long.valueOf(apiRequestIds.iterator().next());
}
return null;
}
@Override
public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
Long apiRequestId = FeignResponseInterceptor.getApiRequestId(response.request());
byte[] responseBodyBytes = new byte[0];
if (apiRequestId != null) {
ApiRequest apiRequest = this.apiRequestRepository.findById(apiRequestId).orElseThrow(EntityNotFoundException::new);
apiRequest.setStatusCode(response.status());
apiRequest.setResponseHeaders(response.headers().toString());
apiRequest.setRequestDuration(System.currentTimeMillis() - apiRequest.getRequestTime().getTime());
if (response.body() != null) {
try (InputStream responseBodyStream = response.body().asInputStream()) {
// 临时存储,获取响应体流数据只支持一次读取
responseBodyBytes = responseBodyStream.readAllBytes();
String jsonResponse = new String(responseBodyBytes, StandardCharsets.UTF_8);
apiRequest.setResponseBody(jsonResponse);
} catch (IOException e) {
throw new IOException("读取响应体失败", e);
}
}
this.apiRequestRepository.save(apiRequest);
}
InputStream cachedInputStream = new ByteArrayInputStream(responseBodyBytes);
return springDecoder.decode(Response.builder()
.status(response.status())
.headers(response.headers())
.body(cachedInputStream, responseBodyBytes.length)
.request(response.request())
.build(), type);
}
使用示例
定义一个 Feign 客户端接口:
@RequestLine("PUT /api/v1.0/trainingResource/{training_resource_id}")
@Headers({"Authorization: Bearer {access_token}"})
TrainingResourceDto.UpdateTrainingResourceResponse updateTrainingResource(
@Param("training_resource_id") Long training_resource_id,
@Param("access_token") String access_token,
TrainingResourceDto.CreateTrainingResource createTrainingResource
);
使用 Feign 客户端
在服务类中,我们可以直接注入并使用 Feign 客户端:
TrainingResourceServiceClient trainingResourceServiceClient = this.feignClientConfig.createClinet(TrainingResourceServiceClient.class, "https://api.example.com");
trainingResourceServiceClient.updateTrainingResource(trainingResource.getTrainingResourceId(),accessToken, updateTrainingResource);
updateTrainingResource 方法: 调用 Feign 客户端的 updateTrainingResource 方法,发送 HTTP 请求。
当发起请求的时候,请求和响应信息就会被保存下来。
总结
总体流程大概如下:
开始:流程开始。
创建 Feign 客户端:使用自定义配置创建 Feign 客户端。
发送请求:准备通过 Feign 客户端发送请求。
FeignRequestInterceptor.apply:请求拦截器被调用。
生成 ApiRequest:创建一个 ApiRequest 对象以记录请求详情。
保存 ApiRequest 到仓库:将 ApiRequest 对象保存到仓库中。
将 ApiRequestId 添加到请求头:将生成的 ApiRequestId 添加到请求头中。
发送 HTTP 请求:向目标 URL 发送 HTTP 请求。
接收 HTTP 响应:从服务器接收 HTTP 响应。
FeignResponseInterceptor.decode:响应拦截器被调用以处理响应。
使用 ApiRequestId 检索 ApiRequest:使用 ApiRequestId 从仓库中检索 ApiRequest。
使用响应数据更新 ApiRequest:使用响应数据(状态码、头、体)更新 ApiRequest。
保存更新后的 ApiRequest 到仓库:将更新后的 ApiRequest 保存回仓库。
解码响应:使用自定义解码器解码响应。
返回解码后的响应:将解码后的响应返回给调用者。
结束:流程结束。
希望这篇博客能对你有一定帮助。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。