OpenFeign请求头丢失问题!OpenFeign同步调用、异步调用获取不到请求头问题!

OpenFeign请求头丢失问题!OpenFeign同步调用、异步调用获取不到请求头问题!

前言:

一般SpringBoot项目中,都会有一个鉴权的拦截器或者过滤器,例如这样:

    @Bean
    public HandlerInterceptor authInterceptor() {
        return new HandlerInterceptor() {
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                String token = request.getHeader("token");
                if (!"zzz".equals(token)) {
                    response.setContentType("application/json;charset=UTF-8");
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    response.getWriter().write("error");
                    return false;
                }
                return true;
            }
        };
    }

如果请求头中没有携带正确的token,那么这个请求就会被拦截。

如果在一次HTTP请求的业务处理中,使用OpenFeign进行远程调用其它服务的接口,会发现原本HTTP请求携带的请求头token,并没有自动赋值给OpenFeign的请求头中,导致被其它服务一个鉴权拦截器所拦截,从而无法正常请求。

那么这就需要我们进行一些特定的处理,将原本HTTP请求中携带的请求头,赋值给OpenFeign的请求头中。

方法一:给Feign接口的方法增加@RequestHeader标注的参数

修改Feign接口的参数列表,增加一个参数,类型可以是Map<String, String>,也可以是org.springframework.http.HttpHeaders,并标注@RequestHeader。推荐类型使用HttpHeaders。OpenFeign会将标注了@RequestHeader的参数作为请求头,进行请求。

/**
 * Feign接口增加HttpHeaders参数,并标注@RequestHeader
 */
@FeignClient(value = "demo2", path = "/demo")
public interface RpcService0 {
    @GetMapping("/get1")
    String get1(@RequestHeader HttpHeaders headers);
}

使用这种方法,需要先获取HttpHeaders,再去调用Feign接口的方法。获取HttpHeaders有两种方法:

第一种:Controller的方法里面同样增加标注@RequestHeader的HttpHeaders参数,然后调用Feign接口时传递HttpHeaders。

    @GetMapping("/get1")
    public String get1(@RequestHeader HttpHeaders headers) {
        // 需要注意移除掉Content-Length,不然Feign远程调用会报错
        headers.remove(HttpHeaders.CONTENT_LENGTH);
        return rpcService.get1(headers);
    }

第二种:通过RequestContextHolder获取Request对象中的对象头,并构建一个HttpHeaders。

    @GetMapping("/get2")
    public String get2() {
        HttpHeaders httpHeaders = new HttpHeaders();

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            // 需要过滤掉请求头中的Content-Length,不然可能会报错
            if (HttpHeaders.CONTENT_LENGTH.equals(headerName)) {
                continue;
            }
            String header = request.getHeader(headerName);
            httpHeaders.add(headerName, header);
        }

        return rpcService.get2(httpHeaders);
    }

这种方式对代码侵入性比较大,还需要改方法的参数列表,一般不推荐使用。

方法二:实现Feign的请求拦截器RequestInterceptor,获取当前HTTP线程的请求头。

    /**
     * 获取当前线程的RequestAttributes对象,
     * 再获取到HttpServletRequest对象,
     * 将HttpServletRequest对象里的Header,
     * 赋值给Feign创建的RequestTemplate中
     */
    @Bean
    public RequestInterceptor requestInterceptor1() {
        return template -> {
            // 获取当前线程的请求属性
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes == null) {
                return;
            }

            // 获取当前请求的请求头
            HttpServletRequest request = attributes.getRequest();
            Enumeration<String> headerNames = request.getHeaderNames();
            if (headerNames == null) {
                return;
            }

            while (headerNames.hasMoreElements()) {
                String headerName = headerNames.nextElement();
                // 需要过滤掉请求头中的Content-Length,不然可能会报错
                if (HttpHeaders.CONTENT_LENGTH.equals(headerName)) {
                    continue;
                }
                String header = request.getHeader(headerName);
                template.header(headerName, header);
            }
        };
    }

