Collisions may occur when using Spring #Cacheable and SimpleKeyGenerator - spring

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

Related

Conditionally override default datasource in springboot

I am trying to configure the spring data source programmatically. The idea is to override the default data source if tenant information is specified or else use the default data source for backward compatibility. I tried coming up with something like :
#Component
#AllArgsConstructor
public class TenancyDataSource {
private TenantConfigs tenantConfigs;
private DataSource dataSource;
#Bean
public DataSource dataSource() {
String tenant = System.getenv(AppConstants.TENANT);
if (!ObjectUtils.isEmpty(tenant)) {
TenantConfigs config =
tenantConfigs
.getDataSources()
.stream()
.filter(TenantConfig -> tenantConfig.getTenantName().equals(tenant))
.findFirst()
.orElse(null);
if (!ObjectUtils.isEmpty(config)) {
return DataSourceBuilder.create()
.url(config.getUrl())
.password(config.getPassword())
.username(config.getUsername())
.build();
}
}
return dataSource;
}
}
But this does not work due to circular dependency.
What would be the best way to implement it ?
SpringBoot treats environment variables as properties by default. So using #ConditionalOnProperty with the "tenant" property worked
#Bean
#ConditionalOnProperty(name = AppConstants.TENANT)
public DataSource dataSource() {
return DataSourceBuilder.create()
.url(config.getUrl())
.password(config.getPassword())
.username(config.getUsername())
.build();
}

Redis cache docker container with spring boot not working on my local Machine

I am using redis cache on my local Machine as a docker image. I have enabled the cache for one of my method using the cacheable anothion. Application is not able to cache the same when i am using on aws it is working instead of localhost
public class RedisConfig {
#Autowired
private JedisConnectionFactory jedisConnectionFactory;
#Bean
public RedisTemplate<Object, Object> redisTemplate() {
System.out.println("localhost")
System.out.println("6379");
jedisConnectionFactory.getHostName();
jedisConnectionFactory.getPort();
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory);
template.setValueSerializer(new GenericToStringSerializer<Object>(Object.class));
return template;
}
#Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
// Number of seconds before expiration. Defaults to unlimited (0)
cacheManager.setDefaultExpiration(60);
cacheManager.setUsePrefix(true);
return cacheManager;
}
}
#Cacheable(value="sgcode" , cacheManager ="cacheManager")
public String getSegmentCode(String aname ) {
Logger.info("code ", "##### SEGMENT METHOD CALLED ##### {}", aname);
return segmentCodeMap.get(aname);
}
Logger line will be printed only once after that it should fetch from cache.
Finally after lot of struggle found the root cause. use the enablecaching anotation in application class

spring-data-redis Jackson serialization

