如何在Feign请求之前和远程响应之后,做一些前置操作?如代替用户再次发起请求

需求

先说下需求,在发起第三方接口调用之前,需要跟除了获取token接口之外的其他接口增加 Authorization header ,由于获取AccessToken的逻辑是公共的,所以没有必要每次调用业务接口之前,都要自己手动处理,第三方返回的AccessToken是没有referenceToken的,所以必然有一次业务在调用的时候Token是过期了,如果过期,第三方那边返回的状态码是401,所以我想统一在响应之后,增加判断如果状态码是401,重新获取AccessToken接口,然后把新的AccessToken放到header当中,重新替用户发起请求,这样在开发看来,我只需要关注业务接口本身,不需要对token过期的问题进行增加逻辑。

实现逻辑

先说下成果,目前请求之前和响应之后要做的,都已经实现了。
请求之前使用:RequestInterceptor
接口响应之后:Decoder

我在发起接口调用之前有2个功能点要实现:

  1. 判断Redis中是否有指定key的缓存,如果为空,调用获取AccessToken接口,然后把获取的AccessToken放到Redis当中。
  2. 从Redis中获取缓存,然后把AccessToken放到非 /get/oauth/token 接口其它接口的 header 中。

在响应之后有1个功能需要实现:

  1. 如果状态码或者业务码是401,重新发起获取AccessToken接口,然后替换旧的Header中的内容,重新代替用户再次发起业务请求。

代码

RequestInterceptor

import com.zcckj.hikvision.starter.constants.HikVisionConstants;
import com.zcckj.hikvision.starter.constants.HikVisionRedisConstants;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * @author lwh
 * @date 2025/1/13
 * @description 统一给业务接口加上请求头
 **/
@Component
@Slf4j
public class HikVisionAccessTokenInterceptor implements RequestInterceptor {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private HikVisionAccessTokenSet hikVisionAccessTokenSet;


    /**
     * @author lwh
     * @date2025/1/13
     * @description 统一给接口增加请求头
     **/
    @Override
    public void apply(RequestTemplate template) {
        String url = template.url();
        Boolean accessTokenExistsStatus = stringRedisTemplate.hasKey(HikVisionRedisConstants.OAUTH_TOKEN_KEY);
        // 用于只给加了指定的Header才能在解码器中拦截
		 template.header("HikVisionDT", "DT");
        // 判断URL中是否包含OAuth token获取请求,或访问令牌不存在的状态
        if (url.contains(HikVisionConstants.OAUTH_TOKEN) || Boolean.FALSE.equals(accessTokenExistsStatus)) {
            hikVisionAccessTokenSet.setGlobalAccessToken();
        }
        // 如果URL中包含OAuth的请求,手动给该请求加上指定的请求参数
        if (url.contains(HikVisionConstants.OAUTH_TOKEN)) {
            // 组装海康威视AccessToken参数
            String oAuthTokenParam = hikVisionAccessTokenSet.getOAuthTokenParam();
            template.body(oAuthTokenParam);
        } else {
            // 如果URL中不包含OAuth的请求,手动给该请求加上指定的请求头
            template.header("Authorization", stringRedisTemplate.opsForValue().get(HikVisionRedisConstants.OAUTH_TOKEN_KEY));
        }

    }


}

Decoder

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.zcckj.hikvision.starter.config.enums.HikVisionErrorCodeEnum;
import com.zcckj.hikvision.starter.constants.HikVisionRedisConstants;
import com.zcckj.hikvision.starter.model.common.HikVisionResult;
import feign.*;
import feign.codec.Decoder;
import org.springframework.data.redis.core.StringRedisTemplate;

import javax.annotation.Resource;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.*;

/**
 * @author lwh
 * @date 2025/1/14
 * @description 自定义feign解码器
 **/
 @Component
public class HikVisionFeignResultDecoder implements Decoder {

    @Resource
    private ObjectMapper objectMapper;
    @Resource
    private HikVisionAccessTokenSet hikVisionAccessTokenSet;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private Client feignClient;

