RedisCacheManager設定Value序列化器技巧
CacheManager基本配置
請參考博文:springboot2.0 redis EnableCaching的配置和使用
RedisCacheManager建構函式
/** * Construct a {@link RedisCacheManager}. * * @param redisOperations */ @SuppressWarnings("rawtypes") public RedisCacheManager(RedisOperations redisOperations) { this(redisOperations, Collections.<String> emptyList()); } /** * Construct a static {@link RedisCacheManager}, managing caches for the specified cache names only. * * @param redisOperations * @param cacheNames * @since 1.2 */ @SuppressWarnings("rawtypes") public RedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) { this.redisOperations = redisOperations; setCacheNames(cacheNames); }
RedisCacheManager需要一個 RedisOperations例項,一般是RedisTemplate。還有一個不必須的快取名稱集合引數。
protected RedisCache createCache(String cacheName) { long expiration = computeExpiration(cacheName); return new RedisCache(cacheName, (usePrefix ? cachePrefix.prefix(cacheName) : null), redisOperations, expiration); }
在建立快取時,通過RedisCache的建構函式傳入 redisOperations(即RedisTemplate例項)。
設定全域性通用的序列化器
GenericJackson2JsonRedisSerializer
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); User user = new User(); user.setName("hjzgg"); user.setAge(26); System.out.println(serializer.deserialize(serializer.serialize(user))); public static class User { private String name; private Integer age; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return Objects.toStringHelper(this) .add("name", name) .add("age", age) .toString(); } }
除錯發現,序列化內容加入了物件的型別資訊,如下。
檢視GenericJackson2JsonRedisSerializer建構函式,序列化和反序列化的實現是通過Jackson的ObjectMapper完成的。並開啟了預設型別的配置。
/** * Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing using the * given {@literal name}. In case of an {@literal empty} or {@literal null} String the default * {@link JsonTypeInfo.Id#CLASS} will be used. * * @param classPropertyTypeName Name of the JSON property holding type information. Can be {@literal null}. */ public GenericJackson2JsonRedisSerializer(String classPropertyTypeName) { this(new ObjectMapper()); if (StringUtils.hasText(classPropertyTypeName)) { mapper.enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, classPropertyTypeName); } else { mapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY); } }
Protostuff序列化和反序列化
import com.dyuproject.protostuff.LinkedBuffer; import com.dyuproject.protostuff.ProtostuffIOUtil; import com.dyuproject.protostuff.Schema; import com.dyuproject.protostuff.runtime.RuntimeSchema; import org.springframework.data.redis.serializer.RedisSerializer; public class ProtostuffRedisSerializer implements RedisSerializer<Object> { private static final Schema<ObjectWrapper> schema = RuntimeSchema.getSchema(ObjectWrapper.class); public ProtostuffRedisSerializer() { } public byte[] serialize(Object object) { if (object == null) { return new byte[0]; } else { LinkedBuffer buffer = LinkedBuffer.allocate(512); byte[] var3; try { var3 = ProtostuffIOUtil.toByteArray(new ObjectWrapper(object), schema, buffer); } finally { buffer.clear(); } return var3; } } public Object deserialize(byte[] bytes) { if (bytes != null && bytes.length != 0) { try { ObjectWrapper objectWrapper = new ObjectWrapper(); ProtostuffIOUtil.mergeFrom(bytes, objectWrapper, schema); return objectWrapper.getObject(); } catch (Exception var3) { throw new RuntimeException(var3.getMessage(), var3); } } else { return null; } } } public class ObjectWrapper { private Object object; public ObjectWrapper(Object object) { this.object = object; } public ObjectWrapper() { } public Object getObject() { return this.object; } public void setObject(Object object) { this.object = object; } }
上面通過Protostuff自定義了一個序列化和反序列化的工具,測試程式碼如下。
ProtostuffRedisSerializer serializer = new ProtostuffRedisSerializer(); Person person = new Person(); person.setName("hjzgg"); person.setAge(26); System.out.println(serializer.deserialize(serializer.serialize(person))); } public static class Person { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
除錯發現,序列化內容加入了物件的型別資訊,如下。
JdkSerializationRedisSerializer
JdkSerializationRedisSerializer serializer = new JdkSerializationRedisSerializer(); User user = new User(); user.setName("hjzgg"); user.setAge(26); System.out.println(serializer.deserialize(serializer.serialize(user))); public static class User implements Serializable { private String name; private Integer age; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return Objects.toStringHelper(this) .add("name", name) .add("age", age) .toString(); } }
JdkSerializationRedisSerializer建構函式如下,序列轉換器和反序列轉換器。
/** * Creates a new {@link JdkSerializationRedisSerializer} using the default class loader. */ public JdkSerializationRedisSerializer() { this(new SerializingConverter(), new DeserializingConverter()); }
發現JdkSerializationRedisSerializer內部使用的是我們最熟悉的ObjectInputStream和ObjectOutputStream。
除錯發現,序列化內容加入了物件的型別資訊,如下。
要快取的 Java 物件必須實現 Serializable 介面,因為 Spring 會將物件先序列化再存入 Redis,比如本文中的 User 類,如果不實現 Serializable 的話將會遇到類似這種錯誤:nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.XXX.User]]。
不同cache設定不同序列化器
Jackson2JsonRedisSerializer
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;Jackson2JsonRedisSerializer<?> serializer1 = new Jackson2JsonRedisSerializer<>(JacksonHelper.genJavaType(User.class)); System.out.println(serializer1.deserialize(serializer1.serialize(user))); public static class User { private String name; private Integer age; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return Objects.toStringHelper(this) .add("name", name) .add("age", age) .toString(); } }
Jackson2JsonRedisSerializer內部序列化過程也是通過Jackson ObjectMapper來完成的,但是序列化內容不包含物件型別資訊,如下。
所以,在使用Jackson2JsonRedisSerializer的時候需要指定當前cache儲存的物件型別。
自定義RedisCacheManager
實現不同RedisCache對應不同的RedisTemplate(即對應不同的序列化器)
static class CustomRedisCacheManager extends RedisCacheManager { private Map<String, RedisCache> redisCaches = Maps.newConcurrentMap(); public static final String CACHE_NAME_DEFAULT = "DEFAULT_CACHE"; public CustomRedisCacheManager(Map<String, CustomRedisConfiguration> configurations) { super(configurations.get(CACHE_NAME_DEFAULT).getRedisTemplate(), configurations.keySet()); configurations.keySet() .stream() .forEach( cacheName -> redisCaches.put(cacheName, new RedisCache(cacheName , null , configurations.get(cacheName).getRedisTemplate() , configurations.get(cacheName).duration.getSeconds())) ); } @Override public Cache getCache(String cacheName) { return redisCaches.get(cacheName); } }
RedisCacheManager 通過載入自定義配置實現類RedisCacheConfigurationProvider獲取不同RedisCache的配置
@Bean @Primary public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate, ObjectProvider<RedisCacheConfigurationProvider> provider) { Map<String, CustomRedisConfiguration> configurations = Maps.newHashMap(); configurations.put(CustomRedisCacheManager.CACHE_NAME_DEFAULT, new CustomRedisConfiguration(redisTemplate, Duration.ofMinutes(20))); RedisCacheConfigurationProvider configurationProvider = provider.getIfAvailable(); if (!Objects.isNull(configurationProvider)) { configurations.putAll(configurationProvider.resolve(redisTemplate.getConnectionFactory())); } RedisCacheManager cacheManager = new CustomRedisCacheManager(configurations); return cacheManager; }
RedisCache自定義配置提供者抽象類,根據不同的快取型別設定不同的序列化器
public static abstract class RedisCacheConfigurationProvider { // key = 快取名稱, value = 快取時間 和 快取型別 protected Map<String, Pair<Duration, JavaType>> configs; protected abstract void initConfigs(); public Map<String, CustomRedisConfiguration> resolve(RedisConnectionFactory connectionFactory) { initConfigs(); Assert.notEmpty(configs, "RedisCacheConfigurationProvider 配置不能為空..."); Map<String, CustomRedisConfiguration> result = Maps.newHashMap(); configs.forEach((cacheName, pair) -> { RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); redisTemplate.setKeySerializer(new JdkSerializationRedisSerializer()); redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(pair.getValue())); redisTemplate.afterPropertiesSet(); result.put(cacheName, new CustomRedisConfiguration(redisTemplate, pair.getKey())); }); return result; } }
使用者根據快取名稱設定不同的儲存型別
@Component public class CouponRedisCacheConfigurationProvider extends RedisCacheConfig.RedisCacheConfigurationProvider { @Override protected void initConfigs() { this.configs = Maps.newHashMap(); this.configs.put(CouponConstants.COUPON_ALL_CACHE, new Pair<>(Duration.ofHours(12), JacksonHelper.genMapType(HashMap.class, String.class, Coupon.class))); this.configs.put(CouponConstants.COUPON_GOOD_CACHE, new Pair<>(Duration.ofHours(12), JacksonHelper.genCollectionType(List.class, String.class))); this.configs.put(CouponConstants.COUPON_HANDLE_TELEPHONE_STATUS_CACHE, new Pair<>(Duration.ofHours(1), JacksonHelper.genCollectionType(List.class, CouponHandle.class))); this.configs.put(CouponConstants.COUPON_HANDLE_TELEPHONE_GOOD_CACHE, new Pair<>(Duration.ofHours(1), JacksonHelper.genJavaType(CompositeCouponHandle.class))); } }
CompositeCacheManager
複合CacheManager實現了給定的委託CacheManager例項集合。允許NoOpCacheManager自動新增到列表末尾,以便在沒有後備儲存的情況下處理快取宣告。否則,任何自定義CacheManager也可以扮演最後一個委託的角色,懶惰地為任何請求的名稱建立快取區域。注意:如果複合管理器委託的常規CacheManagers需要從getCache(String)返回null,如果它們不知道指定的快取名稱,則允許迭代到下一個委託。但是,大多數CacheManager實現都會在請求時回退到命名快取的延遲建立;檢視具有固定快取名稱的“靜態”模式的特定配置詳細資訊(如果有)。
通過CompositeCacheManager 可以配置過個CacheManager,每個CacheManager可以配置不同的序列化器。
public class CompositeCacheManager implements CacheManager, InitializingBean { private final List<CacheManager> cacheManagers = new ArrayList<CacheManager>(); private boolean fallbackToNoOpCache = false; /** * Construct an empty CompositeCacheManager, with delegate CacheManagers to * be added via the {@link #setCacheManagers "cacheManagers"} property. */ public CompositeCacheManager() { } /** * Construct a CompositeCacheManager from the given delegate CacheManagers. * @param cacheManagers the CacheManagers to delegate to */ public CompositeCacheManager(CacheManager... cacheManagers) { setCacheManagers(Arrays.asList(cacheManagers)); } /** * Specify the CacheManagers to delegate to. */ public void setCacheManagers(Collection<CacheManager> cacheManagers) { this.cacheManagers.addAll(cacheManagers); } /** * Indicate whether a {@link NoOpCacheManager} should be added at the end of the delegate list. * In this case, any {@code getCache} requests not handled by the configured CacheManagers will * be automatically handled by the {@link NoOpCacheManager} (and hence never return {@code null}). */ public void setFallbackToNoOpCache(boolean fallbackToNoOpCache) { this.fallbackToNoOpCache = fallbackToNoOpCache; } @Override public void afterPropertiesSet() { if (this.fallbackToNoOpCache) { this.cacheManagers.add(new NoOpCacheManager()); } } @Override public Cache getCache(String name) { for (CacheManager cacheManager : this.cacheManagers) { Cache cache = cacheManager.getCache(name); if (cache != null) { return cache; } } return null; } @Override public Collection<String> getCacheNames() { Set<String> names = new LinkedHashSet<String>(); for (CacheManager manager : this.cacheManagers) { names.addAll(manager.getCacheNames()); } return Collections.unmodifiableSet(names); } }