5

前言

当前已经写了两个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 保存回仓库。
解码响应:使用自定义解码器解码响应。
返回解码后的响应:将解码后的响应返回给调用者。
结束:流程结束。

希望这篇博客能对你有一定帮助。


zZ_jie
386 声望8 粉丝

虚心接受问题,砥砺前行。