Spring Boot @ConditionalOnMissingBean and generic types

conditionalonmissingbean without spring boot
conditionalonmissingbean not working
how spring boot auto-configuration works
spring boot-autoconfigure
spring boot configuration
spring boot conditional component
how to ignore bean creation in spring boot
configuration spring boot

Is there any way to get both of these beans instantiated:

@Bean
@ConditionalOnMissingBean
public Container<Book> bookContainer() {
    return new Container<>(new Book());
}

@Bean
@ConditionalOnMissingBean
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}

@ConditionalOnMissingBean only takes the bean class into account, falsely making bookContainer and computerContainer the same type, thus only one of them gets registered.

I could give explicit qualifiers to each, but since this is a part of a Spring Boot Starter, it would make it annoying to use, as the user would then be forced to somehow know the exact name to override instead of the type only.

Potential approach:

Since it is possible to ask Spring for a bean by its full generic type, I might be able to implement a conditional factory that will try to get a fully-typed instance and produce one if it doesn't already exist. I'm now investigating if/how this can be done. Amazingly, when implementing a custom condition (to be used with @Conditional), it does not have access to the bean type...

While every other modern injection framework operates on full types, Spring somehow still works with raw classes and string names(!) which just blows my mind... Is there any workaround for this?


Here's a fully working solution. I gave up on this approach, but I'm posting it in case someone finds it useful.

I made a custom annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Conditional(MissingGenericBeanCondition.class)
public @interface ConditionalOnMissingGenericBean {

    Class<?> containerType() default Void.class;
    Class<?>[] typeParameters() default {};
}

And a custom condition to go with it:

public class MissingGenericBeanCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if (!metadata.isAnnotated(ConditionalOnMissingGenericBean.class.getName()) || context.getBeanFactory() == null) {
            return false;
        }
        Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnMissingGenericBean.class.getName());
        Class<?> containerType = (Class<?>) attributes.get("containerType");
        Class<?>[] typeParameters = (Class<?>[]) attributes.get("typeParameters");

        ResolvableType resolvableType;
        if (Void.class.equals(containerType)) {
            if (!(metadata instanceof MethodMetadata) || !metadata.isAnnotated(Bean.class.getName())) {
                throw error();
            }
            //When resolving beans within the starter
            if (metadata instanceof StandardMethodMetadata) {
                resolvableType = ResolvableType.forType(((StandardMethodMetadata) metadata).getIntrospectedMethod().getGenericReturnType());
            } else {
                //When resolving beans in an application using the starter
                MethodMetadata methodMeta = (MethodMetadata) metadata;
                try {
                    // This might not be a safe thing to do. See the notes below.
                    Class<?> declaringClass = ClassUtils.forName(methodMeta.getDeclaringClassName(), context.getClassLoader());
                    Type returnType = Arrays.stream(declaringClass.getDeclaredMethods())
                            .filter(m -> m.isAnnotationPresent(Bean.class))
                            .filter(m -> m.getName().equals(methodMeta.getMethodName()))
                            .findFirst().map(Method::getGenericReturnType)
                            .orElseThrow(MissingGenericBeanCondition::error);
                    resolvableType = ResolvableType.forType(returnType);
                } catch (ClassNotFoundException e) {
                    throw error();
                }
            }
        } else {
            resolvableType = ResolvableType.forClassWithGenerics(containerType, typeParameters);
        }

        String[] names = context.getBeanFactory().getBeanNamesForType(resolvableType);
        return names.length == 0;
    }

    private static IllegalStateException error() {
        return new IllegalStateException(ConditionalOnMissingGenericBean.class.getSimpleName()
                + " is missing the explicit generic type and the implicit type can not be determined");
    }
}

These allow me to do the following:

@Bean
@ConditionalOnMissingGenericBean
public Container<Book> bookContainer() {
    return new Container<>(new Book());
}

@Bean
@ConditionalOnMissingGenericBean
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}

Both beans will now be loaded as they're of different generic types. If the user of the starter would register another bean of Container<Book> or Container<Computer> type, then the default bean would not be loaded, exactly as desired.

As implemented, it is also possible to use the annotation this way:

