【SpringBoot4】3、从SpringBoot4升级Sa-Token1.46.0报错

前段时间 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种解决方案:

  1. 升级 Redis 服务到 6.0 以上

我的 Redis 服务版本为 8.0.3,所以是支持 KEEPTTL 的。

  1. 重写 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 全局类型白名单的几种方式:

  1. 实体类实现 SaJsonType(推荐)

业务 Model 实现标记接口 SaJsonType 即可加入白名单,无需额外配置:

java 复制代码
public class SysUser implements SaJsonType {
	// ...
}
  1. 启动前调用 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);
}
  1. 通过 SPI 文件批量声明

在 resources/META-INF/satoken/sa-json-type.list 中按行写入完整类名(# 开头为注释):

text 复制代码
# 允许参与多态 JSON 反序列化的业务类型
com.pj.model.SysUser
com.pj.model.SysRole

如您在阅读中发现不足,欢迎留言!!!

相关推荐
代码中介商4 小时前
C++ 预约系统实战(三):服务端实现——libevent 事件驱动与业务路由
c++·json·c/s
zhoupenghui1686 小时前
Redis 集群的安装配置与服务交互
redis·cluster·redis集群
Wang's Blog8 小时前
Java 接入Redis: Redis下载与源码编译安装
java·服务器·redis
焦虑的说说10 小时前
redis知识汇总
java·redis
Wang's Blog10 小时前
Java 接入Redis: 密码认证与远程连接配置
java·服务器·redis
Wang's Blog12 小时前
Java 接入Redis: Redis简介与NoSQL定位
java·服务器·redis
程序猿乐锅12 小时前
【黑马点评 | 第四篇】Redis缓存雪崩
java·数据库·spring boot·redis·缓存
Wang's Blog13 小时前
Java 接入Redis: 五大数据类型与存储结构选型
java·服务器·redis
Wang's Blog13 小时前
Java 接入Redis: 字符串与哈希类型操作命令
java·服务器·redis