52

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?

gstackoverflow
  • 36,709
  • 117
  • 359
  • 710
Joe
  • 3,581
  • 4
  • 23
  • 28

8 Answers8

30

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.

Ondrej Bozek
  • 10,987
  • 7
  • 54
  • 70
27

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:

Community
  • 1
  • 1
MariuszS
  • 30,646
  • 12
  • 114
  • 155
  • But this is not an attempt to inject a map that's registered as a bean. Autowired does support injecting all implementations which is what the question asked. See the docs: http://docs.spring.io/spring/docs/2.5.x/reference/beans.html#beans-autowired-annotation – Steve Sowerby Jan 04 '14 at 08:55
  • 1
    I have a situation. I dont want to do this from xml beans. I want to do this by annotations. Again, I cant do any hardcoding. Any suggestions? – abi_pat Mar 21 '18 at 09:08
  • Autwire them as lists and then loop through that list to create a map. If you could define the logic as a @Bean method, then you can get away with returning the map as a bean. – Raja Anbazhagan Nov 06 '21 at 07:35
16

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);
    });
}
TimYi
  • 441
  • 5
  • 8
14

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()));
}
Sergii Bishyr
  • 8,331
  • 6
  • 40
  • 69
4
@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.

Rigeborod
  • 356
  • 2
  • 6
3

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

  • Is there a Spring documentation that explains this solution? I am looking for answers on how this solution is working? – Valamburi M May 11 '23 at 09:51
2

This can be achieved with spring-componentmap (https://github.com/krud-dev/spring-componentmap)

Based on your example:

public interface ISendableConverter {

    ISendableMsg convert(BaseMessage baseMessage);

    @ComponentMapKey
    String getType();
}

@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";
    }
}

// Call the converter by type
@Component
class SendableMsgConverter {

    // @ComponentMap replaces @Autowired in this case
    @ComponentMap
    private Map<String, ISendableConverter> converterMap;

    public ISendableMsg convert(BaseMessage baseMessage) {
        ISendableConverter converter = converterMap.get(baseMessage.getType());
        return converter.convert(baseMessage);
    }
}

Disclaimer: I co-developed spring-componentmap

Idan Elhalwani
  • 538
  • 4
  • 12
0

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.

Ihor Patsian
  • 1,288
  • 2
  • 15
  • 25