Some subtleties of injection collections in Spring

Hello everyone! My name is Vladislav Rodin. I am currently teaching courses on software architecture and high load software architecture on the OTUS portal. Now in OTUS there is an open set for the new thread of the Developer course on the Spring Framework . In anticipation of the start of the course, I decided to write a little copyright material that I want to share with you.





Background


Spring contains a lot of “magic” within itself, carrying out some unobvious things on its own. Ignorance or misunderstanding of this can lead to side effects that you may encounter in the process of writing your application using this framework.

One of these unobvious things is injection of Java Collection Framework interfaces. Having independently stepped on a rake related to this topic, and having heard the next questions from my colleagues, I decided to sort it out and record the results of my research in the form of an article with the hope that it would help someone already in work or during the initial development of Spring.

Scenario


Let's look at a service that will work with movie heroes. There will be 3 of them: Rambo, Terminator and Gandalf. Each of them will represent a separate class, and their parent will be common - Hero.

public class Hero {
}

@Component
public class Rambo extends Hero {

    @Override
    public String toString() {
        return "Rambo";
    }
}

@Component
public class Terminator extends Hero {

    @Override
    public String toString() {
        return "Terminator";
    }
}


@Component
public class Gandalf extends Hero {
    @Override
    public String toString() {
        return "Gandalf";
    }
}

Injection List


Let's assume that we want to create a service that will work with the heroes of the militants. Probably, you will have to inject a list of such heroes into it.

No problem! Create a service and configuration:

@Service
@Getter
public class ActionHeroesService {
    @Autowired
    List<Hero> actionHeroes;
}

@Configuration
public class HeroesConfig {

    @Bean
    public List<Hero> action() {
        List<Hero> result = new ArrayList<>();
        result.add(new Terminator());
        result.add(new Rambo());
        return result;
    }
}

Everything is fine, but when checking it can be found that Gandalf is on the list!

Spring, seeing that it was necessary to inject the List, went around the beans located in the context, found among them all that fit the generic type, collected List from them and injected it, ignoring our List.

How to get Spring to do what we want?

Option 1. Crutch


Since the problem is precisely in trying to inject the Java Collection Framework’s interface, you can simply replace List with ArrayList in the service and, of course, in the configuration so that Spring finds an instance of the class we need. Then everything will work as we expected.

@Configuration
public class HeroesConfig {

    @Bean
    public ArrayList<Hero> action() {
        ArrayList<Hero> result = new ArrayList<>();
        result.add(new Terminator());
        result.add(new Rambo());
        return result;
    }
}

@Service
@Getter
public class ActionHeroesService {
    @Autowired
    ArrayList<Hero> actionHeroes;
}

Option 2. More correct


Another way to tie Spring’s hands is to ask him to inject into the service not just any List, but a bean with a special name, adding Qualifier. Thus, we will be able to inject our bean.

@Service
@Getter
public class ActionHeroesService {
    @Autowired
    @Qualifier("action")
    List<Hero> actionHeroes;
}

Injection Maps


If many people know about the nuance of the injection of List, then things are generally worse with the Map.
Let's write a service that will work with the main characters of the films. Map will be injected into it, containing the names of the films by the keys, and by the values ​​of the beans of the main characters:

@Service
@Getter
public class MainCharactersService {
    @Autowired
    Map<String, Hero> mainCharactersByFilmName;
}

@Configuration
public class HeroesConfig {

    @Bean
    public Map<String, Hero> mainCharactersByFilmName() {
        Map<String, Hero> result = new HashMap<>();

        result.put("rambo", new Rambo());
        result.put("terminator", new Terminator());
        result.put("LOTR", new Gandalf());

        return result;
    }
}

At startup, you can see that Gandalf’s key is not LOTR, but gandalf, from which we can conclude that it was not the name of the film that was written, but the name of the bean, whereas in the case of Rimbaud and the terminator it was just lucky: the names of the main characters coincide with movie titles.

In fact, when it is necessary to inject a Map, the key of which is String, and the value of the beans, Spring (as in the case of the List) will simply ignore the Map we proposed, go through the context and collect all the suitable beans s, and will create a Map with them as values ​​and with their names as keys.

The workarounds are similar to those that worked for the List:

Option 1. Crutch


Replace Map with HashMap:

@Service
@Getter
public class MainCharactersService {
    @Autowired
    HashMap<String, Hero> mainCharactersByFilmName;
}


@Configuration
public class HeroesConfig {

    @Bean
    public HashMap<String, Hero> mainCharactersByFilmName() {
        HashMap<String, Hero> result = new HashMap<>();

        result.put("rambo", new Rambo());
        result.put("terminator", new Terminator());
        result.put("LOTR", new Gandalf());

        return result;
    }
}

Option 2. More correct


Add Qualifier:

@Service
@Getter
public class MainCharactersService {
    @Autowired
    @Qualifier("mainCharactersByFilmName")
    Map<String, Hero> mainCharactersByFilmName;
}

Source: https://habr.com/ru/post/undefined/


All Articles