Accessing Beans outside of the Step Scope in Spring Batch - spring

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.

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();
}

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().

Related

How to dynamically initialize ItemWriter Bean with #StepScope at runtime when ItemWriter Bean is extended?

[New to Spring Batch] I have different csv of different format, there can be more csv added in future so I thought of having a common FlatFileItemReader<T> instead of defining #Bean for each csv format, I created a base configuration class then concrete class for each csv type.
Since I have defined Reader bean as #StepScope , during batch job runtime it auto-initializes bean with the first concrete class in the package, same kind of problem is discussed here but answer is not relevant to my case
How do I pass particular concrete class type of ItemReader to my step during job run?
Here is my base configuration class:
public abstract class AbstractBatchItemReader<T> {
private CsvInformation csvInformation;
protected AbstractBatchItemReader(CsvInformation csvInformation) {
this.csvInformation = csvInformation;
}
#Bean
#StepScope
//fileName is retrieved from jobParameters during runtime
public FlatFileItemReader<T> getItemReader(#Value("#{jobParameters['input.file.name']}") String fileName) {
return new FlatFileItemReaderBuilder<T>()
.name("invoiceHeaderItemReader")
.resource(new FileSystemResource(fileName))
.linesToSkip(1)
.delimited()
.names(csvInformation.getHeaders().split(","))
.fieldSetMapper(new BeanWrapperFieldSetMapper<T>() {{
setConversionService(new StringToLocalDateConversion().convert());
setTargetType(csvInformation.getERPClass());
}})
.build();
}
}
Here is the concrete class that extends the base config:
#Configuration
public class InvoiceHeaderReader extends AbstractBatchItemReader<ERPInvoiceHeader> {
protected InvoiceHeaderReader(InvoiceHeaderCsvInformation csvInformation) {
super(csvInformation);
}
}
Here is my base step config:
public abstract class AbstractBatchStep<T> {
private final AbstractBatchItemReader<T> reader;
private final AbstractBatchItemWriter<T> writer;
private final StepBuilderFactory stepBuilderFactory;
protected AbstractBatchStep(AbstractBatchItemReader<T> reader,
AbstractBatchItemWriter<T> writer,
StepBuilderFactory stepBuilderFactory) {
this.reader = reader;
this.writer = writer;
this.stepBuilderFactory = stepBuilderFactory;
}
public Step getStep() {
afterPropertiesSet();
return stepBuilderFactory.get("Batch Step")
.<T, T>chunk(BatchConfiguration.READER_CHUNK_SIZE)
//fileName is passed during runtime
.reader(reader.getItemReader(null))
.writer(writer.getItemWriter())
.build();
}
}
Here is the concrete class that extends step config:
#Configuration("invoice_header")
public class InvoiceHeaderStep extends AbstractBatchStep<ERPInvoiceHeader> {
protected InvoiceHeaderStep(InvoiceHeaderReader reader, InvoiceHeaderWriter writer, StepBuilderFactory stepBuilderFactory) {
super(reader, writer, stepBuilderFactory);
}
}
The whole Job cycle runs only for the first concrete class in the package if I try to run another type of csv it fails with exception.. Unexpected token required n found n exception is obviously because the reader was auto initialized by first class in package, not the one that I pass to Step
Please also suggest if this design pattern is correct of there could possibly be an easy way to achieve this.
I would like to post answer as a reference to others.
I created an AbstractBatchItemReader<T> class which has base configuration
Concrete classes that extends base config class TypeOneCsvReader extends AbstractBatchItemReader<TypeOneEntity>
3.Interface with Csv Information methods and Classes implementing interface for each Csv type
Here is the code sample:
AbstractBatchItemReader:
public abstract class AbstractBatchItemReader<T> {
private CsvInformation csvInformation;
protected AbstractBatchItemReader(CsvInformation csvInformation) {
this.csvInformation = csvInformation;
}
FlatFileItemReader<T> getItemReader() {
return new FlatFileItemReaderBuilder<T>()
.name("Batch Reader")
.resource(resource(null))
.linesToSkip(1)
.delimited()
.quoteCharacter(BatchConfiguration.READER_QUOTE_CHARACTER)
.names(csvInformation.getHeaders().split(","))
.fieldSetMapper(new BeanWrapperFieldSetMapper<T>() {{
setConversionService(StringToLocalDateConversion.convert());
setTargetType(csvInformation.getEntityClass());
}})
.build();
}
#Bean
#StepScope
public Resource resource(#Value("#{jobParameters['input.file.name']}") String fileName) {
return new FileSystemResource(fileName);
}
}
Concrete Class:
#Configuration
public class TypeOneCsvReader extends AbstractBatchItemReader<TypeOneEntity> {
protected TypeOneCsvReader(TypeOneCsv csvInformation) {
super(csvInformation);
}
}
CsvInformation Interface:
public interface CsvInformation {
String getHeaders();
Class getEntityClass();
}
Each implementation of interface has to be annotated with #Component so that Concrete Reader class picks it up via DI
Benefit of having such an approach is, it can be scaled to as many csv type as required and also the Reader logic stays at one place
Thanks

