user2696466
user2696466

Reputation: 740

Can I inject Map of all impl of a bean in Spring

I am writing a service that gets an input based on which I need to call certain impl of one service. This input is a list of names of impls needs to called.

public interface Processor {

  Map<String, String> execute();

} 

@Service("BUCKET_PROCESSOR")
public class BucketProcessor implements Processor {
   ..... //first impl
}

@Service("QUERY_PROCESSOR")
public class QueryProcessor implements Processor {
   ..... //second impl
}

@Service("SQL_PROCESSOR")
public class SQLProcessor implements Processor {
   ..... //third impl
}

then I have a service where I want to inject a map of all these impls so that I can iterate over input and call respective impl.

@Service
public class MyAysncClient {

    @Autowired
    private Map<String, Processor> processorMap;  

    public void execute(List<String> processors) {

        List<Future> tasks = new ArrayList<>();

        for (String p : processors) {
            final Processor processor = this.processorMap.get(p);
            processor.execute()

            ....
        }

    }
}

Upvotes: 11

Views: 18583

Answers (5)

Mark Bramnik
Mark Bramnik

Reputation: 42491

Yes, you can - spring has this feature enabled by default. Namely, you can define inject a Map<String, Processor> into the spring bean.

This will instruct spring to find all beans which are implementations of Processor interface and these will be values of the map, the corresponding keys will be bean names.

So the code presented in the question should work.

Check the documentation of well-known @Autowired annotation.

In the section "Autowiring Arrays, Collections, and Maps" it states the following:

In case of an array, Collection, or Map dependency type, the container autowires all beans matching the declared value type. For such purposes, the map keys must be declared as type String which will be resolved to the corresponding bean names. Such a container-provided collection will be ordered, taking into account Ordered and @Order values of the target components, otherwise following their registration order in the container. Alternatively, a single matching target bean may also be a generally typed Collection or Map itself, getting injected as such.

See This example - the relevant part of it is where the map is injected into the test.

Upvotes: 12

tsarenkotxt
tsarenkotxt

Reputation: 3499

you can just use getBeansOfType(Processor.class):

Returns a Map with the matching beans, containing the bean names as keys and the corresponding bean instances as values

    @Bean
    public Map<String, Processor> processorMap(ApplicationContext context) {
        return context.getBeansOfType(Processor.class);
    }

Upvotes: 11

Vipul Kumar
Vipul Kumar

Reputation: 479

A better and elegant way to do the same is Define a Service locator pattern using below code

@Configuration
public class ProcessorConfig {
    @Bean("processorFactory")
    public FactoryBean<?> serviceLocatorFactoryBean() {
        ServiceLocatorFactoryBean factoryBean = new ServiceLocatorFactoryBean();
        factoryBean.setServiceLocatorInterface(ProcessorFactory.class);
        return factoryBean;
    }
}

public interface ProcessorFactory {
    Processor getProcessor(ProcessorTypes processorTypes);
}

then

public interface Processor {
    Map<String, String> execute();
}

@Component(ProcessorTypes.ProcessorConstants.BUCKET_PROCESSOR)
@Slf4j
public class BucketProcessor implements Processor {

    @Override
    public Map<String, String> execute() {
        return Collections.singletonMap("processor","BUCKET_PROCESSOR");
    }
}

@Component(ProcessorTypes.ProcessorConstants.QUERY_PROCESSOR)
@Slf4j
public class QueryProcessor implements Processor {

    @Override
    public Map<String, String> execute() {
        return Collections.singletonMap("processor","QUERY_PROCESSOR");
    }
}

@Component(ProcessorTypes.ProcessorConstants.SQL_PROCESSOR)
@Slf4j
public class SqlProcessor implements Processor {

    @Override
    public Map<String, String> execute() {
        return Collections.singletonMap("processor","SQL_PROCESSOR");
    }
}

Now define your service injecting the factory

@Service
@RequiredArgsConstructor
@Slf4j
public class ProcessorService {
    private final ProcessorFactory processorFactory;

    public void parseIndividual(ProcessorTypes processorTypes) {
        processorFactory
                .getProcessor(processorTypes)
                .execute();
    }

    public void parseAll(List<ProcessorTypes> processorTypes) {
        processorTypes.forEach(this::parseIndividual);
    }
}

In client, you can execute in below way

processorService.parseAll(Arrays.asList(ProcessorTypes.SQL, ProcessorTypes.BUCKET, ProcessorTypes.QUERY));
processorService.parseIndividual(ProcessorTypes.BUCKET);

If you want to expose as REST API you can do it in below way

@RestController
@RequestMapping("/processors")
@RequiredArgsConstructor
@Validated
public class ProcessorController {
    private final ProcessorService processorService;

    @GetMapping("/process")
    public ResponseEntity<?> parseContent(@RequestParam("processorType") @Valid ProcessorTypes processorTypes) {
        processorService.parseIndividual(ProcessorTypes.BUCKET);
        return ResponseEntity.status(HttpStatus.OK).body("ok");
    }

    @GetMapping("/process-all")
    public ResponseEntity<?> parseContent() {
        processorService.parseAll(Arrays.asList(ProcessorTypes.SQL, ProcessorTypes.BUCKET, ProcessorTypes.QUERY));
        return ResponseEntity.status(HttpStatus.OK).body("ok");
    }
}

Hope your problem gets resolved by the solution

Upvotes: 2

Andrew Kolesnyk
Andrew Kolesnyk

Reputation: 211

Yes, you can, but it needs some improvements to your current code in order to make it work in this way. First of all you have to add the getProcessorName method to the Processor interface:

public interface Processor {
  Map<String, String> execute();

  String getProcessorName();
} 

When you implement it, you should set it's name in returning of getProcessorName method

@Service
public class QueryProcessor implements Processor {
   //...

   @Override
   public String getProcessorName() {
      return "QUERY_PROCESSOR";
   }
}

Then you must create a spring configuration or add bean creation to the existing one

@Configuration
public class MyShinyProcessorsConfiguration {

   @Bean
   @Qualifier("processorsMap")
   public Map<String, Processor> processorsMap(List<Processor> processors) {
      Map<String, Processor > procMap = new HashMap<>();
      processors.forEach(processor -> procMap.put(processor.getProcessorName(), processor);
      return procMap;
   }
}

...and then you can simply add your processors map to any component

@Service
public class MyAysncClient {

    @Autowired
    @Qualifier("processorsMap")
    private Map<String, Processor> processorsMap;  
    
}

Upvotes: 4

Mithat Konuk
Mithat Konuk

Reputation: 457

I think this will help you , add bean configuration into configuration file

@Bean(name = "mapBean")
public Map<String, Processor > mapBean() {
    Map<String, Processor > map = new HashMap<>();
    //populate the map here 
    return map;
}

in your service

@Service
public class MyAysncClient {

    @Autowired
    @Qualifier("mapBean")
    private Map<String, Processor> processorMap;  

    public void execute(List<String> processors) {

        List<Future> tasks = new ArrayList<>();

        for (String p : processors) {
            final Processor processor = this.processorMap.get(p);
            processor.execute()

            ....
        }

    }
}

by the way if you dont need name of the beans (according your example) so define a list , spring will inject all bean defined as service on the same interface

@Autowired 
private List<Processor> processors; // include all defined beans

after that iterate each of them and call execute method.

Upvotes: 1

Related Questions