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);
}
}
}
}
}