Search code examples
javaspringguava

Guava ImmutableBiMap becomes LinkedHashMap and cause Spring autowiring mistake


I have ImmutableBiMap filled with 2 simple Spring beans.

OS: Manjaro Linux JDK version: 1.8.0.102 Oracle Spring version: 4.3.4.RELEASE from

<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Athens-SR1</version>

Creating context throws:

Exception in thread "main" org.springframework.beans.factory.BeanCreationException: 
    Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [...]: Illegal arguments for constructor; nested exception is java.lang.IllegalArgumentException: argument type mismatch
    Caused by: java.lang.IllegalArgumentException: argument type mismatch
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)

As following screen show, when exception is throw by Spring's BeanUtil argument is a LinkedHashMap instead of BiMap.

enter image description here

Minimal, Complete, and Verifiable example:

@Component
@Slf4j
public class TestControl {
    private final BiMap<String, Integer> automatons;

    @Autowired
    public TestControl(BiMap<String, Integer> automatons) {
        this.automatons = automatons;
        log.info("automatons={}", automatons.keySet());
    }
}

@Configuration
public class TextContext {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(
                TextContext.class,
                TestControl.class
        );
        BiMap bean = context.getBean(BiMap.class);
    }

    @Bean
    BiMap<String, Integer> automatons() {
        return ImmutableBiMap.of(
                "Cellular Automaton", cellularAutomaton(),
                "Monte Carlo Automaton", monteCarloAutomaton());
    }

    @Bean
    Integer cellularAutomaton() {
       return 6;
    }

    @Bean
    Integer monteCarloAutomaton() {
       return 5;
    }
}

Solution

  • This is a side effect of how Spring handles some container types.

    Even typed Maps can be autowired as long as the expected key type is String. The Map values will contain all beans of the expected type, and the keys will contain the corresponding bean names: [...]

    A BiMap is a Map.

    Spring isn't trying to inject your automatons bean into the TestControl. Instead, it's trying to find all beans of type Integer as the values, collecting them into a Map (LinkedHashMap as implementation of choice), and associating them with their bean name as the key.

    In this case, it fails because the constructor expects a BiMap.

    One solution is to inject by name.

    @Autowired()
    public TestControl(@Qualifier(value = "automatons") BiMap<String, Integer> automatons) {
        this.automatons = automatons;
    }
    

    By specifying a qualifier with a name, Spring will instead try to find a bean (with the appropriate type) that's named automatons.

    If you're not too attached to the final instance field, you could also inject the field with @Resource

    @Resource(name = "automatons") // if you don't specify the name element, Spring will try to use the field name
    private BiMap<String, Integer> automatons;
    

    For reasons, this will only work 4.3+.

    For beans that are themselves defined as a collection/map or array type, @Resource is a fine solution, referring to the specific collection or array bean by unique name. That said, as of 4.3, collection/map and array types can be matched through Spring’s @Autowired type matching algorithm as well, as long as the element type information is preserved in @Bean return type signatures or collection inheritance hierarchies. In this case, qualifier values can be used to select among same-typed collections, as outlined in the previous paragraph.

    I would be OK with the behavior you're seeing in pre-4.3, but this does seem like a bug for Map. (The correct behavior occurs for List and array types.)

    I've opened SPR-15117 to track it, which has now been resolved (2 day turnover, wow!).