优点:

简单,不需要修改业务代码。

缺点:

  • 异步调用Feign接口不好处理,有些情况无法处理。这也分两种情况:

异步阻塞调用(√)

    public String asyncBlockingRpc() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            try {
                return rpcService.get1();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        }, executorService);

        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            try {
                return rpcService.get2();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        }, executorService);

        try {
            return future1.get() + future2.get();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

这种情况,HTTP线程要等待异步线程执行完OpenFeign远程调用之后,才会返回。那么只需要在异步执行前,获取当前HTTP线程的请求属性RequestAttributes;在异步执行中,将HTTP线程的请求属性RequestAttributes,赋值给异步线程。那么异步线程去调用OpenFeign接口时,也可以通过上面定义的拦截器,获取到请求头。

异步非阻塞调用(×)

    public String asyncNonBlockingRpc(String param) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        // 异步非阻塞操作的话,HTTP线程返回响应之后会清空RequestAttributes。这之后Feign请求拦截器中,request.getHeaderNames()会报错。
        CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            rpcService.set(param);
        }, executorService);

        return "ok";
    }

这种情况像上面一样处理就会有异常结果。HTTP线程发起异步调用后,并没有阻塞等待异步线程执行完成,就直接返回了。而异步线程中实际上是和HTTP线程共用RequestAttributes的,HTTP线程返回之后,会清空掉RequestAttributes里面的属性,那么异步线程的RequestAttributes也空了,OpenFeign拦截器自然也无法获取到请求头了。最终,这个远程调用也会被鉴权拦截器所拦截。

方法三:自定义Feign的请求拦截器RequestInterceptor,定义一个请求头持有器。

先定义一个请求头持有器。这个请求头持有器实际上就是使用一个ThreadLocal<Map<String, String>>,来存储请求头。

    /**
     * 请求头持有器
     */
    public class RequestHeaderHolder {
        private static ThreadLocal<Map<String, String>> threadLocal = new ThreadLocal<>();

        public static void addHeader(String headerName, String header) {
            if (threadLocal.get() == null) {
                threadLocal.set(new HashMap<>());
            }
            threadLocal.get().put(headerName, header);
        }

        public static Map<String, String> getHeaders() {
            return threadLocal.get();
        }

        public static void setHeaders(Map<String, String> headers) {
            threadLocal.set(headers);
        }

        public static void remove() {
            threadLocal.remove();
        }
    }

那么在什么时候往RequestHeaderHolder里面存储请求头呢?这就需要定义一个拦截器了。接收到请求后,这个拦截器会获取HTTP请求头,并存储到RequestHeaderHolder。请求处理完毕后,会清空RequestHeaderHolder。

    /**
     * 请求头处理拦截器
     * 将请求头放入RequestHeaderHolder中
     *
     * @return HandlerInterceptor
     */
    @Bean
    public HandlerInterceptor requestHeaderInterceptor() {
        return new HandlerInterceptor() {
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                Enumeration<String> headerNames = request.getHeaderNames();
                while (headerNames.hasMoreElements()) {
                    String name = headerNames.nextElement();
                    String value = request.getHeader(name);
                    RequestHeaderHolder.addHeader(name, value);
                }

                return true;
            }

            @Override
            public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
                RequestHeaderHolder.remove();
            }
        };
    }

    /**
     * 注册requestHeaderInterceptor
     *
     * @return WebMvcConfigurer
     */
    @Bean
    public WebMvcConfigurer requestHeaderInterceptorConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry
                        .addInterceptor(requestHeaderInterceptor())
                        .addPathPatterns("/**")
                        .order(Ordered.HIGHEST_PRECEDENCE);
            }
        };
    }

