在Springboot项目中序列化数据到redis中,遇到了一些疑惑,在此总结一下,重点是Java中的序列化。
序列化和反序列化
序列化:简单来说就是将应用程序中的数据转化为特定的格式,可以用于网络传输,保存到磁盘,数据库等。例如Java中的ObjectOutputStream、Python中的pickle是专门的序列化类、Hadoop中各节点数据传输。
反序列化:将序列化对象从磁盘、网络等位置重新转化为程序中的对象信息。
为什么需要序列化
实现数据的跨语言使用
实现数据的跨平台使用
数据去内存地址
降低磁盘存储空间
- 存储对象在存储介质中,以便在下次使用的时候,可以很快捷的重建一个副本。直接从磁盘、网络等位置转化为具体的对象。
- 便于数据传输,尤其是在远程调用的时候!前后端JSON数据交互也是序列化,方便了不同语言、框架之间数据交互。
Java中Serializable接口
查看源码发现该接口没有任何属性和方法,相当于仅仅是一个标记,标记该类是可以被序列化的。
在使用JDK自带的ObjectOutputStream时,序列化的对象必须实现Serializable接口,否则会抛出异常。
/**
* Underlying writeObject/writeUnshared implementation.
*/
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
boolean oldMode = bout.setBlockDataMode(false);
depth++;
try {
// 省略号。。。。。。。。。。
// remaining cases
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
} finally {
depth--;
bout.setBlockDataMode(oldMode);
}
}
Python中的pickle模块和Java中的ObjectOutputStream是类似的,都是编程语言自实现的序列化方式,不能跨语言解析。
serialVersionUID属性
因为序列化对象时,如果不显示的设置serialVersionUID,Java在序列化时会根据对象属性自动的生成一个serialVersionUID,再进行存储或用作网络传输。
在反序列化时,会根据对象属性自动再生成一个新的serialVersionUID,和序列化时生成的serialVersionUID进行比对,两个serialVersionUID相同则反序列化成功,否则就会抛异常。
而当显示的设置serialVersionUID后,Java在序列化和反序列化对象时,生成的serialVersionUID都为我们设定的serialVersionUID,这样就保证了反序列化的成功。
transient关键字
序列化对象时如果希望哪个属性不被序列化,则用transient关键字修饰即可。
反序列化之后,字段为对象类型值为null,基本类型为初始值。
Serializable注意事项
- 静态成员变量是不能被序列化的——序列化是针对对象属性的,而静态成员变量是属于类的。
- 当一个父类实现序列化,子类就会自动实现序列化,不需要显式实现Serializable接口。
- 当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化。
- 序列化之后,若原类的信息经过了修改,反序列化会失败。
SpringBoot中Redis序列化与反序列化
自动配置
默认情况下,Redis自动配置类为我们注入了两个RedisTemplate,一个用于操作Object,一个用于完全操作字符串。
@AutoConfiguration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
Redistemplate源码
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
private boolean enableTransactionSupport = false;
private boolean exposeConnection = false;
private boolean initialized = false;
private boolean enableDefaultSerializer = true;
/* 默认序列化反序列化器 */
private @Nullable RedisSerializer<?> defaultSerializer;
private @Nullable ClassLoader classLoader;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashKeySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = RedisSerializer.string();
private @Nullable ScriptExecutor<K> scriptExecutor;
private final ValueOperations<K, V> valueOps = new DefaultValueOperations<>(this);
private final ListOperations<K, V> listOps = new DefaultListOperations<>(this);
private final SetOperations<K, V> setOps = new DefaultSetOperations<>(this);
private final StreamOperations<K, ?, ?> streamOps = new DefaultStreamOperations<>(this,
ObjectHashMapper.getSharedInstance());
private final ZSetOperations<K, V> zSetOps = new DefaultZSetOperations<>(this);
private final GeoOperations<K, V> geoOps = new DefaultGeoOperations<>(this);
private final HyperLogLogOperations<K, V> hllOps = new DefaultHyperLogLogOperations<>(this);
private final ClusterOperations<K, V> clusterOps = new DefaultClusterOperations<>(this);
/**
* Constructs a new <code>RedisTemplate</code> instance.
*/
public RedisTemplate() {}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.RedisAccessor#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet(){...}
注意以下几点
- 有一个默认的序列化器,defaultSerializer。
- keySerializer、valueSerializer、hashKeySerializer、hashValueSerializer分别对应Redis的 Key, Value, HashKey, HashValue。
- 默认的stringSerializer序列化和反序列化字符串的方式就是UTF8编码解码。
- 各种Options定义了对Redis的各种操作。
- 只有一个无参构造方法。
- 有一个afterPropertiesSet方法,用于设置默认的JDK序列化器,父类RedisAccessor实现了InitializingBean接口。
Object-RedisTemplate
看上面源码可知,new完RedisTemplate(空参构造),并且设置完连接工厂,直接返回了该对象,没有进行任何其他配置。根据上文分析,Bean初始化后,会在afterPropertiesSet方法中检查序列化器,默认使用JDK序列化方式。
StringRedisTemplate
该类构造方法也十分简单,全部设置为stringSerializer,只能序列化和反序列化字符串。
JDK序列化的优缺点
JDK序列化测试
如果使用JDK序列化,几乎不需要做任何配置,相当于上文提到的Serializable接口,ObjectOutputStream的工作方式。
// User类
@Data
@ToString
public class User implements Serializable {
private transient int x = 100;
private Long userId;
private String username;
private LocalDateTime lastLoginTime;
}
/* 单元测试 */
@SpringBootTest
class DeseralizeApplicationTests {
@Test
void contextLoads() {
}
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
@Test
void testJdkSer() {
User user = new User();
user.setUserId(100L);
user.setLastLoginTime(LocalDateTime.now());
redisTemplate.opsForValue().set("sadfasd", user);
user = (User) redisTemplate.opsForValue().get("sadfasd");
System.out.println(user);
}
}
// User(x=100, userId=100, username=null, lastLoginTime=2023-04-10T17:40:06.859769600)
注意以下三点
- User类必须实现Serializable接口。
- 能识别transient关键字。
- Java8 LocalDatetime类型可直接序列化反序列化。
查看Redis中序列化结果
优缺点都比较明显
优点
- 无需配置,开箱即用。
- 无需考虑类序列化兼容问题,例如Java时间模块。
- 支持transient关键字。
- 无需考虑final关键字、构造方法等问题。(示例中未体现)
缺点
- 序列化结果难以阅读,无法跨语言交流,只有JDK自己看得懂。
- 序列化结果太长,占用空间。
- 无法跨语言,其他应用程序无法理解。
- 一旦类的信息修改,序列化会失败。本人实际开发过程中遇到此种情况,对象信息已序列化到redis,此时将类移动包或者修改字段信息(Integer -> Long),再次启动项目会反序列化失败,是因为JDK序列化方式死死地记住了类的全部信息,一旦有任何不一致都会报错,参考未设置serialVersionUID的情况。
关于二者的序列化和反序列化速度问题,本人未作测试,有兴趣可自行尝试。
综合优缺点考虑,实际情况下都不会使用JDK序列化方式,一般使用JSON序列化,简洁,跨语言,占用空间小。
配置JSON序列化
在开始测试之前,修改User类的定义如下
@ToString
@Data
public class User implements Serializable {
private transient int x;
private int y;
private Long userId;
private String username;
private Date lastLoginTime;
private LocalDateTime updateTime;
}
最好先对Jackson的序列化配置有一些了解,可以查看个人网站Jackson配置,或者本人优快云的上的Jackson配置,内容完全一致。
GenericJackson2JsonRedisSerializer
GenericJackson2JsonRedisSerializer是SpringBoot提供的使用Jackson序列化任意对象的类,本质上是调用了ObjectMapper的writeValueAsBytes方法序列化后存入redis,所以后续出现的序列化反序列化的问题需要从ObjectMapper找原因。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
运行测试代码
@Test
void testJdkSer() {
User user = new User();
user.setUserId(100L);
user.setX(666);
user.setLastLoginTime(new Date());
user.setUpdateTime(LocalDateTime.now());
redisTemplate.opsForValue().set("sadfasd", user);
user = (User) redisTemplate.opsForValue().get("sadfasd");
System.out.println(user);
}
报错无法解析Java8的time类型
Could not write JSON: Java 8 date/time type
java.time.LocalDateTime
not supported by default: add Module “com.fasterxml.jackson.datatype:jackson-datatype-jsr310” to enable handling.
如果将updateTime字段去掉,序列化结果为
可以看到
- 序列化将类型信息也保存了,便于反序列化。
- Jackson不认识trasient关键字,似乎只有JDK认?
- 普通类型(int、long、String及其包装类型等)未保存类型信息,Date类型存储了对象信息,但是不是@class字段的方式,(可使用JsonTypeInfo改变)。
上文已经提过了,这种配置下,所有的序列化行为都由ObjectMapper完成,所以,弄清上述问题,看一下GenericJackson2JsonRedisSerializer的源码就好了。
public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {
this(new ObjectMapper());
// simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need
// the type hint embedded for deserialization using the default typing feature.
registerNullValueSerializer(mapper, classPropertyTypeName);
StdTypeResolverBuilder typer = new TypeResolverBuilder(DefaultTyping.EVERYTHING,
mapper.getPolymorphicTypeValidator());
typer = typer.init(JsonTypeInfo.Id.CLASS, null);
typer = typer.inclusion(JsonTypeInfo.As.PROPERTY);
if (StringUtils.hasText(classPropertyTypeName)) {
typer = typer.typeProperty(classPropertyTypeName);
}
mapper.setDefaultTyping(typer);
}
注意三行代码
- DefaultTyping.EVERYTHING,序列化时把所有类的类型信息序列化,除Integer、Double、String、Boolean及其基本类型(不能是多态的)及数组之外。似乎与实际情况不符,上图中Long类型未被序列化?为什么上图Long类型没有被序列化,答案在TypeResolverBuilder的useForType中,感兴趣可自行查看。
- JsonTypeInfo.Id.CLASS,即@class字段
- JsonTypeInfo.As.PROPERTY,类型信息作为JSON字段。
其他枚举配置的含义可以查看Jackson枚举类的源码或本人其他博客。
总结一下这种配置的缺点
- 不支持Java8时间类型。
- 序列化带了类信息。
自动识别类型
这是网上能搜到的较多的配置,已添加详细注释
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
/**
* 属性可见,默认是
* Only public fields visible
* Only public getters, is-getters visible
* All setters (regardless of access) visible
* Only public Creators visible
*/
mapper.setDefaultVisibility(JsonAutoDetect.Value.defaultVisibility());
/**
* 如果值为null,不序列化
*/
mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
/**
* 所有非最终类型(NON_FINAL)序列化类型信息
*/
mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
/**
* Java8时间模块
*/
JavaTimeModule javaTimeModule = new JavaTimeModule();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDateTimeSerializer dateTimeSerializer = new LocalDateTimeSerializer(dateTimeFormatter);
LocalDateSerializer localDateSerializer = new LocalDateSerializer(dateFormatter);
LocalDateTimeDeserializer dateTimeDeserializer = new LocalDateTimeDeserializer(dateTimeFormatter);
LocalDateDeserializer localDateDeserializer = new LocalDateDeserializer(dateFormatter);
javaTimeModule.addSerializer(LocalDateTime.class, dateTimeSerializer);
javaTimeModule.addSerializer(LocalDate.class, localDateSerializer);
javaTimeModule.addDeserializer(LocalDateTime.class, dateTimeDeserializer);
javaTimeModule.addDeserializer(LocalDate.class, localDateDeserializer);
mapper.registerModule(javaTimeModule);
jsonRedisSerializer.setObjectMapper(mapper);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
这样配置已经可以使用了,就是序列化了类型信息,以及类一旦发生变化,反序列会失败。
手动指定反序列化类型
这种方式避免了不必要的类型信息,方便数据在不同应用之间共享。
因为序列化是在本地完成的,存入到Redis时都是字符串,实际上我们只需要用到StringRedisTemplate和ObjectMapper,编写简单RedisService如下
@Service
public class RedisService {
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
public RedisService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
this.objectMapper = buildMapper();
}
private ObjectMapper buildMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
JavaTimeModule javaTimeModule = new JavaTimeModule();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDateTimeSerializer dateTimeSerializer = new LocalDateTimeSerializer(dateTimeFormatter);
LocalDateSerializer localDateSerializer = new LocalDateSerializer(dateFormatter);
LocalDateTimeDeserializer dateTimeDeserializer = new LocalDateTimeDeserializer(dateTimeFormatter);
LocalDateDeserializer localDateDeserializer = new LocalDateDeserializer(dateFormatter);
javaTimeModule.addSerializer(LocalDateTime.class, dateTimeSerializer);
javaTimeModule.addSerializer(LocalDate.class, localDateSerializer);
javaTimeModule.addDeserializer(LocalDateTime.class, dateTimeDeserializer);
javaTimeModule.addDeserializer(LocalDate.class, localDateDeserializer);
mapper.registerModule(javaTimeModule);
return mapper;
}
public void set(String key, Object value) throws JsonProcessingException {
redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(value));
}
public <T> T get(String key, Class<T> requiredType) throws JsonProcessingException {
String s = redisTemplate.opsForValue().get(key);
return objectMapper.readValue(s, requiredType);
}
}
执行测试代码,可以看到是我们想要的序列化结果。唯一不足的就是序列化反序列化过程中可能抛出的异常不知道如何优雅处理。
补充
在实际开发过程中,也许类的定义没有那么简单,比如无默认构造方法,有final属性,部分字段、getter方法不想序列化等,这些都是可以通过Jackson配置针对某个特定类来实现的,比如定义属性可见性,@JsonCreator注解等。