Spring Cache Caffeine bulk retrieval - spring

Is it possible to use Caffeine's CacheLoader::loadAll with #Cacheable annotated method with collection parameter, like
#Cacheable(cacheNames = "exampleCache", cacheManager="exampleCacheManager", keyGenerator = "complexKeyGenerator")
List<String> getItems(List<String> keys, String commonForEveryKey) {
return ...
}
#Component
class ComplexKeyGenerator implements KeyGenerator {
#Override
public Object generate(Object target, Method method, Object... params) {
return ((List<String>)params[0]).stream()
.map(item -> new ComplexKey(item, (String) params[1]))
.collect(Collectors.toList());
}
}
#Data
#AllArgsConstructor
class ComplexKey {
String key;
String commonGuy;
}
class CustomCacheLoader implements CacheLoader<ComplexKey, String> {
#Override
public #Nullable String load(#NonNull ComplexKey key) throws Exception {
return loadAll(List.of(key)).get(key);
}
#Override
public #NonNull Map<#NonNull ComplexKey, #NonNull String> loadAll(#NonNull Iterable<? extends #NonNull ComplexKey> keys)
throws Exception {
return ...
}
}
#Bean
CacheManager exampleCacheManager(LoadingCache exampleCache) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.registerCustomCache("exampleCache", exampleCache());
return cacheManager;
}
#Bean
Cache<Object, Object> exampleCache() {
return Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.HOURS)
.recordStats()
.build(new CustomCacheLoader());
}
Looks like Spring Cache invokes CustomCacheLoader::load instead of CustomCacheLoader::loadAll and fails on ClassCastException since it cannot cast collection of keys into single key.
What else should I configure to make it work?

Unfortunately Spring doesn't support retrieving of a collection of cached items by a collection of keys via #Cacheable mechanism.
Here's an issue on that: https://github.com/spring-projects/spring-framework/issues/23221
One option to achieve this is to use a custom library ( https://github.com/qaware/collection-cacheable-for-spring) that provides a #CollectionCacheable annotation:
#CollectionCacheable(cacheNames = "myCache")
Map<Long, MyEntity> findByIds(Collection<Long> ids) {
// do efficient batch retrieve of many MyEntity's and build result map
}
If you'd really like to stick with the code you have you could generalize it to something similar:
#Component
class ComplexKeyGenerator implements KeyGenerator {
#Override
public Object generate(Object target, Method method, Object... params) {
if (params.length < 2 || !(params[0] instanceof Collection && params[1] instanceof String)) {
return SimpleKeyGenerator.generateKey(params);
}
return ((Collection<String>) params[0]).stream()
.map(item -> new ComplexKey(item, (String) params[1]))
.collect(Collectors.toList());
}
}
class CustomCacheLoader implements CacheLoader<Object, Object> {
#Override
public Object load(Object key) throws Exception {
final Collection<Object> keys = (key instanceof Collection) ?
((Collection<Object>) key) : Collections.singletonList(key);
final Collection<Object> values = new ArrayList<>(loadAll(keys).values());
return values;
}
#Override
public Map<Object, Object> loadAll(Iterable<?> keys) throws Exception {
...
}
}

Related

How to synchronization get mono object in nonblockthread?

Summarize the problem
I want to use orika to map the java bean and I create a orika filter the code is
#Slf4j
#Configuration
public class OrikaMapperConfig implements ApplicationContextAware {
#Bean
public MapperFactory init(ApplicationContext applicationContext) {
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
mapperFactory.registerFilter(new CustomFilter<Object, Object>() {
#Override
public boolean filtersSource() {
return true;
}
#Override
public boolean filtersDestination() {
return true;
}
#Override
public <S, D> boolean shouldMap(Type<S> sourceType, String sourceName, S source, Type<D> destType, String destName, D dest, MappingContext mappingContext) {
return true;
}
#Override
public <D> D filterDestination(D destinationValue, Type<?> sourceType, String sourceName, Type<D> destType, String destName, MappingContext mappingContext) {
//how to get login User here with springfluxsecurity?
//I want to modify the destinationValue dynamic with login user role
return destinationValue;
}
#Override
public <S> S filterSource(S sourceValue, Type<S> sourceType, String sourceName, Type<?> destType, String destName, MappingContext mappingContext) {
return sourceValue;
}
});
Map<String, IMapper> mappers = applicationContext.getBeansOfType(IMapper.class);
mappers.forEach((key, iMapper) -> iMapper.register(mapperFactory));
return mapperFactory;
}
#Bean
public MapperFacade mapperFacade(MapperFactory mapperFactory) {
return mapperFactory.getMapperFacade();
}
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
init(applicationContext);
}
}
Describe what you've tried
in method of filterDestination I try to use
ReactiveSecurityContextHolder.getContext().map(context -> (User) context.getAuthentication().getPrincipal()).block()
to get login user but I get the error
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-tcp-nio-4
Question:
I want to know how to fix it ,thanks for your reply.
.block() calls were deprecated some time ago. As far as your .map method needs to work with the User itself and not with Mono<User>, the operator you should apply instead is .flatMap. This will unwrap your user from the Mono and will be trigger when the User "promise" is resolved.

