Feign动态设置header

背景需求

springmvc 可以直接通过拦截器Interceptor和过滤器filter拦截请求头header,从而获取必要的验证信息作为我们业务逻辑服务。比如权限验证,多租户的权限范围等等。

但是在springcloud中微服务的调用其实最终也是远程调用了http,那么能不能在客户端调用的时候发给服务端的header中添加自定义的信息呢,比如业务线ID或者auhtor信息等等进而和springmvc的业务逻辑部分统一避免额外的代码实现。

Feign设置header目前有两种方式

1.实现拦截器RequestInterceptor 统一处理
2.手工创建Feignclient配置拦截器
3.RequestInterceptor +ThreadLocal实现动态传参

拦截器统一处理

我们可以直接在接口的interface的公共包中加入如下配置,这样同样的调用都会经过这个拦截器处理,进而实现统一的header处理。

import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;


@Slf4j
public class HeaderInterceptor implements RequestInterceptor {

		@Value("${feign.contract-op.businessId:sys}")
    private String businessId;
    @Override
    public void apply(RequestTemplate requestTemplate) {
        log.info("requestTemplate.header get value feign.contract-op.businessId: {}= {} ",ContractRequestHeaders.BUSINESS_ID,businessId);
        if(businessId!=null && !"sys".equals(businessId)){
            requestTemplate.header("businessId", businessId);
        }
    }
}

接口类这里直接配置为configuration,这样每次调用时它都执行一次HeaderInterceptor 拦截器

@FeignClient(
        url = "${feign.contract-op.url:https://192.168.1.1:50003/contract-op-service}/billInfo",
        name = "contractOpClient",
        contextId = "billInfoClient",configuration = ContractFeignConfiguration.class
)
public interface BillInfoClient {

    @ApiOperation(value = "分页")
    @GetMapping
    PageInfo<BillInfoListResponse> page(@SpringQueryMap @Validated BillInfoListRequest searchRequest);

}

如果我们不在这个configuration 配置,而是HeaderInterceptor 加上@Configuration注解,那么它会对所有全局的feignclien都会加上这个参数处理。

手工创建

也有文章提供了Feign.builder()手工创建的方式

 @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;


    public DeviceSeriesClient deviceSeriesClient(String businessId) {
        DeviceSeriesClient deviceSeriesClient = Feign.builder().client(client)
                .encoder(new SpringEncoder(this.messageConverters))
                .decoder(new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters))))
                .contract(new SpringMvcContract())
                .requestInterceptor(template -> template.header(HEADER_KEY,   businessId))
                .target(DeviceSeriesClient.class, "https://192.168.1.1:50001/user-service/series");
        return deviceSeriesClient;
    }

这里encoder,decoder它改成了feign默认使用的是SpringEncoder,但是我这里Feign调用服务报错:Load balancer does not have available server for client:xxx
可能是我这边使用nacos作为服务发现和注册中心,获取到的url是注册中信的地址无法进行调用。

RequestInterceptor +ThreadLocal实现动态传参

由于提供方的代码是搭好版本的,只能调用方自行实现,因此在以上都不
我想到的另一个办法,
1.配置全局移除已经存在的header中的key值,防止该key多值数组
2.利用filter拦截前端请求过来的业务ID存入ThreadLocal,然后放入到对应feignclient的requestTemplate中

这样不管是前端还是feign客户端都可以统一在这个地方进行业务参数的加入,同时也支持不同业务参数根据前端需要动态的传递不同值的问题。

import feign.RequestInterceptor;
import feign.RequestTemplate;
import io.deepblueai.cloud.robotics.webapi.exception.ApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;


@Component
@Slf4j
public class RequestParameterFilter extends GenericFilterBean implements RequestInterceptor {

    static ThreadLocal<String> localVar = new ThreadLocal<>();

    @Override
    public void apply(RequestTemplate requestTemplate) {

        requestTemplate.removeHeader("businessId");
        String businessId =localVar.get();
        if(businessId!=null){
            log.info("feign RequestInterceptor businessId = {}",businessId);
            requestTemplate.header("businessId", businessId);
        }
        localVar.remove();
        log.info("feign RequestInterceptor apply end");
    }


	// 放行swagger-ui相应的请求路径
    public static final String[] ALLOW_URL = {"/**/swagger**","/**/webjars/**",
            "/**/swagger-resources/**","/**/v2/api-docs**","/**/csrf","/**/error"};

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        if (servletRequest instanceof HttpServletRequest) {

            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String uri = request.getRequestURI();

            log.info("request uri :" + uri);

            if (isAllow(uri)) {
                filterChain.doFilter(servletRequest, response);
                return;
            }

            String businessId = request.getHeader(ContractRequestHeaders.BUSINESS_ID);
            log.info("Mvc header uri={},businessId={}",request.getRequestURI(),businessId);
            if (businessId == null) {
                log.error("businessId is null");
                throw new ApiException(ExceptionEnum.business_ID_NULL_ERROR.getCode(), "businessId不能为空");
            }else {
                /*Map<String, Object> map = new HashMap<String, Object>(16);
                map.put("businessId", businessId);
                RequestParameterWrapper requestParameterWrapper = new RequestParameterWrapper(request, map);*/
                log.info("Mvc ParameterFilter uri={},businessId={}",request.getRequestURI(),request.getParameter("businessId"));
                //feign header set localval
                localVar.set(businessId);

                //filterChain.doFilter(requestParameterWrapper, response);
            }
			filterChain.doFilter(servletRequest, response);

        }
    }

    public static boolean isAllow(String url) {
        boolean flag = false;
        for (String pattern : ALLOW_URL) {
            AntPathMatcher matcher = new AntPathMatcher();
            if (matcher.match(pattern, url)) {
                flag = true;
                break;
            }
        }
        return flag;
    }


    @Override
    public  void destroy() {
        //执行两次,非feign业务也执行一次
        localVar.remove();
    }
}

因为spring的http是使用的线程池,因此使用ThreadLocal时每次使用完要清理下,避免重复使用线程会互相残留问题。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值