Spring cache, force to set the return type - spring

I'm trying to implement org.springframework.cache.Cache
The cached value is stored as JSON in a SQL database.
In the Cache interface, there are multiple get methods.
ValueWrapper get(Object key);
<T> T get(Object key, #Nullable Class<T> type);
<T> T get(Object key, Callable<T> valueLoader);
This is the first one that is used (without type or any generic information).
The problem is that since I save the value as JSON, I'd like to have the return value of the cached methods to help deserialize it.
How can I force spring to use the method <T> T get(Object key, #Nullable Class<T> type); when using the #Cacheable annotation ?
My cache implementation (kotlin) :
class SqlCache(
private val name: String,
private val expiration: Duration,
private val cacheRepository: CacheRepository,
): Cache {
private val isoObjectMapper = ObjectMapper()
.registerModule(KotlinModule())
.registerModule(JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
override fun getName(): String {
return name
}
override fun getNativeCache(): Any {
return cacheRepository
}
// The method called by spring !
override fun get(key: Any): Cache.ValueWrapper? {
val cache = cacheRepository.find(name = name, key = key.toString()) ?: return null
val value = isoObjectMapper.readValue(cache.value, UserModel::class)
return SimpleValueWrapper(value)
}
override fun <T : Any?> get(key: Any, type: Class<T>?): T? {
val cache = cacheRepository.find(name = name, key = key.toString()) ?: return null
return isoObjectMapper.readValue(cache.value, type)
}
override fun <T : Any?> get(key: Any, valueLoader: Callable<T>): T? {
return null
}
override fun put(key: Any, value: Any?) {
cacheRepository.put(
name = name,
key = key.toString(),
value = value,
expiration = expiration,
)
}
override fun evict(key: Any) {
cacheRepository.delete(
name = name,
key = key.toString(),
)
}
override fun clear() {
}
}
Exemple of cache usage :
interface UserClientAdapter {
#Cacheable(value = ["user-cache"], key = "#id")
fun getUser(id: UUID): UserModel
}
So, in this last method, the user is well stored as a JSON string in the database.
But when we try to get back the cache. This is the method ValueWrapper get(Object key); that is called. So I don't know the expected return type of the method.

You should refer to the RedisCache implementation class.
extends AbstractValueAdaptingCache
Implement the lookup method
Write your deserialization method in the lookup method
protected Object lookup(Object key) {
byte[] value = this.cacheWriter.get(this.name, this.createAndConvertCacheKey(key));
return value == null ? null : this.deserializeCacheValue(value);
}
If you still can't return your results
Then you need to debug CacheAspectSupport.java execute method,
which handles the hit Cache.ValueWrapper

Related

Spring, how do I store java.lang.Class type in mongodb

I'm trying to store java.lang.Class in MongoDb using ReactiveCrudRepository, but I got this following errors.
#Document
data class Letter(
...,
val messageType: Class<*>
)
Can't find a codec for class java.lang.Class.
I tried implementing my custom conversions, but it converts other properties that has type String to java.lang.Class too.
#Bean
fun customConversions(): MongoCustomConversions {
val converters = ArrayList<Converter<*, *>>()
converters.add(object: Converter<String, Class<*>> {
override fun convert(source: String): Class<*> {
return Class.forName(source)
}
})
converters.add(object: Converter<Class<*>, String> {
override fun convert(source: Class<*>): String {
return source.name
}
})
return MongoCustomConversions(converters)
}
You could try using Property Converters. See example bellow:
class ReversingValueConverter implements PropertyValueConverter<String, String, ValueConversionContext> {
#Override
public String read(String value, ValueConversionContext context) {
return reverse(value);
}
#Override
public String write(String value, ValueConversionContext context) {
return reverse(value);
}
}
class Person {
#ValueConverter(ReversingValueConverter.class)
String ssn;
}
See Spring Data MongoDB Reference Documentation for more information.

Object Mapper generic deserialize

I have the problem with Object Mapper deserialize.
I use hibernate + spring boot and needed to deserialize json array for entity field.
This is my hibernate AttributeConverter:
abstract class JsonConverter<T>: AttributeConverter<Collection<T>, String> {
abstract val typeReference: TypeReference<out Collection<T>>
override fun convertToDatabaseColumn(attribute: Collection<T>?): String? {
if (attribute.isNullOrEmpty())
return null
return ObjectMapper.writeValueAsString(attribute)
}
}
implementation for collection
abstract class ListConverter<T>: JsonConverter<T>() {
override val typeReference = object : TypeReference<List<T>>() {}
override fun convertToEntityAttribute(dbData: String?): Collection<T> {
if (dbData == null)
return emptyList()
return ObjectMapper.readValue(dbData, typeReference)
}
}
abstract class SetConverter<T>: JsonConverter<T>() {
override val typeReference = object : TypeReference<Set<T>>() {}
override fun convertToEntityAttribute(dbData: String?): Collection<T> {
if (dbData == null)
return emptySet()
return ObjectMapper.readValue(dbData, typeReference)
}
}
Hibernate entity field
#Column(name = "product_ids")
#Convert(converter = LongSetConverter::class)
var productIds: Set<Long>,
implementation AttributeConverter for this entity field
class LongSetConverter: SetConverter<Long>()
The problem is that Long Generic not working and Object Mapper always return Set of Int
Long (or another type) just ingnore

Why does Spring #Cacheable not pass the annotated method's result type to its deserializer?

This is sample code with Kotlin.
#Configuration
#Bean("cacheManager1hour")
fun cacheManager1hour(#Qualifier("cacheConfig") cacheConfiguration: RedisCacheConfiguration, redisConnectionFactory: RedisConnectionFactory): CacheManager {
cacheConfiguration.entryTtl(Duration.ofSeconds(60 * 60))
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfiguration)
.build()
}
#Bean("cacheConfig")
fun cacheConfig(objectMapper:ObjectMapper): RedisCacheConfiguration {
return RedisCacheConfiguration.defaultCacheConfig()
.computePrefixWith { cacheName -> "yaya:$cacheName:" }
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer()))
}
#RestController
#Cacheable(value = "book", key = "#root.methodName", cacheManager = "cacheManager1hour")
fun getBook(): Book {
return Book()
}
class Book {
var asdasd:String? = "TEST"
var expires_in = 123
}
The GenericJackson2JsonRedisSerializer cannot process the "kotlin class" and we need to add '#class as property' to the Redis cache entry.
Anyway, why do we need the #class? The Spring context is aware of the result's type, why doesn't it get passed? We would have two benefits:
less memory
easy for the Serializer, i.e. objectMapper.readValue(str, T)
Annotated Spring code for illustration
// org.springframework.cache.interceptor.CacheAspectSupport
#Nullable
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
for (Cache cache : context.getCaches()) {
// --> maybe we can pass the context.method.returnType to doGet
Cache.ValueWrapper wrapper = doGet(cache, key);
if (wrapper != null) {
if (logger.isTraceEnabled()) {
logger.trace("Cache entry for key '" + key + "' found in cache '" +
cache.getName() + "'");
}
return wrapper;
}
}
return null;
}
// org.springframework.data.redis.cache.RedisCache
#Override
protected Object lookup(Object key) {
// -> there will get the deserialized type can pass to Jackson
byte[] value = cacheWriter.get(name, createAndConvertCacheKey(key));
if (value == null) {
return null;
}
return deserializeCacheValue(value);
}
Your return type could be:
some abstract class
some interface
In those cases your return type is almost useless to deserialize the object. Encoding the actual class always works .