I'm attempting to use the Jackson serialization feature of spring-data-redis. I am building a ObjectMapper and using the GenericJackson2JsonRedisSerializer as the serializer for the redisTemplate:
#Configuration
public class SampleModule {
#Bean
public ObjectMapper objectMapper() {
return Jackson2ObjectMapperBuilder.json()
.serializationInclusion(JsonInclude.Include.NON_NULL) // Don’t include null values
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) //ISODate
.build();
}
#Bean
public RedisTemplate getRedisTemplate(ObjectMapper objectMapper, RedisConnectionFactory redisConnectionFactory){
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setDefaultSerializer(new GenericJackson2JsonRedisSerializer(objectMapper));
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
I have a SampleBean I am attempting to save:
#RedisHash("sampleBean")
public class SampleBean {
#Id
String id;
String value;
Date date;
public SampleBean(String value, Date date) {
this.value = value;
this.date = date;
}
}
And a repository for that bean:
public interface SampleBeanRepository extends CrudRepository {
}
I am then trying to write the bean to Redis:
ConfigurableApplicationContext context = SpringApplication.run(SampleRedisApplication.class, args);
SampleBean helloSampleBean = new SampleBean("hello", new Date());
ObjectMapper objectMapper = context.getBean(ObjectMapper.class);
logger.info("Expecting date to be written as: " + objectMapper.writeValueAsString(helloSampleBean.date));
SampleBeanRepository repository = context.getBean(SampleBeanRepository.class);
repository.save(helloSampleBean);
context.close();
I expect the redisTemplate to use the Serializer to write the Date inside of the SampleBean as a Timestamp, however it is written as a long.
The relevant spring-data-redis reference: http://docs.spring.io/spring-data/data-redis/docs/current/reference/html/#redis:serializer
Full code sample: https://github.com/bandyguy/spring-redis-jackson-sample-broken
The serializer/mapper used by the template does not affect the one used by the repository since the repository directly operates upon the byte[] using Converter implementations for reading/writing data based on domain type metadata.
Please refer to the Object to Hash Mapping section of the reference manual for guidance how to write and register a custom Converter.
Have you tried disable serialization feature SerializationFeature.WRITE_DATES_AS_TIMESTAMPS?

Upgrade from Spring Boot 1.3 to Spring Boot 1.4 and Pageable is not working as expected.

I am converting an existing Spring Boot application from 1.3.6 to 1.4.1. I would like to have a default page size for repository and controller responses of 25. I am not getting the expected behavior in either case. For repository methods I am getting a page size of 20. For controllers I am getting 0 for the page size.
I added a new configuration class to define the default page size. I found this code snippet in another article. The debug message does get printed out.
#Configuration
public class RestConfigurationAdapter extends WebMvcConfigurerAdapter {
private static final int DEFAULT_PAGE_SIZE = 25;
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
System.out.println("DEBUG: AddArguments----");
PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver();
resolver.setFallbackPageable(new PageRequest(0, DEFAULT_PAGE_SIZE));
argumentResolvers.add(resolver);
super.addArgumentResolvers(argumentResolvers);
}
}
In a custom controller I would like to have a default pageable populated with a size of 25. However the pageable object is null in this controller. In 1.3.x the pageable object worked as expected.
public class BatchManagerController
{
#Autowired
private BatchRepository batchRepository;
#Autowired
private PagedResourcesAssembler pagedResourcesAssembler;
#Transactional(readOnly = true)
#RequestMapping(value = "/search/managerBatchView", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
#PreAuthorize("hasRole(T(com.nextgearcapital.tms.api.util.AuthorityEnum).MANAGER)")
public ResponseEntity<?> getManagerBatchListView(BatchListSearchRequest requestDTO, Pageable pageable, PersistentEntityResourceAssembler myAssembler)
{
System.out.println("DEBUG1:---------- " + pageable);
Page<Batch> batchPage = batchRepository.findBatchesForManager(requestDTO, pageable);
PagedResources<VaultResource> pagedResources = pagedResourcesAssembler.toResource(batchPage, myAssembler);
return new ResponseEntity<>(pagedResources, HttpStatus.OK);
}
}
When calling SDR Repository methods with a pageable parameter, the parameter works correctly, but it has a default page size of 20, rather than 25.
I would appreciate any help and advise in getting the correct configuration for pagination.
You probably have 2 solutions
Register the PageableHandlerMethodArgumentResolver as an #Bean which will disable the auto configuration for Spring Data Web.
Create a BeanPostProcessor to do additional configuration on the existing PageableHandlerMethodArgumentResolver.
Using #Bean
#Configuration
public class RestConfigurationAdapter extends WebMvcConfigurerAdapter {
private static final int DEFAULT_PAGE_SIZE = 25;
#Bean
public PageableHandlerMethodArgumentResolver pageableResolver() {
PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver();
resolver.setFallbackPageable(new PageRequest(0, DEFAULT_PAGE_SIZE));
return resolver;
}
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
System.out.println("DEBUG: AddArguments----");
argumentResolvers.add(pageableResolver());
}
}
Drawback is that it will disable the autoconfiguration for Spring Data Web, so you might miss some things.
Using a BeanPostProcessor.
#Bean
public BeanPostProcessor pageableProcessor() {
private static final int DEFAULT_PAGE_SIZE = 25;
return new BeanPostProcessor() {
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof PageableHandlerMethodArgumentResolver) {
((PageableHandlerMethodArgumentResolver) bean).setFallbackPageable(new PageRequest(0, DEFAULT_PAGE_SIZE));
}
return bean;
}
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
}
Drawback is that it is a little more complex as registering your own PageableHandlerMethodArgumentResolver instance as a bean. Advantage however is that you can simply use this to add additional configuration to existing beans and leave the auto configuration in tact.
Starting in spring-data-commons version 2.0, there is are 2 new classes that might make this kind of thing easier:
SortHandlerMethodArgumentResolverCustomizer
PageableHandlerMethodArgumentResolverCustomizer
Unfortunately that's not the version that ships with the current version (1.5.9) of Spring Boot, so replace at your own risk.
#Bean
PageableHandlerMethodArgumentResolverCustomizer pagingCustomizer() {
// p is PageableHandlerMethodArgumentResolver
return p -> p.setMaxPageSize(25);
}
In this case, one would probably call resolveArgument to manipulate it.
That said, I'm not sure spring-data-rest would use that config. There is a HateoasPageableHandlerMethodArgumentResolver which seems more likely that source of what I would think SDR would use. If that's the case, the BeanPostProcessor #M. Deinum suggested is probably your best option.
Spring Data Web Support

spring-boot-starter-jta-atomikos and spring-boot-starter-batch