spring-data-rest: Validator not being invoked

I am using springboot 2.0.1.RELEASE with spring-data-rest and followed the workaround mentioned here and my Validator is still not being invoked. Here are the details:
ValidatorRegistrar: Workaround for a bug
#Configuration
public class ValidatorRegistrar implements InitializingBean {
private static final List<String> EVENTS;
static {
List<String> events = new ArrayList<String>();
events.add("beforeCreate");
events.add("afterCreate");
events.add("beforeSave");
events.add("afterSave");
events.add("beforeLinkSave");
events.add("afterLinkSave");
events.add("beforeDelete");
events.add("afterDelete");
EVENTS = Collections.unmodifiableList(events);
}
#Autowired
ListableBeanFactory beanFactory;
#Autowired
ValidatingRepositoryEventListener validatingRepositoryEventListener;
#Override
public void afterPropertiesSet() throws Exception {
Map<String, Validator> validators = beanFactory.getBeansOfType(Validator.class);
for (Map.Entry<String, Validator> entry : validators.entrySet()) {
EVENTS.stream().filter(p -> entry.getKey().startsWith(p)).findFirst()
.ifPresent(p -> validatingRepositoryEventListener.addValidator(p, entry.getValue()));
}
}
}
Validator class:
#Component("beforeSaveBidValidator")
public class BeforeSaveBidValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return Bid.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
Bid bid = (Bid)target;
if (!bid.getAddendaAcknowledged()) {
errors.rejectValue("addendaAcknowledged",
"addendaAcknowledged is not true");
}
}
}
Custom RestController for Bids:
#RestController
#RequestMapping(path = "/bids")
public class BidController {
private BidRepository bidRepository;
#Autowired
public BidController(
BidRepository bidRepository) {
this.bidRepository = bidRepository;
}
#PutMapping("{id}")
public Bid update(#RequestBody #Valid Bid bid) {
return bidRepository.save(bid);
}
}
Rest Client Test Code:
Bid bid = new Bid()
...
bid.setAddendaAcknowledged(false)
Map<String, String> uriVariables = new HashMap<String, String>()
uriVariables.put("id", bid.id)
HttpHeaders headers = new HttpHeaders()
headers.setContentType(MediaType.APPLICATION_JSON)
HttpEntity<Bid> entity = new HttpEntity<>(bid, headers)
ResponseEntity<String> response = restTemplate.exchange(
"/bids/{id}", HttpMethod.PUT, entity, Bid.class, bid.id)
// Expected: response.statusCode == HttpStatus.BAD_REQUEST
// Found: response.statusCode == HttpStatus.OK
// Debugger showed that Validator was never invoked.
Any idea what I am missing?
You are trying to use your validator with custom controller, not SDR controller. In this case you can just add it to your controller with #InitBinder annotation:
#RestController
#RequestMapping("/bids")
public class BidController {
//...
#InitBinder("bid") // add this parameter to apply this binder only to request parameters with this name
protected void bidValidator(WebDataBinder binder) {
binder.addValidators(new BidValidator());
}
#PutMapping("/{id}")
public Bid update(#RequestBody #Valid Bid bid) {
return bidRepository.save(bid);
}
}
#Component annotation on your validator is not necessary as well as ValidatorRegistrar class.
How to use validators with SDR controllers you can read in my another answer.

How can I moderate #PathVariable?

I have following methods.
public ResponseEntity some(#PathVariable("id") final String id_) {
final Long id = ((Supplier<Long>) () -> {
try {
if (id_ == null || id_.equals("dontcare")) {
return null;
}
return Long.parseLong(id_);
} catch (final NumberFormatException nfe) {
throw new RuntimeException(nfe);
}
}).get();
}
As you can assume I want my parameter value \d+ or dontcare.
How or What can I use for do following?
public ResponseEntity some(
#Dontcare #PathVariable("id") final Long id) {
}
I found WebArgumentHandler, HandlerMethodArgumentResolver, and PathVariableMethodArgumentResolver, but I can't find any good example.
Please help me.
UPDATE
I prepared following class.
public class DontcarePathVariableResolver
extends PathVariableMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
final boolean flag = parameter.getMethodAnnotation(
DontcarePathVariable.class) != null;
logger.info("flag: {}", flag);
return flag;
}
#Override
protected Object resolveName(String name, MethodParameter parameter,
NativeWebRequest request)
throws Exception {
#SuppressWarnings({"unchecked"})
final Map<String, String> templateVariables
= (Map<String, String>) request.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE,
RequestAttributes.SCOPE_REQUEST);
final String pathValue = templateVariables.get(name);
return new DontcareConverter().convert(pathValue);
}
}
And added it.
#Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new DontcarePathVariableMethodArgumentResolver());
logger.info("resolver added");
//super.addArgumentResolvers(resolvers);
}
}
But it did come to play.
I think a default handler for Long type is already in play.