Pass method argument in Aspect of custom annotation

I'm trying to use something similar to org.springframework.cache.annotation.Cacheable :
Custom annotation:
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface CheckEntity {
String message() default "Check entity msg";
String key() default "";
}
Aspect:
#Component
#Aspect
public class CheckEntityAspect {
#Before("execution(* *.*(..)) && #annotation(checkEntity)")
public void checkEntity(JoinPoint joinPoint, CheckEntitty checkEntity) {
System.out.println("running entity check: " + joinPoint.getSignature().getName());
}
}
Service:
#Service
#Transactional
public class EntityServiceImpl implements EntityService {
#CheckEntity(key = "#id")
public Entity getEntity(Long id) {
return new Entity(id);
}
}
My IDE (IntelliJ) doesn't see anything special with the key = "#id" usage in contrast to similar usages for Cacheable where it's shown with different color than plain text. I'm mentioning the IDE part just as a hint in case it helps, it looks like the IDE is aware in advance about these annotations or it just realizes some connection which doesn't exist in my example.
The value in the checkEntity.key is '#id' instead of an expected number.
I tried using ExpressionParser but possibly not in the right way.
The only way to get parameter value inside the checkEntity annotation is by accessing the arguments array which is not what I want because this annotation could be used also in methods with more than one argument.
Any idea?
Adding another simpler way of doing it using Spring Expression. Refer below:
Your Annotation:
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface CheckEntity {
String message() default "Check entity msg";
String keyPath() default "";
}
Your Service:
#Service
#Transactional
public class EntityServiceImpl implements EntityService {
#CheckEntity(keyPath = "[0]")
public Entity getEntity(Long id) {
return new Entity(id);
}
#CheckEntity(keyPath = "[1].otherId")
public Entity methodWithMoreThanOneArguments(String message, CustomClassForExample object) {
return new Entity(object.otherId);
}
}
class CustomClassForExample {
Long otherId;
}
Your Aspect:
#Component
#Aspect
public class CheckEntityAspect {
#Before("execution(* *.*(..)) && #annotation(checkEntity)")
public void checkEntity(JoinPoint joinPoint, CheckEntitty checkEntity) {
Object[] args = joinPoint.getArgs();
ExpressionParser elParser = new SpelExpressionParser();
Expression expression = elParser.parseExpression(checkEntity.keyPath());
Long id = (Long) expression.getValue(args);
// Do whatever you want to do with this id
// This works for both the service methods provided above and can be re-used for any number of similar methods
}
}
PS: I am adding this solution because I feel this is a simpler/clearner approach as compared to other answers and this might be helpful for someone.
Thanks to #StéphaneNicoll I managed to create a first version of a working solution:
The Aspect
#Component
#Aspect
public class CheckEntityAspect {
protected final Log logger = LogFactory.getLog(getClass());
private ExpressionEvaluator<Long> evaluator = new ExpressionEvaluator<>();
#Before("execution(* *.*(..)) && #annotation(checkEntity)")
public void checkEntity(JoinPoint joinPoint, CheckEntity checkEntity) {
Long result = getValue(joinPoint, checkEntity.key());
logger.info("result: " + result);
System.out.println("running entity check: " + joinPoint.getSignature().getName());
}
private Long getValue(JoinPoint joinPoint, String condition) {
return getValue(joinPoint.getTarget(), joinPoint.getArgs(),
joinPoint.getTarget().getClass(),
((MethodSignature) joinPoint.getSignature()).getMethod(), condition);
}
private Long getValue(Object object, Object[] args, Class clazz, Method method, String condition) {
if (args == null) {
return null;
}
EvaluationContext evaluationContext = evaluator.createEvaluationContext(object, clazz, method, args);
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, clazz);
return evaluator.condition(condition, methodKey, evaluationContext, Long.class);
}
}
The Expression Evaluator
public class ExpressionEvaluator<T> extends CachedExpressionEvaluator {
// shared param discoverer since it caches data internally
private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<>(64);
private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);
/**
* Create the suitable {#link EvaluationContext} for the specified event handling
* on the specified method.
*/
public EvaluationContext createEvaluationContext(Object object, Class<?> targetClass, Method method, Object[] args) {
Method targetMethod = getTargetMethod(targetClass, method);
ExpressionRootObject root = new ExpressionRootObject(object, args);
return new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer);
}
/**
* Specify if the condition defined by the specified expression matches.
*/
public T condition(String conditionExpression, AnnotatedElementKey elementKey, EvaluationContext evalContext, Class<T> clazz) {
return getExpression(this.conditionCache, elementKey, conditionExpression).getValue(evalContext, clazz);
}
private Method getTargetMethod(Class<?> targetClass, Method method) {
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
Method targetMethod = this.targetMethodCache.get(methodKey);
if (targetMethod == null) {
targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
if (targetMethod == null) {
targetMethod = method;
}
this.targetMethodCache.put(methodKey, targetMethod);
}
return targetMethod;
}
}
The Root Object
public class ExpressionRootObject {
private final Object object;
private final Object[] args;
public ExpressionRootObject(Object object, Object[] args) {
this.object = object;
this.args = args;
}
public Object getObject() {
return object;
}
public Object[] getArgs() {
return args;
}
}
I think you probably misunderstand what the framework is supposed to do for you vs. what you have to do.
SpEL support has no way to be triggered automagically so that you can access the actual (resolved) value instead of the expression itself. Why? Because there is a context and as a developer you have to provide this context.
The support in Intellij is the same thing. Currently Jetbrains devs track the places where SpEL is used and mark them for SpEL support. We don't have any way to conduct the fact that the value is an actual SpEL expression (this is a raw java.lang.String on the annotation type after all).
As of 4.2, we have extracted some of the utilities that the cache abstraction uses internally. You may want to benefit from that stuff (typically CachedExpressionEvaluator and MethodBasedEvaluationContext).
The new #EventListener is using that stuff so you have more code you can look at as examples for the thing you're trying to do: EventExpressionEvaluator.
In summary, your custom interceptor needs to do something based on the #id value. This code snippet is an example of such processing and it does not depend on the cache abstraction at all.
Spring uses internally an ExpressionEvaluator to evaluate the Spring Expression Language in the key parameter (see CacheAspectSupport)
If you want to emulate the same behaviour, have a look at how CacheAspectSupport is doing it. Here is an snippet of the code:
private final ExpressionEvaluator evaluator = new ExpressionEvaluator();
/**
* Compute the key for the given caching operation.
* #return the generated key, or {#code null} if none can be generated
*/
protected Object generateKey(Object result) {
if (StringUtils.hasText(this.metadata.operation.getKey())) {
EvaluationContext evaluationContext = createEvaluationContext(result);
return evaluator.key(this.metadata.operation.getKey(), this.methodCacheKey, evaluationContext);
}
return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
}
private EvaluationContext createEvaluationContext(Object result) {
return evaluator.createEvaluationContext(
this.caches, this.metadata.method, this.args, this.target, this.metadata.targetClass, result);
}
I don't know which IDE you are using, but it must deal with the #Cacheable annotation in a different way than with the others in order to highlight the params.
Your annotation can be used with methods with more than 1 parameter, but that doesn't mean you can't use the arguments array. Here's a sollution:
First we have to find the index of the "id" parameter. This you can do like so:
private Integer getParameterIdx(ProceedingJoinPoint joinPoint, String paramName) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = methodSignature.getParameterNames();
for (int i = 0; i < parameterNames.length; i++) {
String parameterName = parameterNames[i];
if (paramName.equals(parameterName)) {
return i;
}
}
return -1;
}
where "paramName" = your "id" param
Next you can get the actual id value from the arguments like so:
Integer parameterIdx = getParameterIdx(joinPoint, "id");
Long id = joinPoint.getArgs()[parameterIdx];
Of course this assumes that you always name that parameter "id". One fix there could be to allow to specify the parameter name on the annotation, something like
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface CheckEntity {
String message() default "Check entity msg";
String key() default "";
String paramName() default "id";
}

