Home > Mobile >  Spring Cache Abstraction with Hazelcast doesn't preserve the order of elements
Spring Cache Abstraction with Hazelcast doesn't preserve the order of elements

Time:02-22

We upgraded Hazelcast from 3.12.12 to 5.0.2 and now Spring cache doesn't preserve the order of the elements in the Map we store in the cache. It used to work before the upgrade. The java.util.TreeMap we store in the cache is ordered using a custom java.util.Comparator.

Below is the code. The getSortedCountriesFromCountryCodes() method, when invoked, returns a Map whose elements are sorted correctly according to the custom comparator, but when the same map is retrieved from the cache, the order is lost and map items are ordered alphabetically using the corresponding keys. Spring version is 5.3.14. Has anyone ever seen such behaviour and maybe knows how to fix it?

@Cacheable(value = COUNTRIES_CACHE, key = "#locale?:'default'")
@Override
public Map<String, String> getSortedLocaleCountriesFullMap(Locale locale) {
    return getSortedCountriesFromCountryCodes(locale, ALL_COUNTRY_CODES);
}

(...)

private TreeMap<String, String> getSortedCountriesFromCountryCodes(Locale locale, List<String> countryCodes) {

    final TreeMap<String, String> sortedCountries = new TreeMap<>(new MessageSourceComparator(messageSource, "label.country.", locale));

    for (String countryCode : countryCodes) {
        String label;
        try {
            label = messageSource.getMessage("label.country."   countryCode, null, locale);
        } catch (final NoSuchMessageException nsme) {
            LOG.error("NoSuchMessageException: 'label.country.{}' not found in messages file. "   "Setting country label to countryCode({})", countryCode,
                    countryCode);
            label = countryCode;
        }
        sortedCountries.put(countryCode, label);
    }
    return sortedCountries;
}

    /**
     * Comparator to sort based on messages retrieved using the keys
     */
    protected static class MessageSourceComparator implements Comparator<String>, Serializable {
        private static final long serialVersionUID = 1L;

        private final transient Locale locale;
        private final transient MessageSource messageSource;
        private final transient String messagePrefix;
        private final transient Collator collator;

        public MessageSourceComparator(MessageSource messageSource, String messagePrefix, Locale locale) {
            this.messageSource = messageSource;
            this.messagePrefix = messagePrefix;
            this.locale = locale;
            if (locale != null) {
                this.collator = Collator.getInstance(locale);
            } else {
                this.collator = Collator.getInstance();
            }

        }

        @Override
        public int compare(final String key1, final String key2) {
            if (collator != null) {
                String message1 = getMessage(key1);
                String message2 = getMessage(key2);
                return collator.compare(message1, message2);
            } else {
                return key1.compareTo(key2);
            }
        }

        private String getMessage(String key) {
            String message;
            try {
                message = messageSource.getMessage(messagePrefix   key, null, locale);
            } catch (NoSuchMessageException nsme) {
                message = key;
            }
            return message;
        }
    }

Here is the Cache config:

@Configuration
@EnableCaching
@EnableAsync
@EnableScheduling
@ComponentScan({ "xxx.yyyyy.zzzzzz" })
public class AppConfig {

    /**
     * The number of backup copies of cache data to use for resilience
     */
    private static final int DEFAULT_BACKUP_COUNT = 2;

    @Bean
    public LogSanitiser logSanitiser() {
        BasicLogSanitiser basicLogSanitiser = new BasicLogSanitiser();
        return basicLogSanitiser;
    }

    /*
     * Use Hazelcast for managing our caches
     * 
     * Takes an autowired list of CacheSimpleConfig objects This allows us to set up our caches in separate config files / modules
     */
    @Bean
    public CacheManager cacheManager(HazelcastInstance hazelcastInstance, List<MapConfig> mapConfigs) {

        for (MapConfig mapConfig : mapConfigs) {
            hazelcastInstance.getConfig()
                    .addMapConfig(mapConfig);
        }

        HazelcastCacheManager cacheManager = new HazelcastCacheManager(hazelcastInstance);
        return cacheManager;
    }



    @Bean
    public MapConfig countriesCacheConfig() {
        return getDefaultMapConfig(DefaultLocaleService.COUNTRIES_CACHE, 25);
    }


    private static MapConfig getDefaultMapConfig(String mapName, int maxHeapUsed) {
        MapConfig mapConfig = new MapConfig(mapName);
        mapConfig.setBackupCount(DEFAULT_BACKUP_COUNT)
                .setEvictionConfig(new EvictionConfig().setEvictionPolicy(EvictionPolicy.LRU)
                        .setSize(maxHeapUsed)
                        .setMaxSizePolicy(MaxSizePolicy.USED_HEAP_SIZE));
        return mapConfig;
    }

 (...)

}

CodePudding user response:

The issue is your MessageSourceComparator, after deserialization it sorts differently.

There was a change how TreeMap is handled between 3.x and 4.x.

In 3.x the TreeMap is serialized using plain Java serialization. Apparently, it deserializes the data in the order it was stored in the map.

In 4.x a special serializer for TreeMap was added, what this serializer does on deserialization is that it creates new TreeSet with deserialized comparator and adds all elements to it. Now because your deserialized comparator is different the elements end up in the wrong order.

I don't think it is reasonable to expect to keep the order when the comparator changes on ser/de. What you can do is to cache LinkedHashMap instead:

    return new LinkedHashMap<>(sortedCountries);

This way you avoid serializing the comparator entirely. It seems you don't modify the set afterward so it shouldn't be a problem.

  • Related