SpringBoot 如何實現自定義Redis序列化
問題
在使用RedisTemplate存儲對象時,如果采用JDK默認的序列化方式,數據會出現許多編碼字符,辨析度不高。比如一個空的User對象,存儲到redis後如下:
這些使用JDK默認序列化方式序列化後的數據簡直慘不忍睹,在使用命令行查詢數據時會很頭疼。
如何使數據更容易辨別呢?
一種辦法是使用StringRedisTemplate,在存入redis前先將數據處理成字符串格式再存入redis,但這種方式的缺點就是每次存入數據前都要手動對非字符串數據進行處理。
另一種方法就是自定義序列化方式,隻需要使用RedisTemplate就能按照自定義的序列化方式存儲對象。
這裡使用的是第二種方法。
環境
這裡使用的SpringBoot2.0.5版本。
依賴信息:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.9.9</version> </dependency> </dependencies>
SpringBoot啟動類:
@SpringBootApplication public class App { public static void main(String[] args) { SpringApplication.run(App.class); } }
User實體類:
public class User implements Serializable { private String username; private String password; private DateTime birthday; public DateTime getBirthday() { return birthday; } public void setBirthday(DateTime birthday) { this.birthday = birthday; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "User{" + "username='" + username + '\'' + ", password='" + password + '\'' + ", birthday=" + birthday + '}'; } }
測試類:
@RunWith(SpringRunner.class) @SpringBootTest public class RedisTest { @Autowired private RedisTemplate redisTemplate; @Test public void testRedis(){ User user = new User(); user.setUsername("charviki"); user.setPassword("123456"); redisTemplate.opsForValue().set("user", user); User user1 = (User) redisTemplate.opsForValue().get("user"); System.out.println(user1); } }
入口點
當引入redis啟動器時,SpringBoot通過RedisTemplate這個類自動幫我們配置瞭許多默認參數,包括redis主機,默認序列化方式等。找到RedisAutoConfiguration這個類,這個類中有如下代碼:
這裡使用瞭註解@ConditionalOnMissingBean(name = “redisTemplate”),大致意思就是如果Spring容器中沒有RedisTemplate這個bean,就會返回一個默認的RedisTemplate(配置信息都在這個類裡面)。
到這裡就有大致的思路瞭,要想實現自定義redis序列化,首先定義一個返回類型為RedisTemplate的bean,並將該bean交由Spring容器管理。
實現自定義序列化
定義RedisConfig類,自定義序列化:
@Component public class RedisConfig { @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){ RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 自定義key序列化方式,直接將String字符串直接作為redis中的key StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(stringRedisSerializer); // 自定義value序列化方式,序列化成json格式 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); return redisTemplate; } }
運行測試類,程序報錯,錯誤信息如下:
java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to cn.charviki.pojo.User
at cn.charviki.test.RedisTest.testRedis(RedisTest.java:33)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73)
at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
先不管錯誤信息,先去在redis中查看數據,如下:
也就是說數據存進去瞭,但是取不出來。
這個時候回去看錯誤信息:
java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to cn.charviki.pojo.User
看異常名也可以知道,類轉換異常,即從redis中取數據後反序列化異常。
這個異常出現的原因是在序列化的時候我們沒有加入類信息,取出來的時候jvm找不到類信息,無法將該json數據轉換對應的類。
解決這個問題隻需要在對值序列化的時候加入類信息,修改redisTemplate方法如下:
@Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){ RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 自定義key序列化方式,直接將String字符串直接作為redis中的key StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(stringRedisSerializer); // 自定義value序列化方式,序列化成json格式 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); //jackson底層的序列化和反序列化使用的是ObjectMapper,我們可以通過ObjectMapper設置序列化信息 // 設置值的默認類型,即類信息 ObjectMapper objectMapper = new ObjectMapper(); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // 添加進序列化中 jackson2JsonRedisSerializer.setObjectMapper(objectMapper); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); return redisTemplate; }
再次運行測試類,控制臺打印信息如下:
這裡我們就實現瞭將對象使用json的序列化方式,但是這裡會出現一個問題,就是當對象中成員變量的數據類型不是JDK中的數據類型時就會出現問題。比如說User類中有一個DateTime類型的birthday變量,這個DateTime是joda-time包下的一個日期類,在上面pom文件中已經引入瞭依賴。在上面測試類中我們沒有為這個變量值賦值,現在讓我們修改測試類:
@Test public void testRedis(){ User user = new User(); user.setUsername("charviki"); user.setPassword("123456"); // 這裡打印出dateTime,方便和redis中對比 DateTime dateTime = new DateTime(); System.out.println("dateTime = " + dateTime); user.setBirthday(dateTime); redisTemplate.opsForValue().set("user",user); User user1 = (User) redisTemplate.opsForValue().get("user"); System.out.println(user1); }
運行測試類,這個時候程序報錯,錯誤信息如下:
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Unrecognized field “era” (class org.joda.time.DateTime), not marked as ignorable (2 known properties: “chronology”, “millis”])
at [Source: (byte[])”[“cn.charviki.pojo.User”,{“username”:”charviki”,”password”:”123456″,”birthday”:{“era”:1,”dayOfMonth”:16,”dayOfWeek”:3,”dayOfYear”:289,”year”:2019,”hourOfDay”:15,”minuteOfHour”:30,”yearOfEra”:2019,”yearOfCentury”:19,”monthOfYear”:10,”weekyear”:2019,”centuryOfEra”:20,”millisOfDay”:55811047,”secondOfDay”:55811,”minuteOfDay”:930,”weekOfWeekyear”:42,”millisOfSecond”:47,”secondOfMinute”:11,”zone”:[“org.joda.time.tz.CachedDateTimeZone”,{“fixed”:false,”uncachedZone”:[“org.joda.time.tz.DateTimeZoneBuilde”[truncated 439 bytes]; line: 1, column: 88] (through reference chain: cn.charviki.pojo.User[“birthday”]->org.joda.time.DateTime[“era”]); nested exception is com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field “era” (class org.joda.time.DateTime), not marked as ignorable (2 known properties: “chronology”, “millis”])
at [Source: (byte[])”[“cn.charviki.pojo.User”,{“username”:”charviki”,”password”:”123456″,”birthday”:{“era”:1,”dayOfMonth”:16,”dayOfWeek”:3,”dayOfYear”:289,”year”:2019,”hourOfDay”:15,”minuteOfHour”:30,”yearOfEra”:2019,”yearOfCentury”:19,”monthOfYear”:10,”weekyear”:2019,”centuryOfEra”:20,”millisOfDay”:55811047,”secondOfDay”:55811,”minuteOfDay”:930,”weekOfWeekyear”:42,”millisOfSecond”:47,”secondOfMinute”:11,”zone”:[“org.joda.time.tz.CachedDateTimeZone”,{“fixed”:false,”uncachedZone”:[“org.joda.time.tz.DateTimeZoneBuilde”[truncated 439 bytes]; line: 1, column: 88] (through reference chain: cn.charviki.pojo.User[“birthday”]->org.joda.time.DateTime[“era”])at org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer.deserialize(Jackson2JsonRedisSerializer.java:75)
at org.springframework.data.redis.core.AbstractOperations.deserializeValue(AbstractOperations.java:334)
at org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:60)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:224)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:184)
at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:95)
at org.springframework.data.redis.core.DefaultValueOperations.get(DefaultValueOperations.java:48)
at cn.charviki.test.RedisTest.testRedis(RedisTest.java:34)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73)
at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field “era” (class org.joda.time.DateTime), not marked as ignorable (2 known properties: “chronology”, “millis”])
at [Source: (byte[])”[“cn.charviki.pojo.User”,{“username”:”charviki”,”password”:”123456″,”birthday”:{“era”:1,”dayOfMonth”:16,”dayOfWeek”:3,”dayOfYear”:289,”year”:2019,”hourOfDay”:15,”minuteOfHour”:30,”yearOfEra”:2019,”yearOfCentury”:19,”monthOfYear”:10,”weekyear”:2019,”centuryOfEra”:20,”millisOfDay”:55811047,”secondOfDay”:55811,”minuteOfDay”:930,”weekOfWeekyear”:42,”millisOfSecond”:47,”secondOfMinute”:11,”zone”:[“org.joda.time.tz.CachedDateTimeZone”,{“fixed”:false,”uncachedZone”:[“org.joda.time.tz.DateTimeZoneBuilde”[truncated 439 bytes]; line: 1, column: 88] (through reference chain: cn.charviki.pojo.User[“birthday”]->org.joda.time.DateTime[“era”])
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:60)
at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:822)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:1152)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1589)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1567)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:294)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:127)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:116)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromAny(AsArrayTypeDeserializer.java:71)
at com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer$Vanilla.deserializeWithType(UntypedObjectDeserializer.java:712)
at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:68)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3129)
at org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer.deserialize(Jackson2JsonRedisSerializer.java:73)
… 37 more
跟前面一樣先查看redis中的數據:
查看控制臺打印的DataTime數據:
對比控制臺數據和redis中的數據,redisTemplate將DataTime使用默認的json序列化後,多瞭許多字段。
再看報錯信息,問題同樣出在反序列化上。從報錯信息中我們可以看出,在反序列化的時候找不到相應的字段。這裡的解決版本是實現針對DataTime類型的序列化和反序列化器,註冊到ObjectMapper中,實現對json序列化器的擴展。
先自定義序列化器,這裡定義序列化器JodaDateTimeJsonSerializer和反序列化器JodaDateTimeJsonDeserializer,兩者都要繼承JsonDeserializer並重寫父類serialize()或deserialize方法。實現代碼如下:
// JodaDateTimeJsonSerializer.java public class JodaDateTimeJsonSerializer extends JsonSerializer<DateTime> { @Override public void serialize(DateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { // 序列化 gen.writeString(value.toString("yyyy-MM-dd HH:mm:ss")); } }
// JodaDateTimeJsonDeserializer.java public class JodaDateTimeJsonDeserializer extends JsonDeserializer<DateTime> { @Override public DateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { String dateString = p.readValueAs(String.class); DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); // 反序列化 return DateTime.parse(dateString,dateTimeFormatter); } }
將自定義序列化器通過ObjectMapper註冊到json序列化器中,修改redisTemplate方法如下:
@Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){ RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 自定義key序列化方式,直接將String字符串直接作為redis中的key StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(stringRedisSerializer); // 自定義value序列化方式,序列化成json格式 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); //jackson底層的序列化和反序列化使用的是ObjectMapper,我們可以通過ObjectMapper設置序列化信息 // 設置值的默認類型,即類信息 ObjectMapper objectMapper = new ObjectMapper(); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // SimpleModule用於設置自定義序列化器 SimpleModule simpleModule = new SimpleModule(); simpleModule.addSerializer(DateTime.class,new JodaDateTimeJsonSerializer()); simpleModule.addDeserializer(DateTime.class,new JodaDateTimeJsonDeserializer()); objectMapper.registerModule(simpleModule); // 添加進json序列化器中 jackson2JsonRedisSerializer.setObjectMapper(objectMapper); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); return redisTemplate; }
這個時候再運行測試類就沒什麼問題瞭。
控制臺打印信息如下:
redis中的數據信息如下:
網上還有一種更加簡便的方法就是使用jackson提供的包,引入依賴:
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-joda</artifactId> <version>2.9.9</version> </dependency>
這個包已經幫我們實現瞭關於joda-time的序列化與反序列化器,我們隻需要在redisTemplate方法中將對應的SimpleModel註冊到ObjectMapper中就行:
objectMapper.registerModule(new JodaModule());
這樣也可以達到同樣的效果。
小結
總之,想要在SpringBoot中實現redis的自定義序列化,需要自定義創建一個redisTemplate的bean,設置要使用的序列化方式。通過ObjectMapper設置一些自定義序列化信息,如反序列化所要用到的類信息等。還可以對特定的數據類型進行自定義序列化,隻需要通過SimpleModel註冊到相應的序列化器即可。最後再將該bean交由Spring容器管理。
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- 解決SpringBoot下Redis序列化亂碼的問題
- 解決json串和實體類字段不一致的問題
- springboot2.5.0和redis整合配置詳解
- SpringBoot結合Redis實現序列化的方法詳解
- 使用註解實現Redis緩存功能