Spring Batch test case with multiple data sources - spring-boot

I have a Spring Batch Classifier to test for which I've defined this test class:
#RunWith(SpringRunner.class)
#SpringBatchTest
#ContextConfiguration(classes = { BatchConfiguration.class })
class CsvOutputClassifierTest {
#Autowired
private FlatFileItemWriter<CsvData> createRequestForProposalWriter;
#Autowired
private FlatFileItemWriter<CsvData> createRequestForQuotationWriter;
private final CsvOutputClassifier csvOutputClassifier = new CsvOutputClassifier(
createRequestForProposalWriter,
createRequestForQuotationWriter);
#Test
void shouldReturnProposalWriter() {
...
}
The batch configuration class has this constructor:
public BatchConfiguration(
final JobBuilderFactory jobBuilderFactory,
final StepBuilderFactory stepBuilderFactory,
#Qualifier("oerationalDataSource") final DataSource oerationalDataSource,
final DwhFileManager dwhFileManager,
final OperationalRepository operationalRepository)
And these beans:
#StepScope
#Bean
public FlatFileItemWriter<CsvData> createRequestForProposalWriter(
#Value("#{jobParameters['startDate']}") String startDate) {
FlatFileItemWriter<CsvData> writer = new FlatFileItemWriter<CsvData>();
...
return writer;
}
#StepScope
#Bean
public FlatFileItemWriter<CsvData> createRequestForQuotationWriter(
#Value("#{jobParameters['startDate']}") String startDate) {
FlatFileItemWriter<CsvData> writer = new FlatFileItemWriter<CsvData>();
...
return writer;
}
Running the test class I'm not able to trigger the first test method as I'm getting:
Error creating bean with name 'batchConfiguration': Unsatisfied dependency expressed through constructor parameter 2; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'javax.sql.DataSource' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {#org.springframework.beans.factory.annotation.Qualifier(value="oerationalDataSource")}
In fact, I defined two different data sources, one for the 'operational' data and the 'app' for Spring Batch persistency:
#Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {
#Bean
#Primary
#ConfigurationProperties("app.datasource")
public DataSourceProperties defaultDataSourceProperties() {
return new DataSourceProperties();
}
#Bean
#Primary
#ConfigurationProperties("app.datasource.configuration")
public HikariDataSource defaultDataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().type(HikariDataSource.class)
.build();
}
#Bean
#ConfigurationProperties("aggr.datasource")
public DataSourceProperties oerationalDataSourceProperties() {
return new DataSourceProperties();
}
#Bean
#ConfigurationProperties("aggr.datasource.configuration")
public HikariDataSource oerationalDataSource(
#Qualifier("oerationalDataSourceProperties") DataSourceProperties oerationalDataSourceProperties) {
return oerationalDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
#Bean
public JdbcTemplate operationalJdbcTemplate(#Qualifier("oerationalDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
In #SpringBatchTest documentation it is reported that just one DataSource should be found or it should be marked as Primary:
It should be noted that JobLauncherTestUtils requires a org.springframework.batch.core.Job bean and that JobRepositoryTestUtils requires a javax.sql.DataSource bean. Since this annotation registers a JobLauncherTestUtils and a JobRepositoryTestUtils in the test context, it is expected that the test context contains a single autowire candidate for a org.springframework.batch.core.Job and a javax.sql.DataSource (either a single bean definition or one that is annotated with org.springframework.context.annotation.Primary).
But I have it. So how to fix it?
Update #1
Thanks to #Henning's tip, I've changed the annotations as follows:
#RunWith(SpringRunner.class)
#SpringBatchTest
#SpringBootTest(args={"--mode=custom", "--startDate=2022-05-31T01:00:00.000Z", "--endDate=2022-05-31T23:59:59.999Z"})
#ContextConfiguration(classes = { BatchConfiguration.class, DataSourceConfiguration.class, LocalFileManager.class, AggregatorRepository.class })
#ActiveProfiles({"integration"})
#EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
Where:
#SpringBootTest is needed to avoid 'Failed to determine a suitable driver class exception'
args is needed to provide the required parameters to the batch
But still having this exception:
Error creating bean with name 'scopedTarget.createRequestForProposalWriter' defined in BatchConfiguration: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.batch.item.file.FlatFileItemWriter]: Factory method 'createRequestForProposalWriter' threw exception; nested exception is java.lang.NullPointerException: text
raised in the implementation of:
#StepScope
#Bean
public FlatFileItemWriter<CsvData> createRequestForProposalWriter(
#Value("#{jobParameters['startDate']}") String startDate)
as the parameter 'startDate' is null.
In my naivety I assumed that I could test in isolation the classifier with something like that:
#Test
void shouldReturnProposalWriter() {
CsvData csvData = create-some-fake-data
CsvOutputClassifier csvOutputClassifier = new CsvOutputClassifier(
createRequestForProposalWriter,
createRequestForQuotationWriter);
ItemWriter itemWriter = csvOutputClassifier.classify(csvData);
some-assert-about-itemWriter-properties
}
So now the question is: how to correctly test the classifier?

You need to list DataSourceConfiguration as argument of #ContextConfiguration, i.e. your test class should start like this
#RunWith(SpringRunner.class)
#SpringBatchTest
#ContextConfiguration(classes = { BatchConfiguration.class, DataSourceConfiguration.class })
class CsvOutputClassifierTest {
...
}
The DataSourceConfiguration is currently not known within the test as you didn't declare it as part of the context or enabled classpath scanning in any form.

Related

Parameter 1 of method batchConfigurer in org.springframework.boot.autoconfigure.batch.BatchConfigurerConfiguration$JdbcBatchConfiguration required

I am using Spring Batch and using #Scheduler annotation to scheduled a job at some frequency.
From the error message it looks like Spring Boot expecting atleast Spring DataSource related entry but I dont need it, because I am not dealing with the DB as of now.
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 1 of method batchConfigurer in org.springframework.boot.autoconfigure.batch.BatchConfigurerConfiguration$JdbcBatchConfiguration required a bean of type 'javax.sql.DataSource' that could not be found.
- Bean method 'dataSource' not loaded because #ConditionalOnProperty (spring.datasource.jndi-name) did not find property 'jndi-name'
- Bean method 'dataSource' not loaded because #ConditionalOnClass did not find required class 'javax.transaction.TransactionManager'
Action:
Consider revisiting the conditions above or defining a bean of type 'javax.sql.DataSource' in your configuration.
application.properties
cron.job.expression=*/1 * * * *
Other class:
#Configuration
#EnableBatchProcessing
#Primary
public class ScheduledDomainJob {
#Autowired
private JobBuilderFactory jobBuilderFactory;
#Autowired
private StepBuilderFactory stepBuilderFactory;
#Bean
public Job scheduledJob() {
return jobBuilderFactory.get("scheduledJob").flow(step1()).end().build();
}
#Bean
public Step step1() {
return stepBuilderFactory.get("step1").<Domain, Domain>chunk(10)
.reader(reader()).writer(writer()).build();
}
#Bean
public FlatFileItemReader<Domain> reader() {
FlatFileItemReader<Domain> reader = new FlatFileItemReader<>();
reader.setResource(new ClassPathResource("csv/domain-1-03-2017.csv"));
reader.setLineMapper(new DefaultLineMapper<Domain>() {{
setLineTokenizer(new DelimitedLineTokenizer() {{
setNames(new String[]{"id", "domain"});
}});
setFieldSetMapper(new BeanWrapperFieldSetMapper<Domain>() {{
setTargetType(Domain.class);
}});
}});
return reader;
}
#Bean
public CustomWriter writer() {
CustomWriter writer = new CustomWriter();
return writer;
}
#Bean
public RunScheduler scheduler() {
RunScheduler scheduler = new RunScheduler();
return scheduler;
}
}
CustomerWriter.java
#Slf4j
public class CustomWriter implements ItemWriter<Domain> {
#Override
public void write(List<? extends Domain> items) throws Exception {
log.info("writer ....... " + items.size());
for (Domain domain : items) {
log.info(domain + "\n");
}
}
}
I hope you are using spring boot version 2. You can make some changes in the code like below.
#EnableBatchProcessing
#Primary
public class ScheduledDomainJob extends DefaultBatchConfigurer
Define the following annotation on you spring-boot application class.
#SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
You can find the more details on Spring Batch Hello world example using Spring boot
Define the following annotation on you springboot initialization class.
#EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
If that does not work, try to add the H2 or hsqldb to the maven dependencies.

How to configure EntityManagerFactoryBuilder bean when testing Spring Boot Batch application?

I have a Spring Boot Batch application that I'm writing integration tests against. However, I'm getting the following error about the EntityManagerFactoryBuilder bean missing when running a test:
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'entityManagerFactory' defined in com.example.DatabaseConfig: Unsatisfied dependency expressed through constructor argument with index 0 of type [org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder]: :
No qualifying bean of type [org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency.
Dependency annotations: {}; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type [org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:749) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
My understanding is that Spring Boot provides the EntityManagerFactoryBuilder bean on application startup. How can I have the EntityManagerFactoryBuilder provided when running tests?
Here's my test code:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = {DatabaseConfig.class, BatchConfiguration.class})
#TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
StepScopeTestExecutionListener.class })
public class StepScopeTestExecutionListenerIntegrationTests {
#Autowired
private FlatFileItemReader<Foo> reader;
#Rule
public TemporaryFolder testFolder = new TemporaryFolder();
public StepExecution getStepExection() {
StepExecution execution = MetaDataInstanceFactory.createStepExecution();
return execution;
}
#Test
public void testGoodData() throws Exception {
//some test code
}
Here's the DatabaseConfig class:
#Configuration
#EnableJpaRepositories(basePackages={"com.example.repository"},
entityManagerFactoryRef="testEntityManagerFactory",
transactionManagerRef = "testTransactionManager")
public class DatabaseConfig {
#Bean
public LocalContainerEntityManagerFactoryBean testEntityManagerFactory(EntityManagerFactoryBuilder builder) {
return builder
.dataSource(dataSource())
.packages("com.example.domain")
.persistenceUnit("testLoad")
.build();
}
#Bean
#ConfigurationProperties(prefix="spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
#Bean
public PlatformTransactionManager testTransactionManager(EntityManagerFactory testEntityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(testEntityManagerFactory);
return transactionManager;
}
}
When (integration) testing a Spring Boot application that is what you should do. The #SpringApplicationConfiguration is intended to take your application class (the one with the #SpringBootApplication annotation) as it will then be triggered to do much of the same auto configuration as a regular Spring Boot application.
You are only including 2 configuration classes and as such no auto configuration will be done.

Spring boot - Issue with multiple DataSource

I have a ReST service that needs to fetch data from two different DBs (Oracle and MySQL) and merge this data in the response.
I have below configuration.
Config for DB 1:
#Configuration
public class DbConfig_DB1{
#Bean(name="siebelDataSource")
public EmbeddedDatabase siebelDataSource(){
return new EmbeddedDatabaseBuilder().
setType(EmbeddedDatabaseType.H2).
addScript("schema.sql").
addScript("test-data.sql").
build();
}
#Autowired
#Qualifier("siebelDataSource")
#Bean(name = "siebelJdbcTemplate")
public JdbcTemplate siebelJdbcTemplate(DataSource siebelDataSource) {
return new JdbcTemplate(siebelDataSource);
}
}
Config for DB2:
#Configuration
public class DbConfig_DB2{
#Bean(name="brmDataSource")
public EmbeddedDatabase brmDataSource(){
return new EmbeddedDatabaseBuilder().
setType(EmbeddedDatabaseType.H2).
addScript("schema-1.sql").
addScript("test-data-1.sql").
build();
}
#Autowired
#Qualifier("brmDataSource")
#Bean(name = "brmJdbcTemplate")
public JdbcTemplate brmJdbcTemplate(DataSource brmDataSource) {
return new JdbcTemplate(brmDataSource);
}
}
Data Access:
#Repository
public class SiebelDataAccess {
protected final Logger log = LoggerFactory.getLogger(getClass());
#Autowired
#Qualifier("siebelJdbcTemplate")
protected JdbcTemplate jdbc;
public String getEmpName(Integer id) {
System.out.println(jdbc.queryForObject("select count(*) from employee", Integer.class));
Object[] parameters = new Object[] { id };
String name = jdbc.queryForObject(
"select name from employee where id = ?", parameters,
String.class);
return name;
}
}
I am not able to start the app as I below error:
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private javax.sql.DataSource org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration.dataSource;
nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [javax.sql.DataSource] is defined: expected single matching bean but found 2: brmDataSource,siebelDataSource
The issue is with two DataSource beans in the context. How to resolve this?
You could mark one of them as #Primary so Spring Boot auto configuration for transactions manager would know which one to pick. If you need to manage transactions with both of them then I'm afraid you would have to setup transactions management explicitly.
Please refer to the Spring Boot documentation
Creating more than one data source works the same as creating the first one. You might want to mark one of them as #Primary if you are using the default auto-configuration for JDBC or JPA (then that one will be picked up by any #Autowired injections).

Spring Data: inject 2 repositories with same name but in 2 different packages

Context
I want to use in the same Spring context two different databases that have entities that share the same name, but not the same structure. I rely on Spring Data MongoDB and JPA/JDBC. I have two packages, containing among others the following files:
com.bar.entity
Car.class
com.bar.repository
CarRepository.class
RepoBarMarker.class
com.bar.config
MongoConfiguration.class
com.foo.entity
Car.class
com.foo.repository
CarRepository.class
RepoFooMarker.class
com.foo.config
JPAConfiguration.class
SpecEntityManagerFactory.class
The content of each Car.class is different, I cannot reuse them. bar uses Spring-Mongo and foo uses Spring-JPA, and repositories are initialised via #EnableMongoRepositories and #EnableJpaRepositories annotations. When in one of my application component I try to access the foo version of the repository:
#Resource
private com.foo.repository.CarRepository carRepository;
I have the following exception when the class containing the #Resource field is created:
Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'carRepository' must be of type [com.foo.repository.CarRepository], but was actually of type [com.sun.proxy.$Proxy31]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:374)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:198)
at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.autowireResource(CommonAnnotationBeanPostProcessor.java:446)
at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.getResource(CommonAnnotationBeanPostProcessor.java:420)
at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor$ResourceElement.getResourceToInject(CommonAnnotationBeanPostProcessor.java:545)
at org.springframework.beans.factory.annotation.InjectionMetadata$InjectedElement.inject(InjectionMetadata.java:155)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:87)
at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessPropertyValues(CommonAnnotationBeanPostProcessor.java:305)
... 26 more
It appears that Spring tries to convert a bar repository to a foo repository, instead of creating a new bean, as in the same stack I also have the following exception:
Caused by: java.lang.IllegalStateException: Cannot convert value of type [com.sun.proxy.$Proxy31 implementing com.bar.repository.CarRepository,org.springframework.data.repository.Repository,org.springframework.aop.SpringProxy,org.springframework.aop.framework.Advised] to required type [com.foo.repository.CarRepository]: no matching editors or conversion strategy found
at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:267)
at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:93)
at org.springframework.beans.TypeConverterSupport.doConvert(TypeConverterSupport.java:64)
... 35 more
If I try instead to autowire the repository:
#Autowire
private com.foo.repository.CarRepository carRepository;
I get the following exception:
Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.foo.CarRepository com.shell.ShellApp.carRepository; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.foo.CarRepository] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {#org.springframework.beans.factory.annotation.Autowired(required=true)}
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:509)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:87)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:290)
... 26 more
Spring-data configuration
In foo (JPA) package, JPAConfigration.class:
#Configuration
#EnableJpaRepositories(basePackageClasses = RepoFooMarker.class)
public class JPAConfiguration {
#Autowired
public DataSource dataSource;
#Autowired
public EntityManagerFactory entityManagerFactory;
#Bean
public EntityManager entityManager(final EntityManagerFactory entityManagerFactory) {
return entityManagerFactory.createEntityManager();
}
#Bean
public Session session(final EntityManager entityManager)
{
return entityManager.unwrap(Session.class);
}
#Bean
public PlatformTransactionManager transactionManager() throws SQLException {
final JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
#Bean
public HibernateExceptionTranslator hibernateExceptionTranslator() {
return new HibernateExceptionTranslator();
}
}
SpecEntityManagerFactory.class:
#Configuration
public class SpecEntityManagerFactory {
#Bean
public EntityManagerFactory entityManagerFactory(final DataSource dataSource) throws SQLException {
final HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(false);
vendorAdapter.setDatabase(Database.POSTGRESQL);
final LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan("com.foo.entity");
factory.setJpaProperties(getHibernateProperties());
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
private Properties getHibernateProperties()
{
final Properties hibernateProperties = new Properties();
hibernateProperties.setProperty("hibernate.temp.use_jdbc_metadata_defaults", "false");
return hibernateProperties;
}
}
In bar (MongoDB) package, MongoConfiguration.class:
#Configuration
#EnableMongoRepositories(basePackageClasses = RepoBarMarker.class)
public class MongoConfiguration extends AbstractRepoConfig {
#Override
#Bean
public MongoOperations mongoTemplate() {
final MongoClient mongo = this.getMongoClient();
final MongoClientURI mongoUri = this.getMongoClientUri();
final MongoTemplate mongoTemplate = new MongoTemplate(mongo, mongoUri.getDatabase());
mongoTemplate.setReadPreference(ReadPreference.secondaryPreferred());
mongoTemplate.setWriteConcern(WriteConcern.UNACKNOWLEDGED);
return mongoTemplate;
}
}
Question
If I change in foo repository the entity name to CarFoo.class and the repository to CarFooRepository.class, then everything works. But is there away to avoid renaming them and still have a real wiring per type, instead of name (as it is what seems to be done here), for Spring Data repositories?
In your case, you can use
#Repository("fooCarRepository")
on the interface declaration of
com.foo.repository.CarRepository
Although when using Spring Data #Repository is not generally needed on the interface, however in your case you need to supply it. That's because you need to make Spring register the implementation of the bean with a custom name (in this case fooCarRepository) in order to avoid the name collision.

Spring JavaConfig + JAX-WS Client

I need to create a webservice client to get sportsdata.
But I'm getting an exception when trying to #Autowired sportsdata.
Exception:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [de.openligadb.schema.SportsdataSoap] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {#org.springframework.beans.factory.annotation.Autowired(required=true)}
JavaConfig:
#Configuration
#ComponentScan(basePackages = "com.example", excludeFilters = { #Filter(Configuration.class) })
public class MainConfig {
private #Value("${openligadb.wsdlDocumentUrl}") String wsdlDocumentUrl;
private #Value("${openligadb.endpointAddress}") String endpointAddress;
private #Value("${openligadb.namespaceUri}") String namespaceUri;
private #Value("${openligadb.serviceName}") String serviceName;
#Bean
public JaxWsPortProxyFactoryBean sportsdata() throws MalformedURLException {
JaxWsPortProxyFactoryBean ret = new JaxWsPortProxyFactoryBean();
ret.setWsdlDocumentUrl(new URL(wsdlDocumentUrl));
ret.setServiceInterface(SportsdataSoap.class);
ret.setEndpointAddress(endpointAddress);
ret.setNamespaceUri(namespaceUri);
ret.setServiceName(serviceName);
return ret;
}
#Bean
public static PropertySourcesPlaceholderConfigurer properties() {
PropertySourcesPlaceholderConfigurer ret = new PropertySourcesPlaceholderConfigurer();
ret.setLocation(new ClassPathResource("application.properties"));
return ret;
}
}
And yes: I know of #PropertySource but I need to create a bean for it to use it later in my Controller as well.
It's a FactoryBean interoperability problem with #Configuration. Take a look at this answer for details.
The short version is to add a bean explicitly to your configuration.
#Bean
public SportsdataSoap sportsdataSoap() throws ... {
return (SportsdataSoap) sportsdata().getObject();
}

Resources