@Bean
//Load this bean only if Container<Car> isn't present
@ConditionalOnMissingGenericBean(containerType=Container.class, typeParameters=Car.class)
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}

Notes: Loading the configuration class in the condition might not be safe, but in this specific case it also might be... I have no clue. Didn't bother investigating further as I settled on a different approach altogether (different interface for each bean). If you have any info on this, please comment.

If explicit types are provided in the annotation, e.g.

@Bean
@ConditionalOnMissingGenericBean(containerType=Container.class, typeParameters=Computer.class)
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}

this concern goes away, but this approach will not work if the type parameters are also generic, e.g. Container<List<Computer>>.

Another way to make this certainly safe would be to accept the declaring class in the annotation:

@ConditionalOnMissingGenericBean(declaringClass=CustomConfig.class)

and then use that instead of

Class<?> declaringClass = ClassUtils.forName(methodMeta.getDeclaringClassName(), context.getClassLoader());

ConditionalOnMissingBean (Spring Boot 2.3.2.RELEASE API), OnBeanCondition.class) public @interface ConditionalOnMissingBean classes that may contain the specified bean types within their generic parameters. Spring Boot @ConditionalOnMissingBean and generic types. Java; sof; but since this is a part of a Spring Boot Starter, it would make it annoying to use, as the


Is there any workaround for this?

Unfortunately, I think not. The problem is that generics in Java do not exist in the compiled bytecode - they are used only for type checking at compile time and they only exist in the source code. Therefore at runtime, when Spring Boot sees your bean configuration class, it only sees two bean definitions that both create a Container. It does not see any difference between them unless you provide it yourself by giving them identifiers.

Spring @ConditionalOnMissingBean Example, The @ConditionalOnMissingBean annotation lets a bean be included based on We will create a main class to run our Spring Boot application. Additional classes that may contain the specified bean types within their generic parameters . The @ConditionalOnMissingBean annotation lets a bean be included based on the absence of specific beans. By default Spring will search entire hierarchy (SearchStrategy.ALL). Prerequisites. Eclipse Neon, Java 1.8, Gradle 5.4.2, Spring Boot 2.1.7. Creating Project


It's possible since Spring Boot v2.1.0.

That version introduced the new field parameterizedContainer on ConditionalOnMissingBean.

@Bean
@ConditionalOnMissingBean(value = Book.class, parameterizedContainer = Container.class)
public Container<Book> bookContainer() {
    return new Container<>(new Book());
}

@Bean
@ConditionalOnMissingBean(value = Computer.class, parameterizedContainer = Container.class)
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}

Spring Boot Conditional beans, Spring Boot Conditional beans tutorial shows how to register beans into the Spring Boot including @ConditionalOnClass , @ConditionalOnMissingBean package com.zetcode.bean; public class GenericMessage� In starting up this application, we don't get a lot of information about how or why Spring Boot decided to compose our application's configuration. But, we can have Spring Boot create a report simply by enabling debug mode in our application.properties file: debug=true Or our application.yml file: debug: true 4. Command-Line Approach


A Custom Auto-Configuration with Spring Boot, If we want to include a bean only if a specified bean is present or not, we can use the @ConditionalOnBean and @ConditionalOnMissingBean� Spring Boot made configuring Spring easier with its auto-configuration feature. In this quick tutorial, we'll explore the annotations from the org.springframework.boot.autoconfigure and org.springframework.boot.autoconfigure.condition packages.


Spring Autowiring of Generic Types, In this article we cover autowiring these generics in Spring. Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:. Spring Boot is well suited for web application development. You can create a self-contained HTTP server by using embedded Tomcat, Jetty, Undertow, or Netty. Most web applications use the spring-boot-starter-web module to get up and running quickly.


Spring Boot: @ConditionalOnMissingBean seems to prevent , I would like to use the @ConditionalOnMissingBean annotation to mark a number of component scanned classes as replaceable by classes that� I'm trying to establish the Kafka consumer using spring-kaka-2.2.0 and spring-boot-2.1.0 but I'm having the weird issue saying ConsumerFactory bean not found but still it is there in config. I have two types of messages in topic Student and Professor I'm trying to consumes them using @KafkaListener and @KafkaHandler but not gone so far