Oracle char type issue in Hibernate HQL query

I have Oracle table, which contains char type columns. In my Entity class i mapped oracle char type to java string type.
Here is the code for my Entity class.
#Entity
#Table(name="ORG")
public class Organization {
private String serviceName;
private String orgAcct;
//Some other properties goes here...
#Column(name="ORG_ACCT", nullable=false, length=16)
public String getOrgAcct() {
return this.orgAcct;
}
public void setOrgAcct(String orgAcct) {
this.orgAcct = orgAcct;
}
#Column(name="SERVICE_NAME",nullable=true, length=16)
public String getServiceName() {
return this.serviceName;
}
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}
}
Here both serviceName and orgAcct are char type variables in Oracle
In my DAO class I wrote a HQL query to fetch Oranization entity object using serviceName and orgAcct properties.
#Repository
#Scope("singleton") //By default scope is singleton
public class OrganizationDAOImpl implementsOrganizationDAO {
public OrganizationDAOImpl(){
}
public Organization findOrganizationByOrgAcctAndServiceName(String orgAcct,String serviceName){
String hqlQuery = "SELECT org FROM Organization org WHERE org.serviceName = :serName AND org.orgAcct = :orgAct";
Query query = getCurrentSession().createQuery(hqlQuery)
.setString("serName", serviceName)
.setString("orgAct", orgAcct);
Organization org = findObject(query);
return org;
}
}
But when I call findOrganizationByOrgAcctAndServiceName() method , I am getting Organization object as null(i.e. HQL query is not retrieving Char type data ).
Please help me to fix this issue. Here I can't change Oracle type char to Varchar2. I need to work with oracle char type variables.
#EngineerDollery After going throw above post, I modified my Entity class with columnDefinition , #Column annotation attribute.
#Column(name="SERVICE_NAME",nullable=true,length=16,columnDefinition="CHAR")
public String getServiceName() {
return this.serviceName;
}
But still I am not able to retrieve the data for corresponding columns.
and I added column size as well in columnDefinition attribute.
#Column(name="SERVICE_NAME",nullable=true,length=16,columnDefinition="CHAR(16)
But still same issue I am facing.
Any thing Am I doing wrong. Please help me.
I resolved this problem using OraclePreparedStatement and Hibernate UserType interface.
Crated a new UserType class by extending org.hibernate.usertype.UserType interface and provided implementation for nullSafeSet(), nullSafeGet() methods .
nullSafeSet() method, we have first parameter as PreparedStatement, inside the method I casted PreparedStatement into OraclePreparedStatement object and pass String value using setFixedCHAR() method.
Here is the complete code of UserType impl class.
package nc3.jws.persistence.userType;
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import org.apache.commons.lang.StringUtils;
import org.hibernate.type.StringType;
import org.hibernate.usertype.UserType;
/**
*
* based on www.hibernate.org/388.html
*/
public class OracleFixedLengthCharType implements UserType {
public OracleFixedLengthCharType() {
System.out.println("OracleFixedLengthCharType constructor");
}
public int[] sqlTypes() {
return new int[] { Types.CHAR };
}
public Class<String> returnedClass() {
return String.class;
}
public boolean equals(Object x, Object y) {
return (x == y) || (x != null && y != null && (x.equals(y)));
}
#SuppressWarnings("deprecation")
public Object nullSafeGet(ResultSet inResultSet, String[] names, Object o) throws SQLException {
//String val = (String) Hibernate.STRING.nullSafeGet(inResultSet, names[0]);
String val = StringType.INSTANCE.nullSafeGet(inResultSet, names[0]);
//System.out.println("From nullSafeGet method valu is "+val);
return val == null ? null : StringUtils.trim(val);
}
public void nullSafeSet(PreparedStatement inPreparedStatement, Object o,
int i)
throws SQLException {
String val = (String) o;
//Get the delegatingStmt object from DBCP connection pool PreparedStatement object.
org.apache.commons.dbcp.DelegatingStatement delgatingStmt = (org.apache.commons.dbcp.DelegatingStatement)inPreparedStatement;
//Get OraclePreparedStatement object using deletatingStatement object.
oracle.jdbc.driver.OraclePreparedStatement oraclePreparedStmpt = (oracle.jdbc.driver.OraclePreparedStatement)delgatingStmt.getInnermostDelegate();
//Call setFixedCHAR method, by passing string type value .
oraclePreparedStmpt.setFixedCHAR(i, val);
}
public Object deepCopy(Object o) {
if (o == null) {
return null;
}
return new String(((String) o));
}
public boolean isMutable() {
return false;
}
public Object assemble(Serializable cached, Object owner) {
return cached;
}
public Serializable disassemble(Object value) {
return (Serializable) value;
}
public Object replace(Object original, Object target, Object owner) {
return original;
}
public int hashCode(Object obj) {
return obj.hashCode();
}
}
Configured this class with #TypeDefs annotation in Entity class.
#TypeDefs({
#TypeDef(name = "fixedLengthChar", typeClass = nc3.jws.persistence.userType.OracleFixedLengthCharType.class)
})
Added this type to CHAR type columns
#Type(type="fixedLengthChar")
#Column(name="SERVICE_NAME",nullable=true,length=16)
public String getServiceName() {
return this.serviceName;
}
char types are padded with spaces in the table. This means that if you have
foo
in one of these columns, what you actually have is
foo<space><space><space>...
until the actual length of the string is 16.
Consequently, if you're looking for an organization having "foo" as its service name, you won't find any, because the actual value in the table if foo padded with 13 spaces.
You'll thus have to make sure all your query parameters are also padded with spaces.

Resources