spring-cloud / spring-cloud/spring-cloud-commons

ConditionalOnBean doesn't match RefreshScope Bean

Open
#1,191 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

waiting-for-triage
Dominant language
Java
Stars
751
Forks
744
Avg merge
1d 14h
Merged PRs (30d)
9

Description

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;

    }
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the referenced line in spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java and reproduce the behavior using the ConditionalOnRefreshScopeBeanTest example. Compare the conditional lookup for the @RefreshScope FactoryBean with the expected TokenProvider creation, then add or update regression coverage so the intended matching behavior is explicit.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring, spring-boot
Domain
backend, cloud
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.