spring-projects / spring-data-redis

Provides support to increase developer productivity in Java when using Redis, a key-value store. Uses familiar Spring concepts such as a template classes for core API usage and lightweight repository style data access.
https://spring.io/projects/spring-data-redis/
Apache License 2.0
1.78k stars 1.18k forks source link

Using Native image results in Serialization Exception #2495

Closed Abhi-Codes closed 1 year ago

Abhi-Codes commented 1 year ago

Describe the bug

This only happens in the native version. In uber jar way, no issue. Spring Boot 3.0.1 openjdk version "17.0.5" 2022-10-18 OpenJDK Runtime Environment GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08) OpenJDK 64-Bit Server VM GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08, mixed mode, sharing)

Stack trace:

com.xxx.ldapservice.config.RedisConfig  : Failure getting from cache: user, exception: org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of `java.util.LinkedList` (no Creators, like default constructor, exist): no default no-arguments constructor found
 at [Source: (byte[])"["java.util.LinkedList",[{"@class":"java.util.HashMap","country":"IN","dn":"CN=gururanib,CN=Users,DC=xxx,DC=com","office":"Remote_Remote Office - IND-Karnataka","title":"Application Developer","manager_last_name":"Sharma","manager_username":"xxx","ldap_created_at":"2021-10-01","bu":"IT","manager_first_name":"xxxx","worker_type":"xxx","state":"Karnataka","department":"IT","first_name":"Bhaskar","email":"xxx@xxx.com","[truncated 436 bytes]; line: 1, column: 25]

Service class where @cacheable annotation is used :

        @Cacheable(value = "user", key = "#username",unless="#result.size()==0")
    @Override
    public List<Map<String, Object>> getADAttributes(String username) {
        LdapQuery query = query().base("DC=xxx,DC=com").where(AD_SAMACCOUNTNAME).is(username);

        return ldapTemplate.search(query, new AttributesMapper<Map<String, Object>>() {
                   ....

RedisConfig

@Slf4j
@Configuration
@EnableCaching
@RequiredArgsConstructor
public class RedisConfig implements CachingConfigurer {

    @Value("${spring.cache.redis.time-to-live}")
    private long redisTimeToLive;

    @Value("${spring.data.redis.timeout}")
    private Duration redisCommandTimeout;

    private final RedisProperties redisProperties;

    @Bean
    protected LettuceConnectionFactory redisConnectionFactory() {
        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
                .master(redisProperties.getSentinel().getMaster());
        redisProperties.getSentinel().getNodes().forEach(s -> sentinelConfig.sentinel(s, redisProperties.getPort()));
        sentinelConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));

        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
                .commandTimeout(redisCommandTimeout).readFrom(ReadFrom.REPLICA_PREFERRED).build();
        return new LettuceConnectionFactory(sentinelConfig, clientConfig);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        final RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new GenericToStringSerializer<>(Object.class));
        redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }

    @Override
    @Bean
    public RedisCacheManager cacheManager() {
        return RedisCacheManager.builder(this.redisConnectionFactory()).cacheDefaults(this.cacheConfiguration())
                .build();
    }

    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(redisTimeToLive))
                .disableCachingNullValues()
                .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }

    @Override
    public CacheErrorHandler errorHandler() {
        return new CacheErrorHandler() {
            @Override
            public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                log.info("Failure getting from cache: " + cache.getName() + ", exception: " + exception.toString());
            }

            @Override
            public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
                log.info("Failure putting into cache: " + cache.getName() + ", exception: " + exception.toString());
            }

            @Override
            public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
                log.info("Failure evicting from cache: " + cache.getName() + ", exception: " + exception.toString());
            }

            @Override
            public void handleCacheClearError(RuntimeException exception, Cache cache) {
                log.info("Failure clearing cache: " + cache.getName() + ", exception: " + exception.toString());
            }
        };
    }

}
Abhi-Codes commented 1 year ago

I resolved this by adding the hints for Serialization.

@SpringBootApplication
@ImportRuntimeHints(LdapServiceApplication.MyRuntimeHints.class)
public class LdapServiceApplication {

static class MyRuntimeHints implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            // Register serialization
            hints.serialization().registerType(HashMap.class).registerType(LinkedList.class);

        }

    }
}
denysandriyanov commented 1 year ago

Hi guys, could you please help me.

I have a method

@Cacheable(value = USER_IDENTITIES_PROTO_CACHE) public UserIdentitiesResponseOuterClass.UserIdentitiesResponse findUserIdentitiesProto(UUID userId) { where UserIdentitiesResponseOuterClass.UserIdentitiesResponse is a class generated by protobuff.

When using JVM everything works well but in the native image I am getting the exception

"Failure putting into cache: user_identities_proto. Cannot serialize"

I have tried to add

hints.serialization() .registerType(TypeReference.of(UserIdentitiesResponseOuterClass.class)) .registerType(UserIdentitiesResponseOuterClass.UserIdentity.class) .registerType(UserIdentitiesResponseOuterClass.UserIdentitiesResponse.class); but it did not help.

I am using Redis as a cache and using JdkSerializationRedisSerializer to serialize.

Content of proto file below:

syntax = "proto3";

package identity;

message UserIdentity { string provider = 1; string sub = 2; }

message UserIdentitiesResponse { UserIdentity primary = 1; repeated UserIdentity identities = 2; } if an application is run in JVM mode (not native) protobuff object is successfully serialized/deserialized. But in native image the mentioned problem happens.

I am assuming the solution would be to add the runtime hints.serialization(), but as I mentioned I did it and it did not help, so something is still missing, but it's not clear what exactly

How to fix it?

NoxFr commented 5 months ago

Up any news here ? I've still got the issues in SB 3.30 without LinkedHashMap hint it does not work !