How to write custom interceptor for spring cache(#cachable)

I am caching data using spring cache. Now i want to encrypt few data before writing into cache and decrypt data while reading. So is there any way i can write custom interceptor/aop for #cachable annotation
Instead of using AOP you can simply use a decorator for your Cache and CacheResolver.
public class EncodingCacheResolver implements CacheResolver {
private final CacheResolver delegate;
public EncodingCacheResolver(CacheResolver delegate) {
this.delegate=delegate;
}
#Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
Collection<Cache> result = delegate.resolveCaches(context);
return result.stream().map(EncodingCache::new).collect(Collectors.toLlist());
}
}
The cache implementation
public class EncodingCache implements Cache {
private final Cache delegate;
public EncodingCache(Cache delegate) {
this.delegate=delegate;
}
public String getName() {
return delegate.getName();
}
public Object getNativeCache() {
return delegate.getNativeCache();
}
public void evict(Object key) {
delegate.evict(key)
}
public void put(Object key, Object value) {
Object encodedValue = encode(value);
this.delegate.put(key, encodedValue);
}
public <T> T get(Object key, Class<T> type) {
Object encodedValue = delegate.get(key, type);
return decode(encodedValue);
}
// Other Cache methods omitted but the pattern is the same
private Object encode(Object value) {
// encoding logic here
}
private Object decode(Object value) {
// decoding logic here
}
}
Then some configuration
#Configuration
#EnableCache
public void CacheConfiguration {
#Bean
public CacheResolver cacheResolver(CacheManager cacheManager) {
return new EncodingCacheResolver(SimpleCache.of(cacheManager));
}
}
Haven't tested the implementation, typed it from the top of my head. But this should more or less be what you need. You don't really need AOP for this.

How to replace default SortArgumentResolver

I need to add private static final Sort sortById = new Sort(Sort.Direction.DESC, ID); to each Pageable. I guess, the best way to do that is to create decorator/adapter for SortArgumentResolver.
I've created class:
public class IdSortArgumentResolver implements SortArgumentResolver {
private static final String ID = "id";
private static final Sort sortById = new Sort(Sort.Direction.DESC, ID);
private final SortArgumentResolver delegate;
public IdSortArgumentResolverAdapter(SortArgumentResolver delegate) {
this.delegate = delegate;
}
#Override
public boolean supportsParameter(MethodParameter methodParameter) {
return delegate.supportsParameter(methodParameter);
}
#Override
public Sort resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
Sort sort = delegate.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
if (isNull(sort)) {
return sortById;
}
if (containsSortById(sort)) {
return sort;
}
return sort.and(sortById);
}
private static boolean containsSortById(Sort currentSort) {
return StreamSupport.stream(currentSort.spliterator(), false)
.anyMatch(order -> ID.equalsIgnoreCase(order.getProperty()));
}
}
How can I change default SortArgumentResolver to IdSortArgumentResolver ? Is it possible? or maybe there is a better way to do that...
P.S It's spring-boot 1.5.2 RELEASE and current SortHandlerMethodArgumentResolver is configured in SpringDataWebConfiguration
Note: in your post, your class name is IdSortArgumentResolver but your constructor is IdSortArgumentResolverAdapter.
Create a configuration class extending WebMvcConfigurerAdapter:
#Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
// ...
}
Override the addArgumentResolvers method and configure your Sort and Pageable resolvers:
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
// FIXME Replace null with what you want
SortArgumentResolver sortResolver = new IdSortArgumentResolver(null);
// For sorting resolution alone
argumentResolver.add(sortResolver);
PageableHandlerMethodArgumentResolver pageableResolver = new PageableHandlerMethodArgumentResolver(sortResolver);
// For sorting resolution encapsulated inside a pageable
argumentResolver.add(pageableResolver);
}
Better solution without a custom class:
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
SortHandlerMethodArgumentResolver sortResolver = new SortHandlerMethodArgumentResolver();
sortResolver.setFallbackSort(new Sort(Sort.Direction.DESC, "id"));
// For sorting resolution alone
argumentResolver.add(sortResolver);
PageableHandlerMethodArgumentResolver pageableResolver = new PageableHandlerMethodArgumentResolver(sortResolver);
// For sorting resolution encapsulated inside a pageable
argumentResolver.add(pageableResolver);
}
Starting in spring-data-commons version 2.0, there is are 2 new classes that will make this kind of thing easier:
SortHandlerMethodArgumentResolverCustomizer
PageableHandlerMethodArgumentResolverCustomizer
Unfortunately that's not the version that ships with the current version of Spring Boot, so replace at your own risk.
#Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
// s is SortHandlerMethodArgumentResolver
return s -> s.setPropertyDelimiter("<-->");
}
In this case, one would probably call resolveArgument to manipulate it.
Spring Data Web Support
for spring boot 2.2.1.RELEASE:
#Configuration
public class LocaleConfiguration implements WebMvcConfigurer {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
SortHandlerMethodArgumentResolver sortArgumentResolver = new SortHandlerMethodArgumentResolver();
Sort defaultSort = Sort.by(new Sort.Order(Sort.Direction.DESC, "id"));
sortArgumentResolver.setFallbackSort(defaultSort);
PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver(sortArgumentResolver);
resolver.setOneIndexedParameters(true);
resolver.setMaxPageSize(20);
resolvers.add(resolver);
}
}

Resources