Spring auto configuration doesn't find generic classes

228 views Asked by At

I'm using Spring Boot 3.1.5 and I'm trying to implement an auto-configuration class (called OccurrentMongoAutoConfiguration) for one of my open-source projects. In this project, I have a generic interface defined like this:

public interface CloudEventConverter<T> {
        ... 
}

I'd like the auto-configuration to create a default bean of such as interface, unless it already exists. Here's the code that I have in my auto-configuration class:

@Bean
@ConditionalOnMissingBean(CloudEventTypeMapper.class)
public CloudEventTypeMapper<?> occurrentTypeMapper() {
    return ReflectionCloudEventTypeMapper.qualified();
}

Now, this CloudEventTypeMapper is used in this method:

@Bean
@ConditionalOnMissingBean(CloudEventConverter.class)
public <T> CloudEventConverter<?> occurrentCloudEventConverter(Optional<ObjectMapper> objectMapper, OccurrentProperties occurrentProperties, CloudEventTypeMapper<T> cloudEventTypeMapper) {
    ObjectMapper mapper = objectMapper.orElseGet(ObjectMapper::new);
    return new JacksonCloudEventConverter.Builder<T>(mapper, occurrentProperties.getCloudEventConverter().getCloudEventSource())
            .typeMapper(cloudEventTypeMapper)
            .build();
}

However, when I try to bootstrap an application that loads my auto-configuration, I get this error:

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 2 of method occurrentCloudEventConverter in org.occurrent.springboot.mongo.blocking.OccurrentMongoAutoConfiguration required a bean of type 'org.occurrent.application.converter.typemapper.CloudEventTypeMapper' that could not be found.


Action:

Consider defining a bean of type 'org.occurrent.application.converter.typemapper.CloudEventTypeMapper' in your configuration.

This is even the case if I try to add such a bean manually:

@Bean
fun rpsTypeMapper(): CloudEventTypeMapper<GameEvent> = ReflectionCloudEventTypeMapper. qualified(GameEvent::class.java)

If I debug the code, I can see that it goes into the rpsTypeMapper, but it doesn't seem to be injected to occurrentCloudEventConverter (and neither is the default occurrentTypeMapper bean).

I've tried removing the generics (<T>) in the OccurrentMongoAutoConfiguration as well as replacing them with <?>, but it doesn't help.

What's wrong and how can I fix it?

PS: Here's the code for the OccurrentMongoAutoConfiguration, and here's the bootstrap code. It's also possible to start the app using test containers, by running TestBootstrap.

To reproduce you can clone the repository from here: [email protected]:johanhaleby/occurrent.git, then switch branch to decider-web and run the main method in org.occurrent.example.domain.rps.decidermodel.TestBoostrap.

2

There are 2 answers

2
peacetrue On

I modified the code for testing:

@Bean
@ConditionalOnMissingBean(CloudEventConverter.class)
public <T> CloudEventConverter<?> occurrentCloudEventConverter(Optional<ObjectMapper> objectMapper, OccurrentProperties occurrentProperties,
                                                               @Autowired(required = false) CloudEventTypeMapper<T> cloudEventTypeMapper) {
    cloudEventTypeMapper = ReflectionCloudEventTypeMapper.qualified();
    ObjectMapper mapper = objectMapper.orElseGet(ObjectMapper::new);
    return new JacksonCloudEventConverter.Builder<T>(mapper, occurrentProperties.getCloudEventConverter().getCloudEventSource())
            .typeMapper(cloudEventTypeMapper)
            .build();
}

Prompt following questions:

  OccurrentMongoAutoConfiguration#occurrentTypeMapper:
  Did not match:
     - @ConditionalOnMissingBean (types: org.occurrent.application.converter.typemapper.CloudEventTypeMapper; SearchStrategy: all) found beans of type 'org.occurrent.application.converter.typemapper.CloudEventTypeMapper' rpsTypeMapper (OnBeanCondition)

At Bootstrap.java, exists follow code:

@Bean
fun rpsTypeMapper(): CloudEventTypeMapper<GameEvent> = ReflectionCloudEventTypeMapper.simple(GameEvent::class.java)

caused occurrentTypeMapper in OccurrentMongoAutoConfiguration.java not invoked:

@Bean
@ConditionalOnMissingBean(CloudEventTypeMapper.class)
public CloudEventTypeMapper<?> occurrentTypeMapper() {
    return ReflectionCloudEventTypeMapper.qualified();
}

But the type CloudEventTypeMapper<GameEvent> not matched occurrentCloudEventConverter.

Otherwise remove @ConditionalOnMissingBean(CloudEventTypeMapper.class) will be ok.

@Bean
public CloudEventTypeMapper<?> occurrentTypeMapper() {
    return ReflectionCloudEventTypeMapper.qualified();
}
3
Alireza Nazari On

The issue comes up because Spring has a tough time figuring out the generic type for CloudEventTypeMapper.

To fix this, you can use a nifty class called ResolvableType from Spring:

public abstract class OccurrentMongoAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(CloudEventTypeMapper.class)
    public CloudEventTypeMapper<?> occurrentTypeMapper() {
        return ReflectionCloudEventTypeMapper.qualified();
    }

    @Bean
    @ConditionalOnMissingBean(CloudEventConverter.class)
    public CloudEventConverter<?> occurrentCloudEventConverter(
            Optional<ObjectMapper> objectMapper,
            OccurrentProperties occurrentProperties,
            CloudEventTypeMapper<?> cloudEventTypeMapper) {
        
        ResolvableType resolvableType = ResolvableType.forClassWithGenerics(CloudEventConverter.class, cloudEventTypeMapper.getClass());

        ObjectMapper mapper = objectMapper.orElseGet(ObjectMapper::new);
        return new JacksonCloudEventConverter.Builder<>(mapper, occurrentProperties.getCloudEventConverter().getCloudEventSource())
                .typeMapper((CloudEventTypeMapper) resolvableType.resolveGeneric())
                .build();
    }
}

With this change, Spring should now be able to figure out the generic type for CloudEventTypeMapper and create the occurrentCloudEventConverter bean smoothly. Give it a try and see if it solves your issue!