掘金 后端 ( ) • 2024-05-02 10:04

微服务使用FeignClient加载不到配置问题(由@PostConstruct影响报错:choosing server for key null)

问题描述

  • @Service或@Component@Autowired了一个 @FeignClient,在 @PostConstruct方法中调用此 @FeignClient的方法,会报错choosing server for key null,怀疑配置的Hystrix超时timeoutInMilliseconds配置没有生效。

业务代码

  • @feignClient
@FeignClient(
    value = TestConstant.test,
    fallback =testClientFallback.class
)
public interface ItestClient {
    String API_PREFIX = "/client";
    String TEST_INFO = API_PREFIX + "/test";

    @GetMapping(TEST_INFO)
    R<List<testInfoVo>> getTesttInfo();
  • @Component或@Service
@Slf4j
@Component
public class TestEnv {

    @Autowired
    private ITestClient testClient;
    
@PostConstruct
public void testConnections() {
     R<List<testInfoVo>> testInfoResponse = testClient.getTestInfo();
    }
  • @FallBack
@Component
public class TestClientFallback implements ITestClient {

    @Override
    public R<List<TestInfoVo>> getTesttInfo() {
       return R.fail("获取数据失败");
    }
}

原因分析

使用@PostConstruct来进行Feign调用后报错:choosing server for key null问题

  • 在使用@PostConstruct注解的方法来调用Feign时会存在加载不到Hystrix配置的问题,这是因为在@Service或@Component的Bean创建后,@PostConstruct方法调用时,还没有加载Hystrix的配置,所以Hystrix使用的默认配置(默认timeoutInMilliseconds为1s)。

  • @FeignClient是通过 @EnableFeignClients注解扫描组装Bean得到的,HystrixCommand是在使用 @FeignClient时才创建,通过feign.hystrix.HystrixInvocationHandler#invoke方法

  • Feign加载: FeignClient 是懒加载的,它的实例化通常是在第一次调用时才完成。在 @PostConstruct 方法中直接使用 FeignClient 可能会导致问题,因为它可能还没有完全初始化好。

  • 错误信息:“choosing server for key null”:

    • 这个错误通常表示 Feign 在尝试找到对应的服务实例时失败了。这可能是因为服务发现组件(如Eureka/nacos)还没有完全启动或者尚未注册足够的服务实例信息。
    • 如果应用启动过程中立即在 @PostConstruct 中尝试通过 FeignClient 进行远程调用,那么服务注册和发现可能还没有完全就绪。

解决方案

1、使用 @DependsOn("configurableEnvironmentConfiguration") 注解

  • 单独控制可以加在 @Service/@Component注解上,此方法是参考的另外一个博主的文章的处理方式
@DependsOn("configurableEnvironmentConfiguration")
@Slf4j
@Service
public class RsaServiceImpl implements RsaEncService {
    ......
}
  • 全局控制加在Application启动类上
@DependsOn("configurableEnvironmentConfiguration")
@SpringBootApplication
public class DevApplication{
    ......
}
  • 处理逻辑
    • Hystrix的配置默认是通过ArchaiusAutoConfiguration中的Bean--ConfigurableEnvironmentConfiguration负责加载
    • 确保在使用 @FeignClientConfigurableEnvironmentConfiguration已经被加载
  • 存在的问题
    • 使用 @DependsOn 注解确实可以解决某些依赖顺序的问题,特别需要确保某些特定的 Bean 在其他的 Bean 之前创建时。@DependsOn 能够明确指定Bean初始化的顺序,但它通常用于依赖关系是明确的和直接的场景。
    • 涉及 FeignClient 的场景,问题可能更加复杂,尤其是当涉及到整个框架级别的自动配置(如 Feign, Hystrix, Ribbon 等)的初始化。FeignClient 的创建和配置涉及多个组件,包括服务发现、负载均衡器以及可能的断路器(Hystrix)。@DependsOn 可能不足以处理这些因为配置加载和处理的异步性或者复杂性而引发的问题。
    • 具体到使用 @DependsOn("configurableEnvironmentConfiguration") 的提议,如果这个 Bean 直接或间接地影响到 FeignClient 的配置加载,这种方法可能有帮助。然而,在大多数标准的 Spring Boot 应用中,这个特定的 Bean 并不常见,且通常不需要显式地管理环境配置的加载顺序,因为 Spring Boot 的自动配置系统已经处理了大部分情况。
    • 所以需要在项目初始化时进行feign调用的话,还可以采用下面的方式。
      • 使用应用事件:监听 ContextRefreshedEventApplicationReadyEvent 事件,这些事件表示 Spring 应用上下文已完全初始化和刷新,所有的 Bean 和配置都已就绪。

2、使用使用应用事件:监听 ContextRefreshedEventApplicationReadyEvent 事件(本次我采用的是ApplicationReadyEvent)

  • 使用应用事件来推迟对ITestClient的调用直到Spring应用完全启动完成,下面是如何修改TesttEnv类来实现这个逻辑的示例:
  • @Component或@Service
@Slf4j
@Component
public class TestEnv implements ApplicationListener<ApplicationReadyEvent> {

    @Autowired
    private ITestClient testClient;
    
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        log.info("##应用已启动完成,开始调用");
        testConnections();
    }
public void testConnections() {
     R<List<testInfoVo>> testInfoResponse = testClient.getTestInfo();
    }
  • 使用ApplicationListener<ApplicationReadyEvent> : 这个接口的onApplicationEvent方法会在Spring Boot应用完全启动并准备好接收请求时被触发。
  • 内部调用testConnections: 实际的逻辑被封装在testConnections方法中,这样可以确保仅在应用已完全启动后执行。
  • 这样修改后,应用在尝试调用远程服务前会等待直到Spring应用上下文完全准备好,从而减少启动过程中由于上下文未完全初始化完成导致的问题。

总结

  • 使用 ApplicationReadyEvent 优化服务启动与远程调用的时序问题

  • 在Spring Boot应用中,特别是集成了 @FeignClient 进行远程调用的场景下,合理控制服务启动顺序和远程调用的时机是确保应用稳定运行的关键。ApplicationReadyEvent 提供了一个在应用完全启动并且就绪后触发事件的机制,使得我们可以在此时刻进行安全的远程调用,从而避免在应用启动过程中出现上下文未完全准备好的问题。

  • 使用 ApplicationReadyEvent 作为触发点:该事件保证在Spring应用上下文完全初始化完成并且所有服务均已启动就绪后才被触发。

    1. 延迟远程调用的执行:通过监听 ApplicationReadyEvent,可以将远程调用的逻辑延迟到应用完全就绪后执行,确保所有依赖的服务和配置已经加载和初始化完毕。
    2. 优化服务启动流程:在微服务架构中,服务之间的依赖和调用顺序尤其关键,利用 ApplicationReadyEvent 可以有效避免启动过程中的循环依赖和早期调用问题。

通过上述方式,我们可以显著提高服务的启动效率和运行稳定性,同时确保远程调用在适当的时间内执行,从而避免因初始化不完全导致的服务调用失败。

参考