Question

I an trying to use an "own made" object as part of the key in a @Cacheable annotation:

@Cacheable(value = "tecdocData", key = "'TCDD:stos::'.concat(#stos)")
List<TecDocData> getTecDocData(Collection<SimpleTecDocObject> stos);

This leads to an error with following stacktrace (a relevant part):

Caused by: org.springframework.expression.AccessException: Problem invoking method: public java.lang.String java.lang.String.concat(java.lang.String)
    at org.springframework.expression.spel.support.ReflectiveMethodExecutor.execute(ReflectiveMethodExecutor.java:73)
    at org.springframework.expression.spel.ast.MethodReference$MethodValueRef.getValue(MethodReference.java:97)
    ... 100 more
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1001E:(pos 0): Type conversion problem, cannot convert from java.util.HashSet<?> to java.lang.String
    at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:72)
    at org.springframework.expression.spel.support.ReflectionHelper.convertArguments(ReflectionHelper.java:281)
    at org.springframework.expression.spel.support.ReflectiveMethodExecutor.execute(ReflectiveMethodExecutor.java:61)
    ... 101 more
Caused by: org.springframework.core.convert.ConversionFailedException: Failed to convert from type java.util.HashSet<?> to type java.lang.String for value '[SimpleTecDocObject [guid=GUID-C2DBD976-79F0-42FB-94CE-9352DE43A184, locale=de, version=1]]'; nested exception is org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type com.sxp.setix.api.SimpleTecDocObject to type java.lang.String
    at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41)
    at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:169)
    at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:66)
    ... 103 more
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type com.sxp.setix.api.SimpleTecDocObject to type java.lang.String
    at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:276)
    at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:172)
    at org.springframework.core.convert.support.CollectionToStringConverter.convert(CollectionToStringConverter.java:65)
    at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:35)
    ... 105 more

As far as I can see, spring do make an attempt to convert a HashSet but fails to convert the contained "SimpleTecDocObject" values.

In order to solve this issue I defined a converter and registered it in my Spring configuration. This was done quite straight ahead as documented in "7.5 Spring 3 Type Conversion" 7. Validation, Data Binding, and Type Conversion

Converter class:

public class SimpleTecdocObjectConverter implements Converter<SimpleTecDocObject, String> {
    @Override
    public String convert(final SimpleTecDocObject source) {
        StringBuilder result = new StringBuilder();

        result.append(source.getGuid()).append(source.getLocale()).append(source.getVersion());

        return result.toString();
    }

}

Spring configuration:

<bean id="conversionService"
    class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <list>
            <bean id="tdConverter" class="com.kion.lsg.service.tecdoc.converter.SimpleTecdocObjectConverter" />
        </list>
    </property>
</bean>

Unfortunately this seems not to work. I get exactly the same exceptions as above. Spring abstraction seems to ignore the conversionService defined above and take some default one.

Solution: Lotsa thanks to Akshay Singhal

Instead of using a converter one can use a static key generator inside of the SpEL expression:

@Cacheable(value = "tecdocData", key = "T(com.kion.lsg.service.tecdoc.converter.SimpleTecDocObjectCollectionKeyGenerator).generateKey(#stos)")
List<TecDocData> getTecDocData(Collection<SimpleTecDocObject> stos);

For example with a KeyGenerator defined as following:

public class SimpleTecDocObjectCollectionKeyGenerator {
    public static String generateKey(final Collection<SimpleTecDocObject> stos){
        List<String> tecdocIdsList = new ArrayList<>();

        for (SimpleTecDocObject sto : stos){
            tecdocIdsList.add(sto.toString());
        }

        Collections.sort(tecdocIdsList);

        return tecdocIdsList.toString();
    }
}
Was it helpful?

Solution

I think the converter is being ignored because you have multiple contexts being loaded, like a ROOT context loaded by a ContextLoaderListener and a web application context loaded for a servlet. And your configuration is being overwritten by the context loaded later.

However that is just a guess, and admittedly not a very informed one, because I don't really know how your application is bootstrapped.

Regardless, looking at your problem, a custom converter might not be the simplest solution. Te whole point of a cache Key is to create a unique identifier out of the arguments, so that subsequent request with the same arguments can be served from the cache.

Because you have "'TCDD:stos::'.concat(#stos)" as your key expression, Spring is forced to use a converter to compute the string value of #stos. A much simpler solution would be to simply change your expression to "'TCDD:stos::'.concat(#stos.toString())", and rely on the Collections toString() method to delegate to your class's toString() method. - Thus creating a string out of the argument, which will be unique for each set of argument.

If you don't want to implement toString(), another option would be to create a Class whose sole responsibility is to convert Collection<SimpleTecDocObject> stos into a String. Create a static method in this utility class, use whatever key creating logic you want in this method, and call it from the spel expression. Something like T(SimpleTecDocObjectCollectionKeyGenerator).generateKey(stos). With this method you can even abstract away TCDD:stos:: to inside the key generator utility class.

Hope this helps.

OTHER TIPS

Thanks, the "#stos.toString()" worked fine. Unfortunately I wasn't able to do it with the KeyGenerator alternative.

This one doesn't work :

"@Cacheable(value = "tecdocData", key =  T(SimpleTecDocObjectCollectionKeyGenerator).generateKey(stos)")" 

This is how the generator looks like:

public class SimpleTecDocObjectCollectionKeyGenerator {
public static String generateKey(final Collection<SimpleTecDocObject> stos){
    List<String> tecdocIdsList = new ArrayList<>();

    for (SimpleTecDocObject sto : stos){
        tecdocIdsList.add(sto.toString());
    }

    Collections.sort(tecdocIdsList);

    return tecdocIdsList.toString();
}

}

Seems like it is never get called.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top