spring-cloud / spring-cloud-commons

Common classes used in different Spring Cloud implementations
Apache License 2.0
705 stars 699 forks source link

ConditionalOnBean doesn't match RefreshScope Bean #1191

Open p-daniil opened 1 year ago

p-daniil commented 1 year ago

Hi!

I'm trying to integrate RefreshScope in my project. The problem is that @ConditionalOnBean condition doesn't match bean, annotated with @RefreshScope. It works fine with Spring Boot 2.1.4.RELEASE, but doesn't work with version 2.6.6.

I have debugged and found, that the root cause is in this line: https://github.com/spring-projects/spring-boot/blob/d4a91004b5b04f0151e9b5df65dceb6443a35e42/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java#L186

Is it expected behaviour or it's a bug? My expectations is that refresh scope bean proxy should trigger real bean creation, because it's injected in provider and it shouldn't be blocked by @ConditionalOnBean condition.

Example I have three autoconfigurations, executing one after another. TokenProviderAutoConfiguration depends on bean, which can be created or not in TokenClientAutoConfiguration. On this bean I added @RefreshScope. After that TokenProviderAutoConfiguration didn't added to context, because of @ConditionalOnBean condition (but only in later Spring Boot version)

package com.example.demo;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class ConditionalOnRefreshScopeBeanTest {

    private static final String TOKEN = "some-token";

    private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
            .withConfiguration(AutoConfigurations.of(
                    RefreshAutoConfiguration.class,
                    TokenClientAutoConfiguration.class,
                    TokenProviderAutoConfiguration.class,
                    DummyBeanAutoConfiguration.class
            ));

    @Test
    void conditionalOnRefreshScopeBeanTest() {
        contextRunner
                .run(context -> {
                    final DummyBean dummyBean = context.getBean(DummyBean.class);
                    final String dummyToken = dummyBean.getToken();
                    Assertions.assertEquals(TOKEN, dummyToken);
                });
    }

    @Configuration
    public static class TokenClientAutoConfiguration {

        @RefreshScope
        @ConditionalOnMissingBean
        @Bean
        public TokenClientFactoryBean tokenClient() {
            // token client created via factory bean if it's matters
            return new TokenClientFactoryBean();
        }
    }

    @AutoConfigureAfter(TokenClientAutoConfiguration.class)
    @Configuration
    @ConditionalOnBean(TokenClient.class)
    public static class TokenProviderAutoConfiguration {
        @Bean
        public TokenProvider tvmTicketProvider(TokenClient tokenClient) {
            return tokenClient::getTokenFor;
        }
    }

    @AutoConfigureAfter(TokenProviderAutoConfiguration.class)
    @Configuration
    public static class DummyBeanAutoConfiguration {

        @Bean
        public DummyBean dummyBean(ObjectProvider<TokenProvider> tokenProviderObjectProvider) throws Exception {
            final TokenProvider tokenProvider = tokenProviderObjectProvider.getIfAvailable();
            if (tokenProvider != null) {
                return new DummyBean(tokenProvider.getToken(1));
            } else {
                return new DummyBean(null);
            }
        }
    }

    public static class DummyBean {
        private final String token;

        public DummyBean(String token) {
            this.token = token;
        }

        public String getToken() {
            return token;
        }
    }

    public static class TokenClientFactoryBean implements FactoryBean<TokenClient>, InitializingBean {

        @Override
        public TokenClient getObject() throws Exception {
            return new TokenClient() {
                @Override
                public String getTokenFor(int id) {
                    return TOKEN;
                }

                @Override
                public void close() {

                }
            };
        }

        @Override
        public Class<?> getObjectType() {
            return TokenClient.class;
        }

        @Override
        public void afterPropertiesSet() throws Exception {
            // some initialization steps
            System.out.println("Token client initialized");
        }
    }

    public interface TokenClient extends AutoCloseable {
        String getTokenFor(int id);

        // other methods

        @Override
        void close();
    }

    @FunctionalInterface
    public interface TokenProvider {

        String getToken(int id) throws Exception;

    }
}
p-daniil commented 1 year ago

Found, that the problem exactly in factory bean. On this line for factory bean returned type Object.class, so later on line 668 type is not matched.

sangmin7648 commented 9 months ago

I have similar but different problem where my configuration class annotated with @RefreshScope and method annotated with @Bean registers BeanDefinition with null BeanClass. ConditionalOnMissingBean checks match using beanclass so it creates another bean with type even if refreshscope bean already exist