Problem initializing Reactor Redis with Spring Boot - spring

I am trying to get Reactive Redis working with existing application that has normal syncronous Redis implementation running. I can't change all the implementation at once, so I'm trying to get them both work at the same time.
This is what I have previously.
#Configuration
#ConfigurationProperties(prefix = "app.redis")
public class MemoryCacheConfiguration {
private String endpoint;
private int port;
#Bean
JedisConnectionFactory jedisConnectionFactory() {
return new JedisConnectionFactory(new RedisStandaloneConfiguration(this.endpoint, this.port));
}
#Bean
public RedisTemplate<String, String> redisTemplate() {
final RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericToStringSerializer<>(Serializable.class));
template.setValueSerializer(new GenericToStringSerializer<>(Serializable.class));
return template;
}
}
This is what I'm adding to the file
#Bean
public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {
return new LettuceConnectionFactory(endpoint, port);
}
#Bean
public ReactiveRedisTemplate<String, String> reactiveRedisTemplate(ReactiveRedisConnectionFactory factory) {
RedisSerializationContext.RedisSerializationContextBuilder<String, String> builder =
RedisSerializationContext.newSerializationContext(new StringRedisSerializer());
RedisSerializationContext<String, String> context =
builder.value(new StringRedisSerializer()).build();
return new ReactiveRedisTemplate<>(factory, context);
}
If I have ReactiveRedisTemplate in the file defined, I'm getting following error about the duplicate
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.data.redis.core.ReactiveRedisTemplate<java.lang.String, java.lang.String>' available: expected single matching bean but found 2: reactiveRedisTemplate,reactiveStringRedisTemplate
However, if I remove the definition so that it is blank, I'm getting the following.
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.data.redis.core.ReactiveRedisTemplate<java.lang.String, java.lang.String>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
What might be the reason for this, as I haven't defined any reactiveStringRedisTemplate and there is no reference to it in my project, but if I remove my custom one, then it doesn't find any Bean required.
I am using Spring Boot version 2.7.3.

It helped that naming the bean properly where it was used so that it detected the correct template.
As I had this:
private final ReactiveRedisTemplate<String, String> redisReactorTemplate;
#Autowired
public RedisService(ReactiveRedisTemplate<String, String> redisReactorTemplate) {
this.redisReactorTemplate = redisReactorTemplate;
}
Instead of this:
private final ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
#Autowired
public RedisService(ReactiveRedisTemplate<String, String> reactiveRedisTemplate) {
this.redisReactorTemplate = reactiveRedisTemplate;
}
I don't know the reason it has two possible beans at that point though (expected single matching bean but found 2: reactiveRedisTemplate,reactiveStringRedisTemplate).

Related

Spring Batch test case with multiple data sources

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.

Getting qualifier names from initialized bean objects

I have two beans of the same type;
#Bean
public RestTemplate jsonTemplate() {
return new RestTemplate();
}
#Bean
public RestTemplate xmlTemplate() {
return new RestTemplate();
}
And I autowire both beans into a list as follows;
#Autowired
private List<RestTemplate> templates;
The list templates will have both beans inside with size=2.
From this list, how can I get their names (["jsonTemplate", "xmlTemplate"])?
It was really simple...
Just doing;
#Autowired
private Map<String, RestTemplate> templates;
will let Spring to insert the names as keys and the beans themselves as the values in
the map.
It seems Spring just stops keeping track of the naming after the injection. So I don't know if there is any other way (or, if even possible, simpler way) than this?
You could use map of beans:
#Bean
public Map<String, RestTemplate> templateMap(RestTemplate jsonTemplate, RestTemplate xmlTemplate) {
Map<String, RestTemplate> map = new HashgMap<>();
map.put("jsonTemplate", jsonTemplate);
map.put("xmlTemplate", xmlTemplate);
return map;
}
#Autowired
private Map<String, RestTemplate> templates;

Collisions may occur when using Spring #Cacheable and SimpleKeyGenerator

When I use #Cacheable and call different method with same parameter, it generated a same key.
SimpleKeyGenerator generated key without cache names.
I use spring-boot 1.3.2 with spring 4.2.4.
Here is a sample:
#Component
public static class CacheableTestClass {
#Cacheable(cacheNames = "test-cacheproxy-echo1")
public String echo1(String text) {
return text;
}
#Cacheable(cacheNames = "test-cacheproxy-echo2")
public String echo2(String text) {
return "Another " + text;
}
}
And run a test:
assertEquals("OK", cacheableTestClass.echo1("OK"));
assertEquals("Another OK", cacheableTestClass.echo2("OK")); // Failure: expected 'Another OK', actual 'OK'.
So, is there a way to resolve this issue?
Thanks a lot.
Update
Here is my CacheManager configuration.
#Bean
#ConditionalOnMissingBean(name = "cacheRedisTemplate")
public RedisTemplate<Object, Object> cacheRedisTemplate(
RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(template.getKeySerializer());
return template;
}
#Bean
public RedisCacheManager cacheManager(#Qualifier("cacheRedisTemplate") RedisTemplate<Object, Object> cacheRedisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(cacheRedisTemplate);
cacheManager.setDefaultExpiration(
redisCacheProperties().getDefaultExpiration());
cacheManager.setExpires(redisCacheProperties().getExpires());
return cacheManager;
}
This has nothing to do with SimpleKeyGenerator but this is a redis-specific issue that does not use the name of the cache as a discriminant for the key it uses to store the value.
You need to invoke setUsePrefix(true) on your RedisCacheManager. This is what Spring Boot does when it auto-configures the cache manager for you. Note that it should have been the default and we're discussing how we can improve the out-of-the-box experience in a future release

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