一、前言

在前面的文章:

我们聊了OpenFeign的概述、为什么会使用Feign代替Ribbon、Feign和OpenFeign的区别、以及详细的OpenFeign实现声明式客户端负载均衡案例。

在一些业务场景中,微服务间相互调用需要做鉴权,以保证我们服务的安全性。即:服务A调用服务B的时候需要将服务B的一些鉴权信息传递给服务B,从而保证服务B的调用也可以通过鉴权,进而保证整个服务调用链的安全。

本文我们就讨论如果通过openfeign的拦截器RequestInterceptor实现服务调用链中上下游服务请求头数据的传递。

二、实现RequestInterceptor

通过RequestInterceptor 拦截器拦截我们的openfeign服务请求,将上游服务的请求头或者请求体中的数据封装到我们的openfeign调用的请求模板中,从而实现上游数据的传递。

1、RequestInterceptor实现类

1)RequestInterceptor实现类

package com.saint.feign.config;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.Objects;

/**
 * 自定义的Feign拦截器
 *
 * @author Saint
 */
@Slf4j
public class MyFeignRequestInterceptor implements RequestInterceptor {
    /**
     * 这里可以实现对请求的拦截,对请求添加一些额外信息之类的
     *
     * @param requestTemplate
     */
    @Override
    public void apply(RequestTemplate requestTemplate) {
        // 1. obtain request
        final ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        // 2. 兼容hystrix限流后,获取不到ServletRequestAttributes的问题(使拦截器直接失效)
        if (Objects.isNull(attributes)) {
            log.error("MyFeignRequestInterceptor is invalid!");
            return;
        }
        HttpServletRequest request = attributes.getRequest();

        // 2. obtain request headers,and put it into openFeign RequestTemplate
        Enumeration<String> headerNames = request.getHeaderNames();
        if (Objects.nonNull(headerNames)) {
            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                String value = request.getHeader(name);
                requestTemplate.header(name, value);
            }
        }

        // todo 需要传递请求参数时放开
        // 3. obtain request body, and put it into openFeign RequestTemplate
//        Enumeration<String> bodyNames = request.getParameterNames();
//        StringBuffer body = new StringBuffer();
//        if (bodyNames != null) {
//            while (bodyNames.hasMoreElements()) {
//                String name = bodyNames.nextElement();
//                String value = request.getParameter(name);
//                body.append(name).append("=").append(value).append("&");
//            }
//        }
//        if (body.length() != 0) {
//            body.deleteCharAt(body.length() - 1);
//            requestTemplate.body(body.toString());
//            log.info("openfeign interceptor body:{}", body.toString());
//        }
    }
}

2)使RequestInterceptor生效(均已验证)

使RequestInterceptor生效的方式有四种;

1> 代码方式全局生效

直接在Spring可以扫描到的路径使用@Bean方法将RequestInterceptor实现类注入到Spring容器;

package com.saint.feign.config;

import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author Saint
 */
@Configuration
public class MyConfiguration {

    @Bean
    public RequestInterceptor requestInterceptor() {
        return new MyFeignRequestInterceptor();
    }
}

2> 配置方式全局生效

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
        # 拦截器配置(和@Bean的方式二选一)
        requestInterceptors:
          - com.saint.feign.config.MyFeignRequestInterceptor

3> 代码方式针对某个服务生效

直接在@FeignClient注解中指定configuration属性为RequestInterceptor实现类

【云原生&amp;微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)-LMLPHP

4、配置方式针对某个服务生效

feign:
  client:
    config:
      SERVICE-A:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
        # 拦截器配置(和@Bean的方式二选一)
        requestInterceptors:
          - com.saint.feign.config.MyFeignRequestInterceptor

2、效果验证

1)feign-server服务改造

在文章 SpringCloud之Feign实现声明式客户端负载均衡详细案例的基础下,我们修改feign-server项目,添加一个MVC拦截器(用于获取请求头中的数据)
【云原生&amp;微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)-LMLPHP

1> MvcInterceptor

package com.saint.feign.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 自定义MVC拦截器
 *
 * @author Saint
 */
@Slf4j
public class MvcInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("token-saint");
        log.info("obtain token is : {}", token);
        return true;
    }

}

2> MvcInterceptorConfig

设置MVC拦截器会拦截哪些路径的请求,这里是所有的请求全部拦截。

package com.saint.feign.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * MVC拦截器配置
 *
 * @author Saint
 */
@Configuration
public class MvcInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MvcInterceptor())
                .addPathPatterns("/**");
    }
}

2)结果验证

1> 执行请求:
【云原生&amp;微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)-LMLPHP【云原生&amp;微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)-LMLPHP

2> feign-consumer中的日志:
【云原生&amp;微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)-LMLPHP

3> feign-server中的日志:
【云原生&amp;微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)-LMLPHP

结果显示,RequestInterceptor生效了

三、结合Hystrix限流使用时的坑(仅做记录)

此处OpenFeign依赖的SpringCloud版本是2020.X之前。

在application.yaml文件中做如下配置开启了Hystrix限流:

feign:
  hystrix:
    enabled: true
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 30000

做完上述配置后,Feign接口的熔断机制为:线程模式;

如果我们自定义了一个RequestInterceptor实现类,就会导致hystrix熔断机制失效,接口调用异常(404、null);

1、原因分析

  • 在feign调用之前,会走RequestInterceptor拦截器,拦截器中使用了ServletRequestAttributes获取请求数据;
  • 默认feign使用的是线程池模式,当开启熔断的时候,负责熔断的线程和执行Feign接口的线程不是同一个线程,ServletRequestAttributes取到的将会是空值。

2、解决方案

将hystrix熔断方式从线程模式改为信号量模式;

feign:
  hystrix:
    enabled: true
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 30000
          strategy: SEMAPHORE

3、Hystrix线程和信号量隔离区别

【云原生&amp;微服务十】SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)-LMLPHP

4、线程和信号量隔离的使用场景?

07-29 19:35