最后定义Feign的拦截器。这个拦截器的逻辑和第二种方法相似,只不过是从RequestHeaderHolder中获取HTTP线程的请求头,并赋值给Feign的请求头。

    /**
     * Feign请求拦截器
     * 通过RequestHeaderHolder获取当前线程的请求头,
     * 赋值给Feign创建的RequestTemplate中
     *
     * @return RequestInterceptor
     */
    @Bean
    public RequestInterceptor requestInterceptor2() {
        return template -> {
            Map<String, String> headers = Config3.RequestHeaderHolder.getHeaders();
            if (headers != null) {
                headers.forEach((headerName, header) -> {
                    // 需要过滤掉请求头中的Content-Length,不然可能会报错
                    if (HttpHeaders.CONTENT_LENGTH.equals(headerName)) {
                        return;
                    }
                    template.header(headerName, header);
                });
            }
        };
    }

这样子就能传递HTTP线程的请求头了。

但是遇到异步Feign远程调用,同样也要另外做处理。

如果是通过创建子线程例如new Thread(() -> {}).start();,来进行异步Feign远程调用,那么只需要RequestHeaderHolder里的ThreadLocal替换为InheritableThreadLocal,那么子线程就会继承父线程的ThreadLocalMap的值,子线程也就可以获取到父线程的请求头,进行正确的远程调用了。

但实际中大部分场景是需要使用线程池的,线程池里的线程和HTTP线程并不是父子关系,那么使用InheritableThreadLocal也没有用了。这就需要在异步执行之前获取到当前HTTP线程的请求头,在异步执行时赋值给异步线程。但这个方法比第二个方法好的就是,HTTP线程哪怕返回了,也不会清理掉已经赋值给异步线程的请求头。

例如:

    @Override
    public String asyncBlockingRpc() {
        Map<String, String> headers = RequestHeaderHolder.getHeaders();

        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            RequestHeaderHolder.setHeaders(headers);
            try {
                return rpcService.get1();
            } finally {
                RequestHeaderHolder.remove();
            }
        }, executorService);

        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            RequestHeaderHolder.setHeaders(headers);
            try {
                return rpcService.get2();
            } finally {
                RequestHeaderHolder.remove();
            }
        }, executorService);

        try {
            return future1.get() + future2.get();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String asyncNonBlockingRpc(String param) {
        Map<String, String> headers = RequestHeaderHolder.getHeaders();
        CompletableFuture.runAsync(() -> {
            RequestHeaderHolder.setHeaders(headers);
            try {
                Thread.sleep(1000);
                rpcService.set(param);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                RequestHeaderHolder.remove();
            }
        }, executorService);

        return "ok";
    }

这种方式始终太麻烦,需要写很多无关的业务代码。我们可以借助Alibaba的TransmittableThreadLocal来对方法三进行改造,减少代码侵入性。

方法四:TransmittableThreadLocal(推荐)

首先引入依赖

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>transmittable-thread-local</artifactId>
            <version>2.14.4</version>
        </dependency>

然后修改RequestHeaderHolder,将ThreadLocal替换为TransmittableThreadLocal

    public static class RequestHeaderHolder {
        private static TransmittableThreadLocal<Map<String, String>> threadLocal = new TransmittableThreadLocal<>();
        // 省略下面代码……
    }

修改线程池:

假设原本的线程池是这样的

    private ExecutorService executorService = Executors.newCachedThreadPool();

现在可以使用TtlExecutors.getTtlExecutorService()来包装一层

    private ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newCachedThreadPool());

以后进行异步调用,就使用这个被包装过的线程池,框架会自动帮我们将HTTP线程的ThreadLocalMap的值传递给线程池中的线程,而无需像方法三那样子手动传递。

    public String asyncBlockingRpc() {

        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> rpcService.get1(), executorService);

        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> rpcService.get2(), executorService);

        try {
            return future1.get() + future2.get();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

点个赞吧~

posted @   zLatiao  阅读(138)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
点击右上角即可分享
微信分享提示