When in a spring boot application, Hazelcast is used as a distributed cache, there are some times that it is forgotten to be changed the serialVersionUID on the objects that are cached and this has as a result to have way a lot of exceptions and most probably alerts to go crazy from the error bellow.

java.io.InvalidClassException: com.company.service.server.domain.SomeObject; local class incompatible: stream classdesc serialVersionUID = -5387655287348283785, local class serialVersionUID = -1803841624490656210
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2002)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1849)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2159)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1666)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:502)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:460)
    at com.hazelcast.internal.serialization.impl.JavaDefaultSerializers$JavaSerializer.read(JavaDefaultSerializers.java:84)
    at com.hazelcast.internal.serialization.impl.JavaDefaultSerializers$JavaSerializer.read(JavaDefaultSerializers.java:77)
    at com.hazelcast.internal.serialization.impl.StreamSerializerAdapter.read(StreamSerializerAdapter.java:48)
    at com.hazelcast.internal.serialization.impl.AbstractSerializationService.toObject(AbstractSerializationService.java:187)
    at com.hazelcast.map.impl.proxy.MapProxySupport.toObject(MapProxySupport.java:1237)
    at com.hazelcast.map.impl.proxy.MapProxyImpl.get(MapProxyImpl.java:120)
    at com.hazelcast.spring.cache.HazelcastCache.lookup(HazelcastCache.java:162)
    at com.hazelcast.spring.cache.HazelcastCache.get(HazelcastCache.java:67)

I was trying to find a way to handle this better and centrally. So every time that something similar will happen we will just evict the incompatible object from cache through the code.

I found that spring boot provides an interface to override for error handing which is: org.springframework.cache.interceptor.CacheErrorHandler .

package org.springframework.cache.interceptor;

import org.springframework.cache.Cache;
import org.springframework.lang.Nullable;

public interface CacheErrorHandler {
    void handleCacheGetError(RuntimeException var1, Cache var2, Object var3);

    void handleCachePutError(RuntimeException var1, Cache var2, Object var3, @Nullable Object var4);

    void handleCacheEvictError(RuntimeException var1, Cache var2, Object var3);

    void handleCacheClearError(RuntimeException var1, Cache var2);
}

But this interface is only for handing runtime exceptions and not checked exceptions as the InvalidClassException is.

So I was curious why this CacheErrorHandler is so specific for runtime exceptions and not for all kind of exceptions. In that way, someone can choose how to handle them centrally and nicely.

Comment From: snicoll

So I was curious why this CacheErrorHandler is so specific for runtime exceptions and not for all kind of exceptions. In that way, someone can choose how to handle them centrally and nicely.

The cache abstraction contract makes no assumption over the signature of your method. You could write something like this:

@Cacheable
public MyObject get(String key) {
  ...
}

If we started accepting something else than a RuntimeException we'd have to do something if the handler implementation did not handle it. Besides, the contract of the cache abstraction is working based on the assumption that no checked exceptions will be thrown. If a checked exception happens as part of interacting with the cache, the Cache implementation should wrap that in a runtime.

Is the exception above thrown or is it logged? Looking at the HazecastCache implementation, it's not obvious. I can see they are throwing back Error which is fair enough but this one is an IOException. If the exception is thrown back to you as is, it would be helpful to provide a more exhaustive stacktrace that contains a call to the annotated method.

I understand what you're trying to do but using a high-level contract like this is fairly limited. It is central only if all your code path would go through the cache abstraction which is not something we could assume. In general, a cache, especially when it's distributed, can be accessed in many many ways.

Comment From: Pavlmits

Thank you very much @snicoll for the nice explanation and I fully understand why this is handled like this from Spring. But do you think that the specific implementation ( HazelcastCache ) from the org.springframework.cache.Cache interface should map the java.io.InvalidClassException to a Runtime exception in order to be more compliant with the cache abstraction specification?

The above exception is thrown as I can see from my Kibana logs:

