Affects: probably all, current test with boot 3.1.2

I am trying add some additional functionality using proxy, replaced original bean with BeanPostProcessor.

Consider following code. It finish successully.

public class DemoApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(
                TestBean.class,
                IntSupplier.class,
                StringSupplier.class,
                SupplierBeanPostProcessor.class
        );
        context.refresh();
        context.getBean(TestBean.class);
    }

    @Component
    public static class IntSupplier implements Supplier<Integer>{
        public Integer get(){
            return 42;
        }
    }

    @Component
    public static class StringSupplier implements Supplier<String>{
        public String get(){
            return "string!!!";
        }
    }

    @Component
    public static class TestBean{
        public TestBean(Supplier<Integer> supplier){

        }
    }

    @Component
    public static class SupplierBeanPostProcessor implements BeanPostProcessor{
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (!(bean instanceof Supplier<?>)) {
                return bean;
            } else {
                return (Supplier) () -> {
                    System.out.println("some cool functionality");
                    return ((Supplier<?>)bean).get();
                };
            }
        }
    }

}

But if I reorder import in following way:

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(
                IntSupplier.class,
                StringSupplier.class,
                TestBean.class,
                SupplierBeanPostProcessor.class
        );
        context.refresh();
        context.getBean(TestBean.class);
    }

Then it finished with exception:

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'java.util.function.Supplier<java.lang.Integer>' available: expected single matching bean but found 2: demoApplication.IntSupplier,demoApplication.StringSupplier

It caused because type of already instantiated singletons is recalculated again. And my proxy in bean post processor uses raw type.

Expected: Type information is gathered at scan time and not changed over time.

Comment From: snicoll

It caused because type of already instantiated singletons is recalculated again. And my proxy in bean post processor uses raw type.

You got it right. There's nothing we can do about this as your post processor is replacing the instance and we need to take that into account. Not taking it into account would for sure breaks somebody else and the current behavior is the less surprising of the two.

Comment From: Fuud

Ok.

May be someone else will need a workaround, let me share it. with use of: common-lang3 and butebuddy:

// 
import java.lang.reflect.Type;
import java.util.function.Supplier;

import org.apache.commons.lang3.reflect.TypeUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

import one.conf.IConfProperty;
import one.serialization.io.ByteConverter;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;

public class Test {
    class SupplierProxy<T> implements Supplier<T>{
        private final Supplier<T> delegate;

        public SupplierProxy(Supplier<T> delegate){
            this.delegate = delegate;
        }

        @Override
        public T get() {
            System.out.println("some cool functionality");
            return delegate.get();
        }
    }

    @Component
    public static class SupplierBeanPostProcessor implements BeanPostProcessor {
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            return bean;
        }

        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (!(bean instanceof Supplier<?>)) {
                return bean;
            } else {
                Type correctTypeParameter = TypeUtils.unrollVariables(
                        TypeUtils.getTypeArguments(bean.getClass(), ByteConverter.class),
                        ByteConverter.class.getTypeParameters()[0]
                );
                TypeDescription.Generic versionedSupplierProxyTyped = TypeDescription.Generic.Builder
                        .parameterizedType(SupplierProxy.class, correctTypeParameter)
                        .build();


                Class<?> loaded = new ByteBuddy()
                        .subclass(versionedSupplierProxyTyped) //делаем класс с такой же generic параметризацией
                        .make()
                        .load(this.getClass().getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
                        .getLoaded();

                try {
                    return loaded.getDeclaredConstructor(ByteConverter.class, IConfProperty.class).newInstance(originalBean, converterVersion);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}