Search code examples
javaspring-cloudspring-cloud-gateway

@RefreshScope causes "object is not an instance of declaring class". Why?


This works:

@Configuration
public class RouteLocatorConfig {
    @Bean
    public RouteLocator routeLocator(RouteLocatorProvider routeLocatorProvider) {
        return routeLocatorProvider.getRouteLocator();
    }
}

but this doesn't:

@Configuration
public class RouteLocatorConfig {
    @Bean
    @RefreshScope
    public RouteLocator routeLocator(RouteLocatorProvider routeLocatorProvider) {
        return routeLocatorProvider.getRouteLocator();
    }
}

RouteLocatorProvider is a class that I wrote myself. It's annotated as a @Component and is scanned by another @Configuration called WebClientConfig. Let's ignore for a moment that it's semantically questionable

@Configuration
@ComponentScan(basePackages = "by.afinny.apigateway")
public class WebClientConfig {
    @Bean
    @LoadBalanced
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }
}
@Component
public class RouteLocatorProvider {
    private final EurekaClient eurekaClient;
    private final WebClient.Builder webClientBuilder;
    private final RouteLocatorBuilder routeLocatorBuilder;
    private final OpenAPIV3Parser apiParser;
    @Getter
    private RouteLocator routeLocator;

    public RouteLocatorProvider(EurekaClient eurekaClient, WebClient.Builder webClientBuilder,
                                RouteLocatorBuilder routeLocatorBuilder) {
        this.eurekaClient = eurekaClient;
        this.webClientBuilder = webClientBuilder;
        this.routeLocatorBuilder = routeLocatorBuilder;
        this.apiParser = new OpenAPIV3Parser();
    }

    @PostConstruct
    public void registerListener() {
        eurekaClient.registerEventListener(event -> {
            if (event instanceof CacheRefreshedEvent) { // wtf, spring
                refreshRoutings();
            }
        });
    }

    private void refreshRoutings() {
        List<Application> applications = findOtherRegisteredApplications();
        Map<Application, SwaggerParseResult> docs = mapToDocs(applications);
        routeLocator = buildRouteLocatorFrom(docs);
        // initially, I called refreshScope.refresh(RouteLocator.class) here but then removed it to isolate the cause – removing it didn't help
    }

// more logic

You may be wondering what all those methods do. But it really doesn't matter. If you implement them like this (just to make the compiler happy), the problem is exactly the same:

    private List<Application> findOtherRegisteredApplications() {
        return Collections.emptyList();
    }

    private Map<Application, SwaggerParseResult> mapToDocs(List<Application> applications) {
        return Collections.emptyMap();
    }

    private RouteLocator buildRouteLocatorFrom(Map<Application, SwaggerParseResult> appsToDocs) {
        return routeLocatorBuilder.routes().build();
    }

Annotating the RouteLocator @Bean method with @RefreshScope leads to this exception:

org.springframework.context.ApplicationContextException: Failed to start bean 'eurekaAutoServiceRegistration'
    at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:182) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:357) ~[spring-context-6.0.12.jar:6.0.12]
    at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na]
    at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:156) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:124) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:958) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:611) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66) ~[spring-boot-3.1.4.jar:3.1.4]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:737) ~[spring-boot-3.1.4.jar:3.1.4]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) ~[spring-boot-3.1.4.jar:3.1.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) ~[spring-boot-3.1.4.jar:3.1.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1309) ~[spring-boot-3.1.4.jar:3.1.4]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1298) ~[spring-boot-3.1.4.jar:3.1.4]
    at by.afinny.apigateway.ApiGatewayV2Application.main(ApiGatewayV2Application.java:9) ~[classes/:na]
