「程序员闯关记」@Scheduled调feign服务调不了,无状态请求

494 阅读3分钟

在实际的开发过程中,我们经常会使用定时任务去执行某些操作,比如定时拉取数据、定时发送邮件等等。而在一些特殊的场景下,我们可能会需要在定时任务中使用 Feign 去调用其他的服务,但是在实际使用过程中,会发现在定时任务中使用 Feign 调用其他服务时会出现问题,比如调用失败或者走 Hystrix 的服务熔断机制。这是为什么呢?

其实,问题的根源在于 Feign 在进行接口调用时会复制 HTTP 请求信息中的 webContext 参数,然后再发起请求。而在使用定时任务时,是没有 HTTP 请求信息的,因此 Feign 就无法获取到 webContext 参数,从而导致调用失败。

具体来说,当我们使用 postman 等工具测试调用接口时,工具会自动加入 HTTP 请求信息,包括一些请求头、请求参数等,这些信息都存放在 HttpServletRequest 等类中。而在使用 Feign 进行接口调用时,会复制这些 HTTP 请求信息,然后再发起请求,从而能够成功调用其他服务。

但是在使用定时任务时,我们并没有进行 HTTP 请求,因此也就没有 webContext 参数可供 Feign 复制。Feign 在调用其他服务时就无法获取到必要的参数,因此调用失败或走 Hystrix 的服务熔断机制。

解决方法:

既然问题的根源在于 Feign 无法获取到 webContext 参数,那么我们就需要想办法让 Feign 在定时任务中也能够获取到这些参数。具体有以下两种解决方法:

1. 使用RequestContextHolder手动传递上下文信息

在定时任务中调用 Feign 接口时,可以手动将上下文信息传递给 Feign,代码如下:

javaCopy code
//获取当前上下文信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();

//将上下文信息传递给Feign
RequestContextHolder.setRequestAttributes(attributes);

//调用Feign接口
ResultDTO result = feignService.doSomething();

这样就可以在定时任务中成功调用 Feign 接口了。但是这种方式需要在每个需要调用 Feign 接口的地方手动传递上下文信息,比较繁琐。

2. 自定义Feign拦截器传递上下文信息

另一种更优雅的方式是自定义 Feign 拦截器,在拦截器中将上下文信息传递给 Feign。首先需要实现一个 RequestInterceptor 接口,代码如下:

javaCopy code
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        //获取当前上下文信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return;
        }
        HttpServletRequest request = attributes.getRequest();

        //将上下文信息添加到Feign请求头中
        requestTemplate.header("user-agent", request.getHeader("user-agent"));
        requestTemplate.header("Authorization", request.getHeader("Authorization"));
        //其他header参数也可以按照这样的方式添加

        //将上下文信息添加到Feign请求参数中
        Map<String, Collection<String>> parameters = new HashMap<>();
        Enumeration<String> names = request.getParameterNames();
        while (names.hasMoreElements()) {
            String name = names.nextElement();
            parameters.put(name, request.getParameterValues(name));
        }
        requestTemplate.requestBodyFromParams(parameters);
    }
}

这个拦截器会在每次 Feign 调用时执行,将上下文信息添加到 Feign 请求头和请求参数中。

接下来,在使用 Feign 的地方添加拦截器即可,代码如下:

javaCopy code
@Configuration
public class FeignConfiguration {

    @Bean
    public FeignRequestInterceptor feignRequestInterceptor() {
        return new FeignRequestInterceptor();
    }

}

这里使用了 @Configuration 注解将 Feign 拦截器配置成一个 Bean,方便在 Feign 的接口中使用。具体使用方式如下:

javaCopy code
@FeignClient(name = "some-service", configuration = FeignConfiguration.class)
public interface SomeServiceFeignClient {

    @GetMapping("/some-url")
    ResultDTO someApi();

}

使用这种方式,就可以在定时任务中成功调用 Feign 接口了,而且不需要手动传递上下文信息,代码更加简洁优雅。

总之,无论采用哪种方式,解决 Feign 在定时任务中调用时取不到上下文信息的问题