exception.exception_class --> com.hazelcast.nio.serialization.HazelcastSerializationException exception.exception_message ---> java.io.InvalidClassException: com.company.service.server.domain.SomeObject; local class incompatible: stream classdesc serialVersionUID = -5387655287348283785, local class serialVersionUID = -1803841624490656210 exception.root_exception_class ---> java.io.InvalidClassException exception.root_exception_message ---> java.io.InvalidClassException: com.company.service.server.domain.SomeObject; local class incompatible: stream classdesc serialVersionUID = -5387655287348283785, local class serialVersionUID = -1803841624490656210`

exception.root_stacktrace --->

java.io.InvalidClassException: com.company.service.server.domain.SomeObject; local class incompatible: stream classdesc serialVersionUID = -5387655287348283785, local class serialVersionUID = -1803841624490656210
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2002)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1849)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2159)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1666)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:502)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:460)
    at com.hazelcast.internal.serialization.impl.JavaDefaultSerializers$JavaSerializer.read(JavaDefaultSerializers.java:84)
    at com.hazelcast.internal.serialization.impl.JavaDefaultSerializers$JavaSerializer.read(JavaDefaultSerializers.java:77)
    at com.hazelcast.internal.serialization.impl.StreamSerializerAdapter.read(StreamSerializerAdapter.java:48)
    at com.hazelcast.internal.serialization.impl.AbstractSerializationService.toObject(AbstractSerializationService.java:187)
    at com.hazelcast.map.impl.proxy.MapProxySupport.toObject(MapProxySupport.java:1237)
    at com.hazelcast.map.impl.proxy.MapProxyImpl.get(MapProxyImpl.java:120)
    at com.hazelcast.spring.cache.HazelcastCache.lookup(HazelcastCache.java:162)
    at com.hazelcast.spring.cache.HazelcastCache.get(HazelcastCache.java:67)
    at org.springframework.cache.interceptor.AbstractCacheInvoker.doGet(AbstractCacheInvoker.java:73)
    at org.springframework.cache.interceptor.CacheAspectSupport.findInCaches(CacheAspectSupport.java:571)
    at org.springframework.cache.interceptor.CacheAspectSupport.findCachedItem(CacheAspectSupport.java:536)
    at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:402)
    at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:346)
    at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:61)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
    at com.company.service.server.payment.Service$$EnhancerBySpringCGLIB$$cfc34a87.getTransactionByIdCached(<generated>)
    at com.company.service.server.payment.server.suggestion.PaymentService$getAlForTransaction$getTransactionByIdFunc$1.invoke(Service.kt:206)
    at com.company.service.payment.server.suggestion.PaymentService$getAlForTransaction$getTransactionByIdFunc$1.invoke(Service.kt:21)
    at com.company.service.payment.server.Service.getAlForTransaction(Service.kt:210)
    at com.company.service.payment.server.suggestion.Service.getAlForTransaction$default(Service.kt:191)
    at com.company.service.payment.server.Service.isAllowed(Service.kt:167)
    at com.company.service.payment.server.Service.isAllowed(Service.kt:119)
    at com.company.service.payment.server.Service.getSuggestions(Service.kt:54)
    at com.company.service.payment.server.Service.selectPaymentSuggestions(Service.kt:46)
    at com.company.service.payment.server.Service.getNonSuggestion(Service.kt:36)
    at sun.reflect.GeneratedMethodAccessor327.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:282)
    at org.springframework.cloud.context.scope.GenericScope$LockedScopedProxyFactoryBean.invoke(GenericScope.java:499)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
    at com.company.service.payment.server.Service$$EnhancerBySpringCGLIB$$ee62fdc.getNonSuggestion(<generated>)
    at com.company.service.payment.server.Service.resetPaymentState(Service.kt:512)
    at com.company.service.payment.server.Service.validateAndFix(Service.kt:432)
    at com.company.service.server.Service.getCodeAndType(Service.kt:83)
    at com.company.service.server.payment.Controller.getCustomerTemp(Controller.kt:29)
    at sun.reflect.GeneratedMethodAccessor329.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792)

Comment From: sbrannen

HazelcastSerializationException is a RuntimeException.

So it looks like Hazelcast throws a HazelcastSerializationException that wraps the InvalidClassException.

In other words, it appears that HazelcastCache is already compliant with Spring's Cache API.

Have you tried implementing CacheErrorHandler and explicitly handling instances of HazelcastSerializationException and then checking if the cause is an instance of InvalidClassException?

Comment From: Pavlmits

ohh you are right @sbrannen! No, I have not tried because I focused on the InvalidClassException and that one is a checked exception. I will check it and I will back to you. Thank you very much for your help and sorry for the confusion.

Comment From: Pavlmits

But because I think that it is not a spring problem I will close the issue and most probably I will comment out the results for future reference.

Comment From: sbrannen

Thanks for the feedback, @Pavlmits.

I'm labeled this issue as "invalid" accordingly.