文章目录
- 1.背景
- 2. 原来项目配置
- 3.正确配置
- 4.小结
1.背景
最近项目上 使用 SpringBoot 2.7.7 + jackson + redis 框架实现将javaBean 序列化和反序列化到 redis 中。但是最近在做登陆的时候将LoginUser 序列化到redis 中没问题,不重启服务的话反序列化成对象也没有问题。但是重启服务后报错,错误信息
t [Source: (byte[])"["com.dechnic.oms.common.core.domain.model.LoginUser",{"userId":1,"deptId":1,"token":"40dda8a0-9d33-4097-90b7-fa5a5407f3d4","loginTime":"2023-02-04 16:22:08","expireTime":"2023-02-04 16:52:08","ipaddr":"127.0.0.1","permissions":["java.util.HashSet",["*:*:*"]],"sysUserVO":["com.dechnic.oms.common.core.vo.SysUserVO_$$_jvstb8_0",{"userId":1,"deptId":1,"deptName":"qqqqq","userName":"admin","nickName":"超级管理员","userType":"01","status":"0","expirationDate":"2099-12-31","roleName":"管理员","[truncated 82 bytes]; line: 1, column: 329] (through reference chain: com.dechnic.oms.common.core.domain.model.LoginUser["sysUserVO"])
at org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer.deserialize(Jackson2JsonRedisSerializer.java:75)
at org.springframework.data.redis.core.AbstractOperations.deserializeValue(AbstractOperations.java:360)
at org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:62)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:224)
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:191)
at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:97)
at org.springframework.data.redis.core.DefaultValueOperations.get(DefaultValueOperations.java:54)
at com.dechnic.oms.common.core.redis.RedisCache.getCacheObject(RedisCache.java:104)
at com.dechnic.oms.framework.service.TokenService.verifyToken(TokenService.java:141)
at com.dechnic.oms.framework.security.filter.JwtAuthenticationTokenFilter.doFilterInternal(JwtAuthenticationTokenFilter.java:35)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:111)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:91)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:112)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:82)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:221)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:186)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:354)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:177)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:891)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1784)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
Caused by: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'com.dechnic.oms.common.core.vo.SysUserVO_$$_jvstb8_0' as a subtype of `com.dechnic.oms.common.core.vo.SysUserVO`: no such class found
at [Source: (byte[])"["com.dechnic.oms.common.core.domain.model.LoginUser",{"userId":1,"deptId":1,"token":"40dda8a0-9d33-4097-90b7-fa5a5407f3d4","loginTime":"2023-02-04 16:22:08","expireTime":"2023-02-04 16:52:08","ipaddr":"127.0.0.1","permissions":["java.util.HashSet",["*:*:*"]],"sysUserVO":["com.dechnic.oms.common.core.vo.SysUserVO_$$_jvstb8_0",{"userId":1,"deptId":1,"deptName":"qqqqq","userName":"admin","nickName":"超级管理员","userType":"01","status":"0","expirationDate":"2099-12-31","roleName":"管理员","[truncated 82 bytes]; line: 1, column: 329] (through reference chain: com.dechnic.oms.common.core.domain.model.LoginUser["sysUserVO"])
at com.fasterxml.jackson.databind.exc.InvalidTypeIdException.from(InvalidTypeIdException.java:43)
at com.fasterxml.jackson.databind.DeserializationContext.invalidTypeIdException(DeserializationContext.java:2073)
at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownTypeId(DeserializationContext.java:1564)
at com.fasterxml.jackson.databind.jsontype.impl.ClassNameIdResolver._typeFromId(ClassNameIdResolver.java:76)
at com.fasterxml.jackson.databind.jsontype.impl.ClassNameIdResolver.typeFromId(ClassNameIdResolver.java:66)
at com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase._findDeserializer(TypeDeserializerBase.java:159)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:97)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromObject(AsArrayTypeDeserializer.java:61)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1292)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:138)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:314)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:177)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:120)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromAny(AsArrayTypeDeserializer.java:71)
at com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer$Vanilla.deserializeWithType(UntypedObjectDeserializer.java:781)
at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:74)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4674)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3731)
at org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer.deserialize(Jackson2JsonRedisSerializer.java:73)
... 66 common frames omitted
2. 原来项目配置
RedisConfig 配置
package com.dechnic.oms.framework.config;
import com.dechnic.oms.common.utils.MapperUtils;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
/**
* redis配置
* @author zhangzs
* @date 2023/1/4 9:54
* @Description
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
//FAIL_ON_UNKNOWN_PROPERTIES在序列化的时候,如果遇到不认识的字段的处理方式
//默认启用特性,这意味着在遇到未知属性时抛出JsonMappingException。在引入该特性之前,这是默认的默认设置。
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
//FAIL_ON_EMPTY_BEANS决定了在没有找到类型的存取器时发生了什么(并且没有注释表明它是被序列化的)。如果启用(默认),
// 将抛出一个异常来指明这些是非序列化类型;如果禁用了,它们将被序列化为空对象,即没有任何属性。
//请注意,这个特性只对那些没有任何识别注释的“空”bean产生影响(如@json序列化):那些有注释的bean不会导致抛出异常。
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false);
//在序列化时,只有那些值为null或被认为为空的值的属性才不会被包含在内。
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
//objectMapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
//将类名称序列化到json串中,去掉会导致得出来的的是LinkedHashMap对象,直接转换实体对象会失败
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
//是否允许单引号
objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addSerializer(LocalDate.class,new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addSerializer(LocalTime.class,new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDate.class,new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addDeserializer(LocalTime.class,new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
objectMapper.registerModule(javaTimeModule);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setValueSerializer(jackson2JsonRedisSerializer);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 缓存30分钟
Duration duration = Duration.ofSeconds(30*60*60);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(duration);
return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}
@Override
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... objects) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
sb.append("&");
for (Object obj : objects) {
sb.append(obj.getClass().getName());
sb.append("&");
try {
sb.append(MapperUtils.obj2json(obj));
} catch (Exception e) {
e.printStackTrace();
}
sb.append("&");
}
return DigestUtils.sha256Hex(sb.toString());
}
};
}
@Bean("myKeyGenerator")
@Primary
public KeyGenerator myKeyGenerator(){
return new KeyGenerator(){
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName()+"["+ Arrays.asList(params).toString()+"]";
}
};
}
}
备注:objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
这句是将类名称序列化到json串中,去掉会导致得出来的的是LinkedHashMap对象,直接转换实体对象会失败,同时FINAL 类型的熟悉不序列化和反序列化、将最外层json 串包装成数组类型,在redis 中存的字符串最外层是 [] ,如果配置成JsonTypeInfo.As.WRAPPER_OBJECT 则最外层是{}。
LoginUser 类:
package com.dechnic.oms.common.core.domain.model;
import com.dechnic.oms.common.constant.UserStatus;
import com.dechnic.oms.common.core.vo.SysUserVO;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.bytebuddy.asm.Advice;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Set;
/**
* @author zhangzs
* @date 2023/1/3 17:48
* @Description
*/
@Data
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LoginUser implements UserDetails{
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 部门ID
*/
private Long deptId;
/**
* 用户唯一标识
*/
private String token;
/**
* 登录时间
*/
private LocalDateTime loginTime;
/**
* 过期时间
*/
private LocalDateTime expireTime;
/**
* 登录IP地址
*/
private String ipaddr;
/**
* 登录地点
*/
private String loginLocation;
/**
* 权限列表
*/
private Set<String> permissions;
private SysUserVO sysUserVO;
public LoginUser(SysUserVO user, Set<String> permissions){
this.userId = user.getUserId();
this.deptId = user.getDeptId();
this.sysUserVO = user;
this.permissions = permissions;
}
@JsonIgnore
@Override
public String getPassword()
{
return sysUserVO.getPassword();
}
@Override
public String getUsername()
{
return sysUserVO.getUserName();
}
/**
* 账户是否未过期,过期无法验证
*/
@JsonIgnore
@Override
public boolean isAccountNonExpired()
{
return LocalDate.now().isBefore(sysUserVO.getExpirationDate());
}
/**
* 指定用户是否解锁,锁定的用户无法进行身份验证
*
* @return
*/
@JsonIgnore
@Override
public boolean isAccountNonLocked()
{
return sysUserVO.getStatus().equals(UserStatus.OK.getCode());
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*
* @return
*/
@JsonIgnore
@Override
public boolean isCredentialsNonExpired()
{
return true;
}
/**
* 是否可用 ,禁用的用户不能身份验证
*
* @return
*/
@JsonIgnore
@Override
public boolean isEnabled()
{
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
{
return null;
}
}
loginUser 里面封装了一个负责对象SysUserVO
package com.dechnic.oms.common.core.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import static com.fasterxml.jackson.annotation.JsonTypeInfo.As.WRAPPER_ARRAY;
/**
* @ desc:
* @ Author: houqida
* @ Date : 2023/1/7
*/
@ApiModel("用户查询VO")
@Data
public class SysUserVO extends BaseVO {
private static final long serialVersionUID = -8177563824574495L;
/**
* 用户ID
*/
@ApiModelProperty("用户ID")
private Long userId;
/**
* 所在单位ID
*/
@ApiModelProperty("所属单位ID")
private Long deptId;
@ApiModelProperty("所属单位")
private String deptName;
/**
* 用户账号
*/
@ApiModelProperty("用户账号")
private String userName;
/**
* 用户昵称
*/
@ApiModelProperty("用户昵称")
private String nickName;
/**
* 用户类型
*/
@ApiModelProperty("用户类型")
private String userType;
/**
* 用户邮箱
*/
@ApiModelProperty("邮箱")
private String email;
/**
* 手机号码
*/
@ApiModelProperty("手机号")
private String phonenumber;
/**
* 帐号状态(0正常 1停用)
*/
@ApiModelProperty("状态(0正常 1停用)")
private String status;
/**
* 密码
*/
@JsonIgnore
private String password;
/**
* 密码过期时间
*/
@JsonIgnore
private LocalDateTime passwordExpire;
/**
* 过期时间
*/
@ApiModelProperty("有效期限")
private LocalDate expirationDate;
@ApiModelProperty("用户角色")
private String roleName;
@ApiModelProperty("用户已分配角色id列表")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<Long> roleIdList;
@ApiModelProperty("用户已分配角色列表")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<SysRoleVO> roleList;
}
后来通过在SysUserVO 上添加
@JsonTypeName("SysUserVO")
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
这 两句的作用是将 SysUserVO 已指定别名的方式配置,而不是以包名加类名的方式,同时是WRAPPER_OBJECT 对象方式。
这样在redis 中存储的时候就变成以下这样,同时反序列化也不会提示以上的异常
登陆成功后,redis 里面也存储了LoginUser 的信息,但是当你重启服务后,再次执行别的接口报如下错误
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `java.lang.Long` from Array value (token `JsonToken.START_ARRAY`)
at [Source: (byte[])"["com.dechnic.oms.common.core.domain.model.LoginUser",{"userId":1,"deptId":1,"token":"702ddf92-c2ae-4265-9d27-330096268df4","loginTime":"2023-02-04 16:15:54","expireTime":"2023-02-04 16:45:54","ipaddr":"127.0.0.1","permissions":["java.util.HashSet",["*:*:*"]],"sysUserVO":{"SysUserVO":{"userId":1,"deptId":1,"deptName":"qqqqq","userName":"admin","nickName":"超级管理员","userType":"01","status":"0","expirationDate":"2099-12-31","roleName":"管理员","roleIdList":["java.util.ArrayList",[["java"[truncated 39 bytes]; line: 1, column: 495] (through reference chain: com.dechnic.oms.common.core.domain.model.LoginUser["sysUserVO"]->com.dechnic.oms.common.core.vo.SysUserVO["roleIdList"]->java.util.ArrayList[0])
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)
at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1741)
at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1515)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._deserializeFromArray(StdDeserializer.java:222)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseLong(StdDeserializer.java:952)
at com.fasterxml.jackson.databind.deser.std.NumberDeserializers$LongDeserializer.deserialize(NumberDeserializers.java:575)
at com.fasterxml.jackson.databind.deser.std.NumberDeserializers$LongDeserializer.deserialize(NumberDeserializers.java:550)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer._deserializeFromArray(CollectionDeserializer.java:355)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:244)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:28)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:120)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromArray(AsArrayTypeDeserializer.java:53)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserializeWithType(CollectionDeserializer.java:283)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:138)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:314)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:177)
at com.fasterxml.jackson.databind.jsontype.impl.AsWrapperTypeDeserializer._deserialize(AsWrapperTypeDeserializer.java:121)
at com.fasterxml.jackson.databind.jsontype.impl.AsWrapperTypeDeserializer.deserializeTypedFromObject(AsWrapperTypeDeserializer.java:52)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1292)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:138)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:314)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:177)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:120)
at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromAny(AsArrayTypeDeserializer.java:71)
at com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer$Vanilla.deserializeWithType(UntypedObjectDeserializer.java:781)
at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:74)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4674)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3731)
at org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer.deserialize(Jackson2JsonRedisSerializer.java:73)
... 66 common frames omitted
后来分析到是当执行token 解析的时候
/**
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param request
* @return 令牌
*/
public LoginUser verifyToken(HttpServletRequest request) {
String token = getToken(request);
if (StringUtils.isNotEmpty(token)) {
try {
Algorithm algorithm = Algorithm.HMAC512(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(isUser)
.build();
DecodedJWT jwt = verifier.verify(token);
String uuid = jwt.getClaim(Constants.LOGIN_USER_KEY).asString();
String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey);
if (null != user && user.getExpireTime().minusMinutes(MINUTE_TEN).isBefore(LocalDateTime.now())) {
refreshToken(user);
}
return user;
} catch (JWTVerificationException exception) {
log.error("验证Token失败:", exception);
}
}
return null;
}
执行 LoginUser user = redisCache.getCacheObject(userKey);
这句从redis 中获取登陆用户信息时报错。根据以上异常定位到是解析List roleIdList 这句时报错。定位到可能时解析List 集合的问题。
SysUserVO正确配置如下:
@ApiModelProperty("用户已分配角色id列表")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = WRAPPER_ARRAY)
private List<Long> roleIdList;
@ApiModelProperty("用户已分配角色列表")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = WRAPPER_ARRAY)
private List<SysRoleVO> roleList;
备注: 在List 集合熟悉上添加 @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = WRAPPER_ARRAY)
此句的作用是此属性是包装成数组类型。
3.正确配置
RedisConfig 配置
package com.dechnic.oms.framework.config;
import com.dechnic.oms.common.utils.MapperUtils;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
/**
* redis配置
* @author zhangzs
* @date 2023/1/4 9:54
* @Description
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
//FAIL_ON_UNKNOWN_PROPERTIES在序列化的时候,如果遇到不认识的字段的处理方式
//默认启用特性,这意味着在遇到未知属性时抛出JsonMappingException。在引入该特性之前,这是默认的默认设置。
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
//FAIL_ON_EMPTY_BEANS决定了在没有找到类型的存取器时发生了什么(并且没有注释表明它是被序列化的)。如果启用(默认),
// 将抛出一个异常来指明这些是非序列化类型;如果禁用了,它们将被序列化为空对象,即没有任何属性。
//请注意,这个特性只对那些没有任何识别注释的“空”bean产生影响(如@json序列化):那些有注释的bean不会导致抛出异常。
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false);
//在序列化时,只有那些值为null或被认为为空的值的属性才不会被包含在内。
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
//objectMapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
//将类名称序列化到json串中,去掉会导致得出来的的是LinkedHashMap对象,直接转换实体对象会失败
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
//是否允许单引号
objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addSerializer(LocalDate.class,new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addSerializer(LocalTime.class,new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDate.class,new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addDeserializer(LocalTime.class,new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
objectMapper.registerModule(javaTimeModule);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setValueSerializer(jackson2JsonRedisSerializer);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 缓存30分钟
Duration duration = Duration.ofSeconds(30*60*60);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(duration);
return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}
@Override
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... objects) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
sb.append("&");
for (Object obj : objects) {
sb.append(obj.getClass().getName());
sb.append("&");
try {
sb.append(MapperUtils.obj2json(obj));
} catch (Exception e) {
e.printStackTrace();
}
sb.append("&");
}
return DigestUtils.sha256Hex(sb.toString());
}
};
}
@Bean("myKeyGenerator")
@Primary
public KeyGenerator myKeyGenerator(){
return new KeyGenerator(){
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName()+"["+ Arrays.asList(params).toString()+"]";
}
};
}
}
SysUserVO:
package com.dechnic.oms.common.core.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import static com.fasterxml.jackson.annotation.JsonTypeInfo.As.WRAPPER_ARRAY;
/**
* @ desc:
* @ Author: houqida
* @ Date : 2023/1/7
*/
@ApiModel("用户查询VO")
@Data
@JsonTypeName("SysUserVO")
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
public class SysUserVO extends BaseVO {
private static final long serialVersionUID = -8177563824574495L;
/**
* 用户ID
*/
@ApiModelProperty("用户ID")
private Long userId;
/**
* 所在单位ID
*/
@ApiModelProperty("所属单位ID")
private Long deptId;
@ApiModelProperty("所属单位")
private String deptName;
/**
* 用户账号
*/
@ApiModelProperty("用户账号")
private String userName;
/**
* 用户昵称
*/
@ApiModelProperty("用户昵称")
private String nickName;
/**
* 用户类型
*/
@ApiModelProperty("用户类型")
private String userType;
/**
* 用户邮箱
*/
@ApiModelProperty("邮箱")
private String email;
/**
* 手机号码
*/
@ApiModelProperty("手机号")
private String phonenumber;
/**
* 帐号状态(0正常 1停用)
*/
@ApiModelProperty("状态(0正常 1停用)")
private String status;
/**
* 密码
*/
@JsonIgnore
private String password;
/**
* 密码过期时间
*/
@JsonIgnore
private LocalDateTime passwordExpire;
/**
* 过期时间
*/
@ApiModelProperty("有效期限")
private LocalDate expirationDate;
@ApiModelProperty("用户角色")
private String roleName;
@ApiModelProperty("用户已分配角色id列表")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = WRAPPER_ARRAY)
private List<Long> roleIdList;
@ApiModelProperty("用户已分配角色列表")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = WRAPPER_ARRAY)
private List<SysRoleVO> roleList;
}
4.小结
- jackson 主要通过ObjectMapper 来设置一些序列化、反序列化的属性。
- redisConfig 里面通过
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
做通用配置
LoginUser 缓存到redis 中的最外层格式为[],但是 SysUserVO 为LoginUser 的一个复杂对象,所有需要通过
@JsonTypeName("SysUserVO")
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
此配置,将SysUserVO 配置成以昵称命名的方式,同时包装成对象类型,
但是SysUserVO 里面又有List roleIdList; 和 List roleList; 等list 类型
需要在这些属性上添加 @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = WRAPPER_ARRAY)
同理SysRoleVO 也要做相应的配置