    /**
     * @author lwh
     * @date 2025/1/14
     * @description 解码
     **/
    @Override
    public Object decode(Response response, Type type) throws IOException, FeignException {
         Map<String, Collection<String>> headers = response.request().headers();
         // 如果拦截到没有加指定Header的Feign,则不增加特殊逻辑
        if (!headers.containsKey("HikVisionDT")) {
            Response.Body body = response.body();
            if (body == null) {
                return null;
            }
            if (String.class.equals(type)) {
                return Util.toString(body.asReader());
            }
            if (byte[].class.equals(type)) {
                return Util.toByteArray(response.body().asInputStream());
            }
            String bodyStr = Util.toString(response.body().asReader(Util.UTF_8));
            //对结果进行转换
            return json2obj(bodyStr, type);
        }
        int status = response.status();
        if (status == HikVisionErrorCodeEnum.ERROR_CODE_401.getCode()) {
            response = resetRequest(response);
        }
        if (Objects.isNull(response.body())) {
            throw new RuntimeException( "没有返回有效的数据");
        }
        String bodyStr = Util.toString(response.body().asReader(Util.UTF_8));
        //对结果进行转换
        HikVisionResult result = json2obj(bodyStr, type);
        //如果返回错误,且为内部错误,则直接抛出异常
        if (Objects.nonNull(result.getCode()) && HikVisionErrorCodeEnum.ERROR_CODE_401.getCode().equals(result.getCode())) {
            response = resetRequest(response);
            if (Objects.isNull(response.body())) {
                throw new RuntimeException( "没有返回有效的数据");
            }
            bodyStr = Util.toString(response.body().asReader(Util.UTF_8));
            //对结果进行转换
            result = json2obj(bodyStr, type);
        }
        return result;
    }

    /**
     * @author lwh
     * @date 2025/1/14
     * @description 重置请求
     **/
    private Response resetRequest(Response response) throws IOException {
        hikVisionAccessTokenSet.setGlobalAccessToken();
        // 获取原始请求信息
        Request originalRequest = response.request();

        Map<String, Collection<String>> headers = originalRequest.headers();
        LinkedHashMap<String, Collection<String>> newHeaderMap = new LinkedHashMap<>();
        newHeaderMap.put("Authorization", Collections.singletonList(stringRedisTemplate.opsForValue().get(HikVisionRedisConstants.OAUTH_TOKEN_KEY)));
        headers.forEach((key, value) -> {
            if (!key.equalsIgnoreCase("Authorization")) {
                newHeaderMap.put(key, value);
            }
        });
        Request request = Request.create(originalRequest.httpMethod(), originalRequest.url(), newHeaderMap, originalRequest.body(), originalRequest.charset());
        Request.Options options = new Request.Options(10, 60, true);
        return feignClient.execute(request, options);
    }

    /**
     * @author lwh
     * @date 2025/1/14
     * @description json转换
     **/
    private <T> T json2obj(String jsonStr, Type targetType) {
        try {
            JavaType javaType = TypeFactory.defaultInstance().constructType(targetType);
            return objectMapper.readValue(jsonStr, javaType);
        } catch (IOException e) {
            throw new IllegalArgumentException("将JSON转换为对象时发生错误:" + jsonStr, e);
        }
    }
}

FeignConfig

拦截器和解码器不需要在FeignConfig配置,只需要在对应的Feign接口的configuration配置,如果在FeignConfig配置,则所有的Feign接口都会生效,而我只想让指定的Feign接口生效,如果你想让所有的Feign接口都生效,那么可以配置在这里面。

import feign.Client;
import feign.Logger;
import feign.Retryer;
import feign.codec.Decoder;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

/**
 * @author lwh
 * @date 2024/11/12
 * @description feign配置
 **/
@Configuration
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class HikVisionFeignConfig {

    /**
     * @author lwh
     * @date 2025/1/14
     * @description 日志级别
     **/
    @Bean
    @ConditionalOnMissingBean(Logger.Level.class)
    Logger.Level feignLoggerLevel() {
        return Logger.Level.BASIC;
    }

    /**
     * @author lwh
     * @date 2025/1/14
     * @description 调用重试
     **/
    @Bean
    @ConditionalOnMissingBean(Retryer.class)
    Retryer feignRetryer() {
        return new Retryer.Default();
    }

    /**
     * @author lwh
     * @date 2025/1/14
     * @description feign客户端
     **/
    @Bean
    @ConditionalOnMissingBean({Client.class})
    public Client feignClient(OkHttpClient client) {
        return new feign.okhttp.OkHttpClient(client);
    }
    
    /**
     * @author lwh
     * @date 2025/1/14
     * @description 连接池
     **/
    @Bean
    @ConditionalOnMissingBean({ConnectionPool.class})
    public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) {
        Integer maxTotalConnections = httpClientProperties.getMaxConnections();
        Long timeToLive = httpClientProperties.getTimeToLive();
        TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
        return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
    }

    /**
     * @author lwh
     * @date 2025/1/14
     * @description feign客户端
     **/
    @Bean
    public OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) {
        Boolean followRedirects = httpClientProperties.isFollowRedirects();
        Integer connectTimeout = httpClientProperties.getConnectionTimeout();
        Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
        return httpClientFactory.createBuilder(disableSslValidation)
                .connectTimeout((long)connectTimeout, TimeUnit.SECONDS)
                .followRedirects(followRedirects)
                .connectionPool(connectionPool)
                .build();
    }

结尾

上面的代码只是示例,代码是不全的,核心在于如何在请求之前和响应之后,做一些逻辑处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术武器库

一句真诚的谢谢,胜过千言万语

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值