前段时间 Sa-Token 发布了 1.46.0 版本,看了官网的更新日志后,发现优化了一些问题和增加了很多的新功能,果断想更新到最新版本。可是更新版本号后启动项目访问却出现了几个问题,这个记录一下,帮助大家少踩一点坑。
1、KEEPTTL 的问题
Redis 6.0 开始,新增了 KEEPTTL 的支持,当你更新 Redis 里面的数据时,可以保持原有数据的 TTL 的值,属于原子操作,不需要你先去获取 TTL ,再设置 TTL。
Sa-Token 中的代码是这样写的:
java
/**
* SET key value XX KEEPTTL:仅 key 存在时覆写 value,并保留原 TTL
*/
public void setStringAndKeepTTL(String finalKey, String value) {
stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.set(
stringRedisTemplate.getStringSerializer().serialize(finalKey),
stringRedisTemplate.getStringSerializer().serialize(value),
Expiration.keepTtl(),
RedisStringCommands.SetOption.ifPresent()
)
);
}
启动代码,进行登录时却报了以下的错误:
text
Caused by: org.redisson.client.RedisException: ERR invalid expire time in 'set' command. channel: [id: 0x741630d3, L:/127.0.0.1:57730 - R:127.0.0.1/127.0.0.1:6379] command: (SET), params: [[65, 117, 116, 104, 111, 114, 105, 122, 97, 116, ...], [123, 34, 64, 99, 108, 97, 115, 115, 34, 58, ...], PX, -2000, XX], promise: java.util.concurrent.CompletableFuture@1ce80367[Not completed, 1 dependents]
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:439)
at org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:220)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:148)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:124)
at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:545)
at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1429)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918)
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:176)
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.handle(AbstractNioChannel.java:445)
at io.netty.channel.nio.NioIoHandler$DefaultNioRegistration.handle(NioIoHandler.java:388)
at io.netty.channel.nio.NioIoHandler.processSelectedKey(NioIoHandler.java:596)
at io.netty.channel.nio.NioIoHandler.processSelectedKeysOptimized(NioIoHandler.java:571)
at io.netty.channel.nio.NioIoHandler.processSelectedKeys(NioIoHandler.java:512)
at io.netty.channel.nio.NioIoHandler.run(NioIoHandler.java:484)
at io.netty.channel.SingleThreadIoEventLoop.runIo(SingleThreadIoEventLoop.java:225)
at io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:196)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1195)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
... 1 common frames omitted
大致意思是,设置的 TTL 值为 -2000,Redis 服务那边不支持设置负数的 TTL,所以报错了。当我进行排查后,就发现了,SpringBoot 4.1.0 版本中对这种写法已经标注过时了,我觉得就是过时的 API 导致计算出来的 TTL 不对。官网给出了解决方案:
bash
https://sa-token.com/more/common-questions.html#Q:Redis-6.0-以下版本集成报错:ERR-syntax-error
给出了2种解决方案:
- 升级 Redis 服务到 6.0 以上
我的 Redis 服务版本为 8.0.3,所以是支持 KEEPTTL 的。
- 重写 update 方法
java
@Configuration
public class SaTokenDaoConfig {
@Bean
@Primary
public SaTokenDao saTokenDao() {
return new SaTokenDaoForRedisTemplate() {
@Override
public void update(String key, String value) {
String finalKey = wrapKey(key);
long expireMs = stringRedisTemplate.getExpire(finalKey, TimeUnit.MILLISECONDS);
// -2 = 无此键
if (expireMs == SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
// -1 = 永不过期
if (expireMs == SaTokenDao.NEVER_EXPIRE) {
stringRedisTemplate.opsForValue().set(finalKey, value);
} else {
stringRedisTemplate.opsForValue().set(finalKey, value, expireMs, TimeUnit.MILLISECONDS);
}
}
};
}
}
重写的思路大致就是需要自己先获取 TTL 的值,再决定如何去更新数据。但是这样就变成了不是原子操作了,我给出另一种写法如下:
java
@Configuration
public class SaTokenDaoConfig {
@Bean
@Primary
public SaTokenDao saTokenDao() {
return new SaTokenDaoForRedisTemplate() {
@Override
public void update(String key, String value) {
String finalKey = wrapKey(key);
stringRedisTemplate.opsForValue().setIfPresent(finalKey, value, Expiration.keepTtl());
}
};
}
}
2、JSON 反序列化的问题
官网中说引入了 sa-token-spring-boot4-starter 会自动引入 sa-token-jackson3 作为默认 JSON 方案。当我往 Redis 中写入登录用户的个人信息对象后,取出来的时候却报了以下的错误:
text
Caused by: tools.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'com.biz.common.vo.resp.LoginUserRespVO' as a subtype of `java.lang.Object`: Configured `PolymorphicTypeValidator` (of type `tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator`) denied resolution
at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); byte offset: #UNKNOWN] (through reference chain: cn.dev33.satoken.session.SaSession["dataMap"]->java.util.concurrent.ConcurrentHashMap["user_info"])
at tools.jackson.databind.exc.InvalidTypeIdException.from(InvalidTypeIdException.java:41)
at tools.jackson.databind.DeserializationContext.invalidTypeIdException(DeserializationContext.java:2143)
at tools.jackson.databind.DatabindContext._throwSubtypeClassNotAllowed(DatabindContext.java:348)
at tools.jackson.databind.DatabindContext.resolveAndValidateSubType(DatabindContext.java:240)
at tools.jackson.databind.jsontype.impl.ClassNameIdResolver._typeFromId(ClassNameIdResolver.java:87)
at tools.jackson.databind.jsontype.impl.ClassNameIdResolver.typeFromId(ClassNameIdResolver.java:70)
at tools.jackson.databind.jsontype.impl.TypeDeserializerBase._findDeserializer(TypeDeserializerBase.java:154)
at tools.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:119)
at tools.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:103)
at tools.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromAny(AsPropertyTypeDeserializer.java:203)
at tools.jackson.databind.deser.jdk.UntypedObjectDeserializerNR.deserializeWithType(UntypedObjectDeserializerNR.java:98)
at tools.jackson.databind.deser.jdk.MapDeserializer._deserializeNoNullChecks(MapDeserializer.java:885)
at tools.jackson.databind.deser.jdk.MapDeserializer._readAndBindStringKeyMap(MapDeserializer.java:606)
at tools.jackson.databind.deser.jdk.MapDeserializer.deserialize(MapDeserializer.java:428)
at tools.jackson.databind.deser.jdk.MapDeserializer.deserialize(MapDeserializer.java:30)
at tools.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:138)
at tools.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:103)
at tools.jackson.databind.deser.jdk.MapDeserializer.deserializeWithType(MapDeserializer.java:471)
at tools.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
at tools.jackson.databind.deser.bean.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:659)
at tools.jackson.databind.deser.bean.BeanDeserializer._deserializeOther(BeanDeserializer.java:235)
at tools.jackson.databind.deser.bean.BeanDeserializer.deserialize(BeanDeserializer.java:202)
at tools.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:138)
at tools.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:103)
at tools.jackson.databind.deser.bean.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1395)
at tools.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:72)
at tools.jackson.databind.deser.DeserializationContextExt.readRootValue(DeserializationContextExt.java:266)
at tools.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:2639)
at tools.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:1544)
at cn.dev33.satoken.json.SaJsonTemplateForJackson3.jsonToObject(SaJsonTemplateForJackson3.java:96)
... 92 common frames omitted
官网给出了解决方案:
bash
https://sa-token.com/plugin/json-extend.html#JSON-全局类型白名单机制
注册 JSON 全局类型白名单的几种方式:
- 实体类实现 SaJsonType(推荐)
业务 Model 实现标记接口 SaJsonType 即可加入白名单,无需额外配置:
java
public class SysUser implements SaJsonType {
// ...
}
- 启动前调用 registerAllowType
在 JSON 插件完成初始化之前 注册(Spring Boot 请在 main 方法里、SpringApplication.run 之前;Solon 请在 Solon.start 之前):
java
import cn.dev33.satoken.strategy.SaJsonStrategy;
public static void main(String[] args) {
// 在项目启动前,将所有需要反序列化的 Bean Class 进行注册
SaJsonStrategy.instance.registerAllowType(SysUser.class);
SpringApplication.run(Application.class, args);
}
- 通过 SPI 文件批量声明
在 resources/META-INF/satoken/sa-json-type.list 中按行写入完整类名(# 开头为注释):
text
# 允许参与多态 JSON 反序列化的业务类型
com.pj.model.SysUser
com.pj.model.SysRole
如您在阅读中发现不足,欢迎留言!!!