3

Is it possible to access beans defined outside of the step scope? For example, if I define a strategy "strategyA" and pass it in the job parameters I would like the @Value to resolve to the strategyA bean. Is this possible? I am currently working round the problem by getting the bean manually from the applicationContext.

@Bean
@StepScope
public Tasklet myTasklet(
        @Value("#{jobParameters['strategy']}") MyCustomClass myCustomStrategy)

    MyTasklet myTasklet= new yTasklet();

    myTasklet.setStrategy(myCustomStrategy);

    return myTasklet;
}

I would like to have the ability to add more strategies without having to modify the code.

Haim Raman
  • 11,508
  • 6
  • 44
  • 70
Ash McConnell
  • 1,340
  • 1
  • 17
  • 28

2 Answers2

5

The sort answer is yes. This is more general spring/design pattern issue rater then Spring Batch.
The Spring Batch tricky parts are the configuration and understanding scope of bean creation.
Let’s assume all your Strategies implement Strategy interface that looks like:

interface Strategy {
    int execute(int a, int b); 
};

Every strategy should implements Strategy and use @Component annotation to allow automatic discovery of new Strategy. Make sure all new strategy will placed under the correct package so component scan will find them.
For example:

@Component
public class StrategyA implements Strategy {
    @Override
    public int execute(int a, int b) {
        return a+b;
    }
}

The above are singletons and will be created on the application context initialization.
This stage is too early to use @Value("#{jobParameters['strategy']}") as JobParameter wasn't created yet.

So I suggest a locator bean that will be used later when myTasklet is created (Step Scope).

StrategyLocator class:

public class StrategyLocator {
    private Map<String, ? extends Strategy> strategyMap;

    public Strategy lookup(String strategy) {
        return strategyMap.get(strategy);
    }

    public void setStrategyMap(Map<String, ? extends Strategy> strategyMap) {
        this.strategyMap = strategyMap;
    }

}

Configuration will look like:

@Bean
@StepScope
public MyTaskelt myTasklet () {
      MyTaskelt myTasklet = new MyTaskelt();
      //set the strategyLocator
      myTasklet.setStrategyLocator(strategyLocator());
      return myTasklet;
}
@Bean 
protected StrategyLocator strategyLocator(){
    return  = new StrategyLocator();    
}    

To initialize StrategyLocator we need to make sure all strategy were already created. So the best approach would be to use ApplicationListener on ContextRefreshedEvent event (warning in this example strategy names start with lower case letter, changing this is easy...).

@Component
public class PlugableStrategyMapper implements ApplicationListener<ContextRefreshedEvent> {
    @Autowired
    private StrategyLocator strategyLocator;
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
        Map<String, Strategy> beansOfTypeStrategy = applicationContext.getBeansOfType(Strategy.class);
        strategyLocator.setStrategyMap(beansOfTypeStrategy);        
    }

}

The tasklet will hold a field of type String that will be injected with Strategy enum String using @Value and will be resolved using the locator using a "before step" Listener.

    public class MyTaskelt implements Tasklet,StepExecutionListener {
        @Value("#{jobParameters['strategy']}")
        private String strategyName;
        private Strategy strategy;
        private StrategyLocator strategyLocator;

        @BeforeStep
        public void beforeStep(StepExecution stepExecution) {
            strategy = strategyLocator.lookup(strategyName);        
        }
        @Override
        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
            int executeStrategyResult = strategy.execute(1, 2); 
        }
           public void setStrategyLocator(StrategyLocator strategyLocator) {
               this.strategyLocator = strategyLocator;
           }
    }

To attach the listener to the taskelt you need to set it in your step configuration:

@Bean
protected Step myTaskletstep() throws MalformedURLException {
     return steps.get("myTaskletstep")
    .transactionManager(transactionManager())
    .tasklet(deleteFileTaskelt())
    .listener(deleteFileTaskelt())
    .build();
 }
Haim Raman
  • 11,508
  • 6
  • 44
  • 70
  • Thanks Haim for such a comprehensive and well explained example. I fear my question wasn't quite as well explained. I was hoping to avoid having the explicit dependency on each strategy if possible. I was hoping to be able to provide a reference to the bean (using it's name derived from the strategy job parameter). In my case it is more important to remove this coupling so I think I will have to use my workaround of using applicationContext.getBean(strategyFromJobParams). – Ash McConnell May 13 '14 at 08:25
  • Can you please elaborate? Spring will create an instance of every Strategy anyway as you said it’s a singletone. applicationContext.getBean(strategyFromJobParams). Is acutely a big map that reference all beans an coupling it to spring API. My solution replace applicationContext with the locater which is limited map for specific bean, The enun is used as a reference to the Strategy bean. – Haim Raman May 13 '14 at 09:17
  • I would like to have the ability to add more strategies without having to modify the code (adding autowired strategyC to the code and to the map). This was possible previously with the XML config, I thought there would be a way with the JavaConfig without having to use applicationContext.getBean. It's not a huge deal, I just thought it would be possible – Ash McConnell May 14 '14 at 13:05
  • Ash McConnell see my changes, I added support form plugable stategies. – Haim Raman May 15 '14 at 06:54
0

jobParameters is holding just a String object and not the real object (and I think is not a good pratice store a bean definition into parameters).
I'll move in this way:

@Bean
@StepScope    
class MyStategyHolder {
  private MyCustomClass myStrategy;
  // Add get/set

  @BeforeJob
  void beforeJob(JobExecution jobExecution) {
    myStrategy = (Bind the right strategy using job parameter value);
  }
}

and register MyStategyHolder as listener.
In your tasklet use @Value("#{MyStategyHolder.myStrategy}") or access MyStategyHolder instance and perform a getMyStrategy().

Luca Basso Ricci
  • 17,829
  • 2
  • 47
  • 69
  • Thanks for your answer! I might be confused, but your code has the same issue as my original answer, there is no way to bind the strategy without going to the application context and using the job parameter to select the right bean. The #{jobParameter['strategy']} should attempt to find a bean with the name strategyA for example (as it uses # instead of $). This seemed to work ok with an XML config, but it has stopped working when we moved to JavaConfig – Ash McConnell May 09 '14 at 10:08
  • Sorry,but to be honest I have never used JavaConfig. You can try wire `myStrategyHolder` into tasklet and use getter() to access strategy – Luca Basso Ricci May 09 '14 at 13:00
  • Does strategy returns a singletone object or a prototype – Haim Raman May 11 '14 at 07:49
  • Your choice; depends on your requests – Luca Basso Ricci May 11 '14 at 08:21
  • strategy returns is a singleton – Ash McConnell May 12 '14 at 08:41
  • fine. strategy scope is not important (but better singleton or scope than prototype) – Luca Basso Ricci May 12 '14 at 09:00