#Bean-method providing a list is ignored, if there is a #Bean-method proving a single bean

I have a configuration providing a single bean and a configuration providing a list of beans. All these beans have the same type.
When I start up an application context with these configurations, I see that an autowired list of the bean type only contains the single bean. I want it to include all beans of that type. I use Spring 5.2.0.
I have boiled it down to one configuration: if I provide a single bean and a list of beans, only the single bean will be used.
This is reproduced in the following test. It fails, because the list only contains "A" and "D" (which shows it did not autowire the list of beans):
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = { TestConfiguration.class })
class AutowiringListsTest {
#Autowired
private List<TestBean> testBeanList;
#Test
void testThatBothConfigurationsContributeToBeanList() {
final List<String> idList = testBeanList.stream().map(TestBean::getId).sorted().collect(Collectors.toList());
assertThat(idList, hasItems("A", "B", "C", "D"));
}
#Configuration
public static class TestConfiguration {
#Bean
public TestBean someBean() {
return new TestBean("A");
}
#Bean
public List<TestBean> someMoreBeans() {
return Arrays.asList(new TestBean("B"), new TestBean("C"));
}
#Bean
public TestBean anotherBean() {
return new TestBean("D");
}
}
public static class TestBean {
private final String id;
public TestBean(final String id) {
this.id = id;
}
private String getId() {
return id;
}
}
}
I want to get this to run so that multiple modules can provide beans of a certain type.
Some modules want to provide multiple beans and their number depends on a property.
Some modules will always provide one bean.
The module using the beans (autowiring them as list) should autowire all beans.
How can I get this to run? In what scenario does Spring's behavior make sense?
I can work around the issue by introducing a TestBeanFactory. Each configuration that wants to contribute to the list of TestBeans instead provides a factory.
#Configuration
public static class TestConfiguration {
/** Implemented once in the configuration that defines <code>TestBean</code>. */
#Bean
public List<TestBean> testBeansFromFactory(Collection<TestBeanFactory> factories) {
return factories.stream().map(TestBeanFactory::createTestBeans).flatMap(Collection::stream)
.collect(toList());
}
// Further methods can be defined in various configurations that want to add to the list of TestBeans.
#Bean
public TestBeanFactory someBean() {
return () -> Arrays.asList(new TestBean("A"));
}
#Bean
public TestBeanFactory someMoreBeans() {
return () -> Arrays.asList(new TestBean("B"), new TestBean("C"));
}
#Bean
public TestBeanFactory anotherBean() {
return () -> Arrays.asList(new TestBean("D"));
}
}
public static class TestBean { ... }
public static interface TestBeanFactory {
public Collection<TestBean> createTestBeans();
}
That works and is only slightly more code.
M.Deinum makes a point in the comments for Spring's behavior being consistent:
As you defined a bean of type List it will be used. Autowiring is based on type, it will try to detect the bean of the certain type. The collection (and map as well) is a special one looking up all dependencies of the given type.

Spring Data Rest: #Autowire in Custom JsonDeserializer