Is it possible to use both these starters in a single application?
I want to load records from a CSV file into a database table. The Spring Batch tables are stored in a different database, so I assume I need to use JTA to handle the transaction.
Whenever I add #EnableBatchProcessing to my #Configuration class it configures a PlatformTransactionManager, which stops this being auto-configured by Atomikos.
Are there any spring boot + batch + jta samples out there that show how to do this?
Many Thanks,
James
I just went through this and I found something that seems to work. As you note, #EnableBatchProcessing causes a DataSourceTransactionManager to be created, which messes up everything. I'm using modular=true in #EnableBatchProcessing, so the ModularBatchConfiguration class is activated.
What I did was to stop using #EnableBatchProcessing and instead copy the entire ModularBatchConfiguration class into my project. Then I commented out the transactionManager() method, since the Atomikos configuration creates the JtaTransactionManager. I also had to override the jobRepository() method, because that was hardcoded to use the DataSourceTransactionManager created inside DefaultBatchConfiguration.
I also had to explicitly import the JtaAutoConfiguration class. This wires everything up correctly (according to the Actuator's "beans" endpoint - thank god for that). But when you run it the transaction manager throws an exception because something somewhere sets an explicit transaction isolation level. So I also wrote a BeanPostProcessor to find the transaction manager and call txnMgr.setAllowCustomIsolationLevels(true);
Now everything works, but while the job is running, I cannot fetch the current data from batch_step_execution table using JdbcTemplate, even though I can see the data in SQLYog. This must have something to do with transaction isolation, but I haven't been able to understand it yet.
Here is what I have for my configuration class, copied from Spring and modified as noted above. PS, I have my DataSource that points to the database with the batch tables annotated as #Primary. Also, I changed my DataSource beans to be instances of org.apache.tomcat.jdbc.pool.XADataSource; I'm not sure if that's necessary.
#Configuration
#Import(ScopeConfiguration.class)
public class ModularJtaBatchConfiguration implements ImportAware
{
#Autowired(required = false)
private Collection<DataSource> dataSources;
private BatchConfigurer configurer;
#Autowired
private ApplicationContext context;
#Autowired(required = false)
private Collection<BatchConfigurer> configurers;
private AutomaticJobRegistrar registrar = new AutomaticJobRegistrar();
#Bean
public JobRepository jobRepository(DataSource batchDataSource, JtaTransactionManager jtaTransactionManager) throws Exception
{
JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
factory.setDataSource(batchDataSource);
factory.setTransactionManager(jtaTransactionManager);
factory.afterPropertiesSet();
return factory.getObject();
}
#Bean
public JobLauncher jobLauncher() throws Exception {
return getConfigurer(configurers).getJobLauncher();
}
// #Bean
// public PlatformTransactionManager transactionManager() throws Exception {
// return getConfigurer(configurers).getTransactionManager();
// }
#Bean
public JobExplorer jobExplorer() throws Exception {
return getConfigurer(configurers).getJobExplorer();
}
#Bean
public AutomaticJobRegistrar jobRegistrar() throws Exception {
registrar.setJobLoader(new DefaultJobLoader(jobRegistry()));
for (ApplicationContextFactory factory : context.getBeansOfType(ApplicationContextFactory.class).values()) {
registrar.addApplicationContextFactory(factory);
}
return registrar;
}
#Bean
public JobBuilderFactory jobBuilders(JobRepository jobRepository) throws Exception {
return new JobBuilderFactory(jobRepository);
}
#Bean
// hopefully this will autowire the Atomikos JTA txn manager
public StepBuilderFactory stepBuilders(JobRepository jobRepository, JtaTransactionManager ptm) throws Exception {
return new StepBuilderFactory(jobRepository, ptm);
}
#Bean
public JobRegistry jobRegistry() throws Exception {
return new MapJobRegistry();
}
#Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
AnnotationAttributes enabled = AnnotationAttributes.fromMap(importMetadata.getAnnotationAttributes(
EnableBatchProcessing.class.getName(), false));
Assert.notNull(enabled,
"#EnableBatchProcessing is not present on importing class " + importMetadata.getClassName());
}
protected BatchConfigurer getConfigurer(Collection<BatchConfigurer> configurers) throws Exception {
if (this.configurer != null) {
return this.configurer;
}
if (configurers == null || configurers.isEmpty()) {
if (dataSources == null || dataSources.isEmpty()) {
throw new UnsupportedOperationException("You are screwed");
} else if(dataSources != null && dataSources.size() == 1) {
DataSource dataSource = dataSources.iterator().next();
DefaultBatchConfigurer configurer = new DefaultBatchConfigurer(dataSource);
configurer.initialize();
this.configurer = configurer;
return configurer;
} else {
throw new IllegalStateException("To use the default BatchConfigurer the context must contain no more than" +
"one DataSource, found " + dataSources.size());
}
}
if (configurers.size() > 1) {
throw new IllegalStateException(
"To use a custom BatchConfigurer the context must contain precisely one, found "
+ configurers.size());
}
this.configurer = configurers.iterator().next();
return this.configurer;
}
}
#Configuration
class ScopeConfiguration {
private StepScope stepScope = new StepScope();
private JobScope jobScope = new JobScope();
#Bean
public StepScope stepScope() {
stepScope.setAutoProxy(false);
return stepScope;
}
#Bean
public JobScope jobScope() {
jobScope.setAutoProxy(false);
return jobScope;
}
}
I found a solution where I was able to keep #EnableBatchProcessing but had to implement BatchConfigurer and atomikos beans, see my full answer in this so answer.

Resources