简介
说明
本文介绍解决Spring-Data-Redis的“java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to xxx”报错的方法。
出现的场景
SpringBoot项目中使用Redis来进行缓存。把数据放到缓存中时没有问题,但从缓存中取出来反序列化为对象时报错:“java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to xxx”。(xxx为反序列化的目标对象对应的类。)
只有这个类里有其他对象字段才会报这个问题,如果这个类里都是初始的类型(比如:Integer,String)则不会报这个错误。
只要用到Redis序列化反序列化的地方都会遇到这个问题,比如:RedisTemplate,Redisson,@Cacheable注解等。
问题复现
业务代码
Controller
package com.example.demo.controller; import com.example.demo.entity.Result; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("user") public class UserController { @Autowired private UserService userService; @GetMapping("page") public Result page(int pageNo, int pageSize) { return userService.page(pageNo, pageSize); } }
Service
接口
package com.example.demo.service; import com.example.demo.entity.Result; public interface UserService { Result page(int pageNo, int pageSize); }
实现
package com.example.demo.service.impl; import com.example.demo.constant.RedisConstant; import com.example.demo.entity.Result; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @Service public class UserServiceImpl implements UserService { private final List<User> allUsers = Arrays.asList( new User(1L, "Tony1", 20), new User(2L, "Tony2", 18), new User(3L, "Tony3", 30), new User(4L, "Tony4", 25), new User(5L, "Tony5", 28) ); @Override @Cacheable(cacheNames = "userPageCache") public Result<List<User>> page(int pageNo, int pageSize) { String format = String.format("pageNo: %s, pageSize: %s", pageNo, pageSize); System.out.println("从数据库中读数据。" + format); int from = (pageNo - 1) * pageSize; int to = Math.min(allUsers.size(), (pageNo) * pageSize); List<User> users = new ArrayList<>(allUsers.subList(from, to)); return new Result<List<User>>().data(users); } }
Entity
User
package com.example.demo.entity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor // 必须要有无参构造函数。因为Redis反序列化为对象时要用到 @NoArgsConstructor public class User { private Long id; private String userName; private Integer age; }
Result
package com.example.demo.entity; import lombok.Data; @Data public class Result<T> { private boolean success = true; private int code = 1000; private String message; private T data; public Result() { } public Result(boolean success) { this.success = success; } public Result<T> success(boolean success) { Result<T> result = new Result<>(success); if (success) { result.code = 1000; } else { result.code = 1001; } return result; } public Result<T> success() { return success(true); } public Result<T> failure() { return success(false); } /** * @param code {@link ResultCode#getCode()} */ public Result<T> code(int code) { this.code = code; return this; } public Result<T> message(String message) { this.message = message; return this; } public Result<T> data(T data) { this.data = data; return this; } }
Redis配置代码
package com.example.demo.config; import com.example.demo.constant.RedisConstant; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; 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.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.*; import org.springframework.util.StringUtils; import java.time.Duration; import java.util.HashMap; import java.util.Map; @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { /** * 重写缓存Key生成策略。 * 包名+方法名+参数列表。防止缓存Key冲突 */ @Bean @Override public KeyGenerator keyGenerator() { return (target, method, params) -> { // 存放最终结果 StringBuilder resultStringBuilder = new StringBuilder("cache:key:"); // 执行方法所在的类 resultStringBuilder.append(target.getClass().getName()).append("."); // 执行的方法名称 resultStringBuilder.append(method.getName()).append("("); // 存放参数 StringBuilder paramStringBuilder = new StringBuilder(); for (Object param : params) { if (param == null) { paramStringBuilder.append("java.lang.Object[null],"); } else { paramStringBuilder .append(param.getClass().getName()) .append("[") .append(String.valueOf(param)) .append("],"); } } if (StringUtils.hasText(paramStringBuilder.toString())) { // 去掉最后的逗号 String trimLastComma = paramStringBuilder.substring(0, paramStringBuilder.length() - 1); resultStringBuilder.append(trimLastComma); } return resultStringBuilder.append(")").toString(); }; } @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { Map<String, RedisCacheConfiguration> configurationMap = new HashMap<>(); RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(5)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer())) .disableCachingNullValues(); return RedisCacheManager.builder(factory) .initialCacheNames(configurationMap.keySet()) .withInitialCacheConfigurations(configurationMap) .cacheDefaults(config) .build(); } @Bean public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(keySerializer()); template.setValueSerializer(valueSerializer()); template.setHashKeySerializer(keySerializer()); template.setHashValueSerializer(valueSerializer()); template.afterPropertiesSet(); return template; } private RedisSerializer<String> keySerializer() { return new StringRedisSerializer(); } private RedisSerializer<Object> valueSerializer() { Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); return jackson2JsonRedisSerializer; } }
测试
第1次访问(成功)
http://localhost:8080/user/page?pageNo=1&pageSize=2
结果:成功访问,结果或存入Redis
第2次访问(失败)
http://localhost:8080/user/page?pageNo=1&pageSize=2
结果:报错
后端输出:
2022-01-11 14:59:23.805 ERROR 68468 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.example.demo.entity.Result] with root cause java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.example.demo.entity.Result at com.sun.proxy.$Proxy56.page(Unknown Source) ~[na:na] at com.example.demo.controller.UserController.page(UserController.java:18) ~[classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_201] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_201] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_201] at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_201] at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE] at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.15.RELEASE.jar:5.2.15.RELEASE] ...... at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) [tomcat-embed-core-9.0.46.jar:9.0.46] at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.46.jar:9.0.46] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_201] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_201] at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.46.jar:9.0.46] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_201]
原因分析
SpringBoot 的缓存使用 jackson 来做数据的序列化与反序列化,如果默认使用 Object 作为序列化与反序列化的类型,则其只能识别 java 基本类型,遇到复杂类型时,jackson 就会先序列化成 LinkedHashMap ,然后再尝试强转为所需类别,这样大部分情况下会强转失败。
解决方案
修改RedisTemplate这个bean的valueSerializer,设置默认类型。
关键代码:
private RedisSerializer<Object> valueSerializer() { Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 此项必须配置,否则如果序列化的对象里边还有对象,会报如下错误: // java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX objectMapper.activateDefaultTyping( objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); // 旧版写法: // objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); return jackson2JsonRedisSerializer; }
解决后的测试
代码
只修改Redis配置类
package com.example.demo.config; import com.example.demo.constant.RedisConstant; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; 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.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.*; import org.springframework.util.StringUtils; import java.time.Duration; import java.util.HashMap; import java.util.Map; @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { /** * 重写缓存Key生成策略。 * 包名+方法名+参数列表。防止缓存Key冲突 */ @Bean @Override public KeyGenerator keyGenerator() { return (target, method, params) -> { // 存放最终结果 StringBuilder resultStringBuilder = new StringBuilder("cache:key:"); // 执行方法所在的类 resultStringBuilder.append(target.getClass().getName()).append("."); // 执行的方法名称 resultStringBuilder.append(method.getName()).append("("); // 存放参数 StringBuilder paramStringBuilder = new StringBuilder(); for (Object param : params) { if (param == null) { paramStringBuilder.append("java.lang.Object[null],"); } else { paramStringBuilder .append(param.getClass().getName()) .append("[") .append(String.valueOf(param)) .append("],"); } } if (StringUtils.hasText(paramStringBuilder.toString())) { // 去掉最后的逗号 String trimLastComma = paramStringBuilder.substring(0, paramStringBuilder.length() - 1); resultStringBuilder.append(trimLastComma); } return resultStringBuilder.append(")").toString(); }; } @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { Map<String, RedisCacheConfiguration> configurationMap = new HashMap<>(); RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(5)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer())) .disableCachingNullValues(); return RedisCacheManager.builder(factory) .initialCacheNames(configurationMap.keySet()) .withInitialCacheConfigurations(configurationMap) .cacheDefaults(config) .build(); } @Bean public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(keySerializer()); template.setValueSerializer(valueSerializer()); template.setHashKeySerializer(keySerializer()); template.setHashValueSerializer(valueSerializer()); template.afterPropertiesSet(); return template; } private RedisSerializer<String> keySerializer() { return new StringRedisSerializer(); } private RedisSerializer<Object> valueSerializer() { Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 此项必须配置,否则如果序列化的对象里边还有对象,会报如下错误: // java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX objectMapper.activateDefaultTyping( objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); // 旧版写法: // objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); return jackson2JsonRedisSerializer; } }
测试
第1次访问(成功)
http://localhost:8080/user/page?pageNo=1&pageSize=2
可以看到,在写入Redis时,会带上类全名。这样在反序列化时就能成功了。
第2次访问(成功)
http://localhost:8080/user/page?pageNo=1&pageSize=2
请先
!