I am trying to autowire a component into a custom JsonDeserializer but cannot get it right even with the following suggestions I found:
Autowiring in JsonDeserializer: SpringBeanAutowiringSupport vs HandlerInstantiator
Right way to write JSON deserializer in Spring or extend it
How to customise the Jackson JSON mapper implicitly used by Spring Boot?
Spring Boot Autowiring of JsonDeserializer in Integration test
My final goal is to accept URLs to resources in different microservices and store only the ID of the resource locally. But I don't want to just extract the ID from the URL but also verify that the rest of the URL is correct.
I have tried many things and lost track a bit of what I tried but I believe I tried everything mentioned in the links above. I created tons of beans for SpringHandlerInstantiator, Jackson2ObjectMapperBuilder, MappingJackson2HttpMessageConverter, RestTemplate and others and also tried with setting the SpringHandlerInstantiator in RepositoryRestConfigurer#configureJacksonObjectMapper.
I am using Spring Boot 2.1.6.RELEASE which makes me think something might have changed since some of the linked threads are quite old.
Here's my last attempt:
#Configuration
public class JacksonConfig {
#Bean
public HandlerInstantiator handlerInstantiator(ApplicationContext applicationContext) {
return new SpringHandlerInstantiator(applicationContext.getAutowireCapableBeanFactory());
}
}
#Configuration
public class RestConfiguration implements RepositoryRestConfigurer {
#Autowired
private Validator validator;
#Autowired
private HandlerInstantiator handlerInstantiator;
#Override
public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
validatingListener.addValidator("beforeCreate", validator);
validatingListener.addValidator("beforeSave", validator);
}
#Override
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
objectMapper.setHandlerInstantiator(handlerInstantiator);
}
}
#Component
public class RestResourceURLSerializer extends JsonDeserializer<Long> {
#Autowired
private MyConfig config;
#Override
public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ServiceConfig serviceConfig = config.getServices().get("identity");
URI serviceUri = serviceConfig.getExternalUrl();
String servicePath = serviceUri.getPath();
URL givenUrl = p.readValueAs(URL.class);
String givenPath = givenUrl.getPath();
if (servicePath.equals(givenPath)) {
return Long.parseLong(givenPath.substring(givenPath.lastIndexOf('/') + 1));
}
return null;
}
}
I keep getting a NullPointerException POSTing something to the API endpoint that is deserialized with the JsonDeserializer above.
I was able to solve a similar problem by marking my deserializer constructor accept a parameter (and therefore removing the empty constructor) and marking constructor as #Autowired.
public class MyDeserializer extends JsonDeserializer<MyEntity> {
private final MyBean bean;
// no default constructor
#Autowired
public MyDeserializer(MyBean bean){
this.bean = bean
}
...
}
#JsonDeserialize(using = MyDeserializer.class)
public class MyEntity{...}
My entity is marked with annotation #JsonDeserialize so I don't have to explicitly register it with ObjectMapper.

how to avoid using context.getbean in spring

There have been several arguments around not using ApplicationContext.getBean() to get a bean reference, of which most are based on logic that it violates the principles of Inversion of control.
Is there a way to get reference to prototype scoped bean without calling context.getBean() ?
Consider to use Spring Boot!
Than you can do something like this...
Runner:
#SpringBootApplication
public class Runner{
public static void main(String[] args) {
SpringApplication.run(Runner.class, args);
}
}
Some Controller:
#Controller
public class MyController {
// Spring Boot injecting beans through #Autowired annotation
#Autowired
#Qualifier("CoolFeature") // Use Qualifier annotation to mark a class, if for example
// you have more than one concreate class with differant implementations of some interface.
private CoolFeature myFeature;
public void testFeature(){
myFeature.doStuff();
}
}
Some cool feature:
#Component("CoolFeature") // To identify with Qualifier
public class CoolFeature{
#Autowired
private SomeOtherBean utilityBean;
public void doStuff(){
// use utilityBean in some way
}
}
No XML files to handle.
We can still access context for manual configurations if needed.
Suggested reading:
Spring Boot Reference
Pro Spring Boot
This type of problem can be solved using method injection, which is described in more detail here: https://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html#beans-factory-method-injection
This is the most common approach to create prototype bean:
abstract class MyService {
void doSome() {
OtherService otherService = getOtherService();
}
abstract OtherService getOtherService();
}
#Configuration
class Config {
#Bean
public MyService myService() {
return new MyService() {
OtherService getOtherService() {
return otherService();
}
}
}
#Bean
#Scope("prototype")
public OtherService otherService() {
return new OtherService();
}
}

Events in Spring Boot

Is it possible to use events in Spring Boot? I need to execute one method but without waiting for return. I'm trying to use this:
public class GerarSeloEvent extends ApplicationEvent {
private TbPedido pedido;
private Integer cdCartorio;
public GerarSeloEvent(Object source, TbPedido pedido, Integer cdCartorio) {
super(source);
this.pedido = pedido;
this.cdCartorio = cdCartorio;
}
public TbPedido getPedido() {
return pedido;
}
public Integer getCdCartorio() {
return cdCartorio;
}
}
#Component
public class GerarSeloListener implements ApplicationListener<GerarSeloEvent> {
#Autowired
SeloService seloService;
#Override
public void onApplicationEvent(GerarSeloEvent event) {
seloService.gerarSelos(event.getPedido(), event.getCdCartorio());
}
}
and my call
GerarSeloEvent gerarSelos = new GerarSeloEvent(this, pedido, cdCartorio);
EnviarEmailPedidoEvent enviarEmail = new EnviarEmailPedidoEvent(this, pedido);
publisher.publishEvent(gerarSelos);
But my code waits to return anything to my front-end. I need one async event.
This should work:
#Component
public class GerarSeloListener {
private final SeloService seloService;
#Autowired
public GerarSeloListener(SeloService seloService) { ... }
#EventListener
#Async
public void handleGerarSeloEvent(GerarSeloEvent event event) {
....
}
You need to add #EnableAsync on one of your configuration (the best place is your #SpringBootApplication annotated class). But as Martin already said you don't need event if you want to process a method asynchronously: only add #Async and invoke it the usual way.
You may want to read the documentation

Resources