41
votes

Here's the Map

@Autowired
private Map<String, ISendableConverter> converters;

and ISendableConverter

public interface ISendableConverter {

    ISendableMsg convert(BaseMessage baseMessage);

    String getType();
}

There are some classes that implements ISendableConverter

I want to inject them into the variable converters by using spring @Autowried annotation.

The instance of class as value, and the result of method getType() as key.

like this one

@Component
public class SendableVoiceMsgConverter implements ISendableConverter {

    @Override
    public ISendableMsg convert(BaseMessage baseMessage) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public String getType() {
        return "VOICE";
    }
}

Is this possible? and how?

7
Have you actually just tried this? It should work in Spring 2.5 and beyond. The key will be the bean name.Steve Sowerby
No, you can't inject them into a Map where the key is the return value of a method call on a bean.Sotirios Delimanolis
Possible duplicate of Spring can't autowire Map beanOhadR

7 Answers

24
votes

Try with something like @Resource - I have not tested this code.

@Resource(name="converters")
private Map<String, ISendableConverter> converters;

or

@Value("#{converters}")
private Map<String, ISendableConverter> converters;

From Spring Docs

(..) beans that are themselves defined as a collection or map type cannot be injected through @Autowired, because type matching is not properly applicable to them. Use @Resource for such beans, referring to the specific collection or map bean by unique name.

This should work, only if you prepare converters bean like this:

<util:map id="converters" scope="prototype" map-class="java.util.HashMap" 
          key-type="java.lang.String" value-type="...ISendableConverter">
    <entry key="VOICE" value="sendableVoiceMsgConverter" />
</util:map>

This is also similar question:

19
votes

You can create an automatically initialized map with keys of your choice using Spring Java configuration:

In class annotated with @Configuration annotation:

@Autowired
private List<ISendableConverter> sendableConverters;

@Bean
public Map<String, ISendableConverter> sendableConvertersMap() {
    Map<String, ISendableConverter> sendableConvertersMap = new HashMap<>();
    for (ISendableConverter converter : sendableConverters) {
        sendableConvertersMap.put(converter.getType(), converter);
    }
    return sendableConvertersMap;
}

Than you inject this map with:

@Resource()
private Map<String, ISendableConverter> sendableConverters;

You can optionally add some selector string to your @Resource annotation if you have defined more maps of the same type.

This way all you have to do is implement ISendableConverter by your spring bean and it will automatically appear in Map defined above. You don't need to create map items by hand for each implementation.

14
votes

you can do something like this:

@SuppressWarnings("unused")
private List<OneTypeImageLoader> imageLoaders;
private Map<String, OneTypeImageLoader> imageLoaderMap=new HashMap<>();

@Autowired
public void setImageLoaders(List<OneTypeImageLoader> imageLoaders) {
    this.imageLoaders = imageLoaders;
    imageLoaders.forEach(l-> {
        imageLoaderMap.put(l.getType(), l);
    });
}
13
votes

Try something like this, it works for me

private Map<String, ISendableConverter> converters;

@Autowired
public void setUploadServices(Set<ISendableConverter> converters){
    this.conveters = converters.stream()
        .collect(Collectors.toMap(ISendableConverter::getType, Function.identity()));
}

The same result can be achieved using constructor injection:

private Map<String, ISendableConverter> converters;

@Autowired
public Foo(Set<ISendableConverter> converters) {
    this.conveters = converters.stream()
        .collect(Collectors.toMap(ISendableConverter::getType, Function.identity()));
}
3
votes
@Component("VOICE")
public class SendableVoiceMsgConverter implements ISendableConverter {

    @Override
    public ISendableMsg convert(BaseMessage baseMessage) {
        // TODO Auto-generated method stub
        return null;
    }
}

You may want to just add the type name directly into the component annotation, that will do the job.

0
votes

You can make it more generic and build something like this:

    public interface BasicStrategy {
        String getKey();
    }

    public final class StrategyMap<K extends BasicStrategy> extends HashMap<String, K> {

    public StrategyMap(List<K> strategies) {
        super(strategies.stream().collect(Collectors.toMap(K::getKey, Function.identity())));
    }

    @Override
    public K get(Object key) {
        BasicStrategy basicStrategy = super.get(key);
        if (basicStrategy == null) {
            throw new RuntimeException("No strategy found for key: '" + key + "'");
        }
        return (K) basicStrategy;
    }
}

Now you can use this StrategyMap everywhere around your code like this:

private StrategyMap<ISendableConverter> converters;

@Autowired
public Foo(List<ISendableConverter> converters) {
    this.conveters = new StrategyMap<>(converters);
}

This approach generify the creation of a StrategyMap and also logic for the case where value is not found can be centralized.

PS: Of course ISendableConverter has to extend BasicStrategy interface.

0
votes

First, set Bean name like the return value of getType()

@Component("VOICE")
public class SendableVoiceMsgConverter implements ISendableConverter {

    @Override
    public ISendableMsg convert(BaseMessage baseMessage) {
        // TODO Auto-generated method stub
        return null;
    }
}

And you can use like this:

@Autowired
private Map<String, ISendableConverter> converters;

this map's V type is instance of Bean , and K type is Bean's name