虽然理论上 Koltin “NTR” 了 Java 的一切,但是在实际开发中总能碰到许多奇奇怪怪的问题。
添加依赖 首先是 Redis 的依赖:
1 2 3 dependencies { implementation("org.springframework.boot:spring-boot-starter-data-redis" ) }
这个包会通过反射来加载一些 class,这就要求 class 必需有一个无参的构造函数,而 Kotlin 的 data class 默认没有无参构造函数,并且 data class 默认为 final 类型,不可以被继承,会出现下列错误:
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of …
Kotlin 官方为我们提供了两个插件:
无参编译器插件 NoArg 插件的作用是:为指定的类在编译时添加一个无参的构造。
全开放编译器插件 AllOpen 的作用是:使用指定的注解标注类而其成员无需显式使用 open 关键字打开。
Spring Boot 只需要 NoArg 插件,因为添加的 kotlin-spring 插件包含 AllOpen 了,会自动给 Spring 注解类 open ,下面的只作为演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 plugins { val kotlinVersion = "1.6.21" id("org.jetbrains.kotlin.plugin.noarg" ) version kotlinVersion id("org.jetbrains.kotlin.plugin.allopen" ) version kotlinVersion kotlin("plugin.jpa" ) version kotlinVersion } allOpen { annotation ("com.gitfy.gitfyapi.util.AllOpen" ) } noArg { annotation ("com.gitfy.gitfyapi.util.NoArg" ) }
特别说明一下这俩插件的使用,除了在 plugins
里添加依赖,还需要加上上面的俩注解声明,其中前面的是声明的注解类所在包,比如我在 om.gitfy.gitfyapi.util
包下新建 Annotations.kt
:
1 2 3 4 5 package com.gitfy.gitfyapi.utilannotation class NoArg annotation class AllOpen
这些弄完就可以在 data class 里使用注解了:
1 2 3 4 5 6 @NoArg data class Repo ( var platform: String, var owner: String, var repo: String, )
如果项目中已经添加了 kotlin-jpa
插件,那么基本上就不必单独添加无参插件了。
kotlin-jpa
对无参插件做了包装,当使用 @Entity
、 @Embeddable
与 @MappedSuperclass
这几个注解时,都会默认支持无参注解的。
当然也可以手动给 data class 赋初始值。
添加配置 这里将 key
序列化为 string
,value
序列化为 json
。
SpringBoot 的缓存使用 jackson 来做数据的序列化与反序列化,如果默认使用 Any 作为序列化与反序列化的类型,则其只能识别基本类型,遇到复杂类型时,jackson 就会先序列化成 LinkedHashMap ,然后再尝试强转为所需类别,这样大部分情况下会强转失败,出现以下错误:
class java.util.LinkedHashMap cannot be cast to class …
解决方法是修改 RedisTemplate 这个 bean 的 valueSerializer,即设置默认类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 @Configuration class RedisConfig { private fun serializer () : Jackson2JsonRedisSerializer<Any> { val jackson2JsonRedisSerializer = Jackson2JsonRedisSerializer(Any::class .java) val objectMapper = ObjectMapper().apply { setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY) activateDefaultTyping( this .polymorphicTypeValidator, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY ) } jackson2JsonRedisSerializer.setObjectMapper(objectMapper) return jackson2JsonRedisSerializer } @Bean fun redisTemplate (redisConnectionFactory: RedisConnectionFactory ) : RedisTemplate<String, Any> { val redisTemplate = RedisTemplate<String, Any>() val stringRedisSerializer = StringRedisSerializer() val jackson2JsonRedisSerializer = serializer() redisTemplate.apply { setConnectionFactory(redisConnectionFactory) keySerializer = stringRedisSerializer valueSerializer = jackson2JsonRedisSerializer hashKeySerializer = stringRedisSerializer hashValueSerializer = jackson2JsonRedisSerializer afterPropertiesSet() } return redisTemplate } }
封装工具 为了方便,封装一个简单的工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @Component class RedisUtil { @Autowired private lateinit var redisTemplate: RedisTemplate<String, Any> fun get (key: String ) : Any? { if (key == "" ) return null try { return redisTemplate.opsForValue().get (key) } catch (e: Exception) { e.printStackTrace() } return null } fun set (key: String , value: Any , expireTime: Long ? = null ) : Boolean { if (key == "" ) return false try { redisTemplate.opsForValue().set (key, value) expireTime?.let { redisTemplate.expire(key, it, TimeUnit.SECONDS) } return true } catch (e: Exception) { e.printStackTrace() } return false } }
测试使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @RunWith(SpringJUnit4ClassRunner::class) @SpringBootTest class RedisUtilTest { @Autowired private lateinit var redisUtil: RedisUtil @Test fun get () { val repo = redisUtil.get ("github:SukiEva:GitfyApi" ) println(repo) val obj:Repo = repo as Repo println(obj) } @Test fun set () { val repo = Repo("github" , "SukiEva" , "GitfyApi" ) redisUtil.set ("github:SukiEva:GitfyApi" , repo) } }
如果你也是 Kotlin 玩家,这里肯定会碰到报错:
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Could not resolve subtype of [simple type, class java.lang.Object]: missing type id property '@class'
在 Java
中,实体类没有任何额外配置,Redis
序列化/反序列化一样没有问题,是因为值序列化器 GenericJackson2JsonRedisSerializer
,该类会自动添加一个 @class
字段,因此不会出现上面的问题。
但是在 Kotlin
中,类默认不是 open
的,也就是无法添加 @class
字段,因此便会反序列化失败。
首先当然可以通过不用 data class 来解决,直接给普通 class open,这当然不是我们想要的,所以需要添加一个类注解 @JsonTypeInfo
:
1 2 3 4 5 6 7 @NoArg @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) data class Repo ( var platform: String, var owner: String, var repo: String, )
该注解的 use
用于指定类型标识码,该值只能为 JsonTypeInfo.Id.CLASS
。
这时 Redis 中的结构是:
1 2 3 4 5 6 { "@class" : "com.gitfy.gitfyapi.pojo.Repo" , "platform" : "github" , "owner" : "SukiEva" , "repo" : "GitfyApi" }
并且此时 get
方法也能正常获取值,并强制类型转换为对应的对象。