Caused by: java.lang.IllegalArgumentException: object is not an instance of declaring class
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
    at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:281) ~[spring-core-6.0.12.jar:6.0.12]
    at org.springframework.cloud.context.scope.GenericScope$LockedScopedProxyFactoryBean.invoke(GenericScope.java:482) ~[spring-cloud-context-4.0.4.jar:4.0.4]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.0.12.jar:6.0.12]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:244) ~[spring-aop-6.0.12.jar:6.0.12]
    at jdk.proxy2/jdk.proxy2.$Proxy107.getRoutes(Unknown Source) ~[na:na]
    at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onNext(FluxMergeSequential.java:208) ~[reactor-core-3.5.10.jar:3.5.10]
    at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335) ~[reactor-core-3.5.10.jar:3.5.10]
    at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294) ~[reactor-core-3.5.10.jar:3.5.10]
    at reactor.core.publisher.FluxMergeSequential$MergeSequentialMain.onSubscribe(FluxMergeSequential.java:198) ~[reactor-core-3.5.10.jar:3.5.10]
    at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201) ~[reactor-core-3.5.10.jar:3.5.10]
    at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83) ~[reactor-core-3.5.10.jar:3.5.10]
    at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:62) ~[reactor-core-3.5.10.jar:3.5.10]
    at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:54) ~[reactor-core-3.5.10.jar:3.5.10]
    at reactor.core.publisher.Flux.subscribe(Flux.java:8773) ~[reactor-core-3.5.10.jar:3.5.10]
    at reactor.core.publisher.Flux.blockLast(Flux.java:2752) ~[reactor-core-3.5.10.jar:3.5.10]
    at org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.lambda$onApplicationEvent$0(WeightCalculatorWebFilter.java:140) ~[spring-cloud-gateway-server-4.0.7.jar:4.0.7]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory$DependencyObjectProvider.ifAvailable(DefaultListableBeanFactory.java:2070) ~[spring-beans-6.0.12.jar:6.0.12]
    at org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.onApplicationEvent(WeightCalculatorWebFilter.java:140) ~[spring-cloud-gateway-server-4.0.7.jar:4.0.7]
    at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:174) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:167) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:145) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:437) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:370) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.cloud.gateway.route.RouteRefreshListener.reset(RouteRefreshListener.java:73) ~[spring-cloud-gateway-server-4.0.7.jar:4.0.7]
    at org.springframework.cloud.gateway.route.RouteRefreshListener.onApplicationEvent(RouteRefreshListener.java:54) ~[spring-cloud-gateway-server-4.0.7.jar:4.0.7]
    at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:174) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:167) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:145) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:437) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:370) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.cloud.netflix.eureka.serviceregistry.EurekaAutoServiceRegistration.start(EurekaAutoServiceRegistration.java:85) ~[spring-cloud-netflix-eureka-client-4.0.3.jar:4.0.3]
    at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:179) ~[spring-context-6.0.12.jar:6.0.12]
    ... 13 common frames omitted
    Suppressed: java.lang.Exception: #block terminated with an error
        at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:103) ~[reactor-core-3.5.10.jar:3.5.10]
        at reactor.core.publisher.Flux.blockLast(Flux.java:2753) ~[reactor-core-3.5.10.jar:3.5.10]
        ... 30 common frames omitted

It must have something to do with the dependency on RouteLocatorProvider because if I rewrite the routeLocator() method like so:

    @Bean
    @RefreshScope
    public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder) {
        return routeLocatorBuilder.routes().build();
    }

the application starts up successfully (that is, it does start up)

Why is it happening and how do I fix it?


Solution

  • According to the exception log

    If the @RefreshScope annotation is not used, the custom routeLocator bean is not proxied. At this time, the RouteLocator instance returned by routeLocatorProvider.getRouteLocator() is actually null, that is, NullBean, because you initialize the routeLocator variable by listening to the CacheRefreshedEvent event, and this event It is sent by a scheduled task in DiscoveryClient (initScheduledTasks()), with a default delay of 30 seconds.

        @Bean
        @Primary
        @ConditionalOnMissingBean(name = "cachedCompositeRouteLocator")
        public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {
            return new CachingRouteLocator(new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
        }
    

    Therefore, the routeLocators obtained from the main RouteLocator actually only have a built-in RouteDefinitionRouteLocator (because NullBean will be filtered when injected), and there is no your customized routeLocator. There will be no problem when calling getRoutes() later. Of course, in this case, you can customize it. RouteLocator is equivalent to not being defined.

    If the @RefreshScope annotation is used, the custom routeLocator will be proxied (but the real bean of the proxy is still NullBean). The routeLocators actually have two beans. Finally, when getRoutes() is executed, it will be executed on the NullBean instance, thus reporting object is not an instance of declaring class error