dromara / hutool

🍬A set of tools that keep Java sweet.
https://hutool.cn
Other
29.09k stars 7.5k forks source link

<Hutool版本:5.8.16> Redis配置Jackson2JsonRedisSerializer序列化器,redis的list中存入Tree<Long>列表,报错class cn.hutool.core.lang.tree.Tree cannot be cast to class java.lang.String #3256

Closed MuShanYu closed 1 year ago

MuShanYu commented 1 year ago

版本情况

JDK版本: Java 11.0.18 hutool版本: 5.X.X(请确保最新尝试是否还有问题)

问题描述(包括截图)

Redis配置Jackson2JsonRedisSerializer序列化器,往redis的list中存入Tree列表,报错class cn.hutool.core.lang.tree.Tree cannot be cast to class java.lang.String 树结构构建成功,但是序列化出问题。

  1. 复现代码 RedisConfig配置:

    @Bean
    @SuppressWarnings("all")
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 自定义 String Object
        RedisTemplate<?, ?> template = new RedisTemplate();
        // 配置连接工厂
        template.setConnectionFactory(redisConnectionFactory);
    
        // Json 序列化配置
        Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        // ObjectMapper 转译
        ObjectMapper objectMapper = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会报异
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectJackson2JsonRedisSerializer.setObjectMapper(objectMapper);
    
        // String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
    
        // key 采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash 的key也采用 String 的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value 序列化方式采用 jackson
        template.setValueSerializer(objectJackson2JsonRedisSerializer);
        // hash 的 value 采用 jackson
        template.setHashValueSerializer(objectJackson2JsonRedisSerializer);
        template.afterPropertiesSet();
    
        return template;
    }

    RedisUtil放入list的方法:

    public void setCacheList(String key, List<T> dataList) {
        ListOperations<String, T> listOperations = redisTemplate.opsForList();
        listOperations.rightPushAll(key, dataList);
    }

    调用:

    // 构建树结构
    List<Tree<Long>> productClassTreeStruct = toTreeList(parentAndChildProductClassList, parentProductClass.getParentId());
    // 存入缓存中
    redisUtil.setCacheList(cacheKey, productClassTreeStruct);
  2. 堆栈信息
    java.lang.ClassCastException: class cn.hutool.core.lang.tree.Tree cannot be cast to class java.lang.String (cn.hutool.core.lang.tree.Tree is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap')
    at org.springframework.data.redis.serializer.StringRedisSerializer.serialize(StringRedisSerializer.java:36) ~[spring-data-redis-2.7.13.jar:2.7.13]
    at org.springframework.data.redis.core.AbstractOperations.rawValue(AbstractOperations.java:128) ~[spring-data-redis-2.7.13.jar:2.7.13]
    at org.springframework.data.redis.core.AbstractOperations.rawValues(AbstractOperations.java:155) ~[spring-data-redis-2.7.13.jar:2.7.13]
    at org.springframework.data.redis.core.DefaultListOperations.rightPushAll(DefaultListOperations.java:303) ~[spring-data-redis-2.7.13.jar:2.7.13]
    at com.example.product.common.utils.RedisUtil.setCacheList(RedisUtil.java:62) ~[main/:na]
    at com.example.product.center.service.ProductClassService.queryProductClassList(ProductClassService.java:114) ~[main/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
    at org.glassfish.jersey.server.model.internal.ResourceMethodInvocationHandlerFactory.lambda$static$0(ResourceMethodInvocationHandlerFactory.java:52) ~[jersey-server-2.35.jar:na]
    at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher$1.run(AbstractJavaResourceMethodDispatcher.java:124) ~[jersey-server-2.35.jar:na]
    at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.invoke(AbstractJavaResourceMethodDispatcher.java:167) ~[jersey-server-2.35.jar:na]
    at org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$TypeOutInvoker.doDispatch(JavaResourceMethodDispatcherProvider.java:219) ~[jersey-server-2.35.jar:na]
    at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.dispatch(AbstractJavaResourceMethodDispatcher.java:79) ~[jersey-server-2.35.jar:na]
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:475) ~[jersey-server-2.35.jar:na]
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:397) ~[jersey-server-2.35.jar:na]
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:81) ~[jersey-server-2.35.jar:na]
    at org.glassfish.jersey.server.ServerRuntime$1.run(ServerRuntime.java:255) ~[jersey-server-2.35.jar:na]
    at org.glassfish.jersey.internal.Errors$1.call(Errors.java:248) ~[jersey-common-2.35.jar:na]
    at org.glassfish.jersey.internal.Errors$1.call(Errors.java:244) ~[jersey-common-2.35.jar:na]
    at org.glassfish.jersey.internal.Errors.process(Errors.java:292) ~[jersey-common-2.35.jar:na]
    at org.glassfish.jersey.internal.Errors.process(Errors.java:274) ~[jersey-common-2.35.jar:na]
    at org.glassfish.jersey.internal.Errors.process(Errors.java:244) ~[jersey-common-2.35.jar:na]
    at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:265) ~[jersey-common-2.35.jar:na]
    at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:234) ~[jersey-server-2.35.jar:na]
    at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:684) ~[jersey-server-2.35.jar:na]
    at org.glassfish.jersey.servlet.WebComponent.serviceImpl(WebComponent.java:394) ~[jersey-container-servlet-core-2.35.jar:na]
    at org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:346) ~[jersey-container-servlet-core-2.35.jar:na]
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:366) ~[jersey-container-servlet-core-2.35.jar:na]
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:319) ~[jersey-container-servlet-core-2.35.jar:na]
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:205) ~[jersey-container-servlet-core-2.35.jar:na]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:209) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-9.0.76.jar:9.0.76]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.28.jar:5.3.28]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.28.jar:5.3.28]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.28.jar:5.3.28]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.28.jar:5.3.28]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.28.jar:5.3.28]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.28.jar:5.3.28]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:481) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:130) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:926) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1791) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.76.jar:9.0.76]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
MuShanYu commented 1 year ago

我的redis配置应该是没有问题的。

Createsequence commented 1 year ago

看起来是使用 jackson 序列化 hutool 的 Tree 的时候报错了,检查一下 Jackson2JsonRedisSerializer 是否支持序列化 hutool 的 Tree 类型的对象,如果不支持,尝试看看为 jackson 添加用于处理 Tree 类型数据的 jackson 序列化器。

MuShanYu commented 1 year ago

我描述一下我遇到此问题的场景,我需要缓存产品分类,产品分类是一个树型结构,所有我想直接在redis中缓存已经生成好的Tree列表。在我尝试自定义还是报出同样的错误,objectMapper.registerModule(new TreeCustomModule());我将Tree通过fastJson转为字符串。我觉得这不是Jackson2JsonRedisSerializer的问题,它是spirng已经提供的序列化方式实现。可能也许会有和我遇到同样场景的人,希望大佬们能优化一下这个。目前我决定缓存对应的实体列表,最后转为Tree返回。

MuShanYu commented 1 year ago

看起来是使用 jackson 序列化 hutool 的 Tree 的时候报错了,检查一下 Jackson2JsonRedisSerializer 是否支持序列化 hutool 的 Tree 类型的对象,如果不支持,尝试看看为 jackson 添加用于处理 Tree 类型数据的 jackson 序列化器。

作者大佬能调试一下这个问题吗,为了使用这个还要对这个类进行特殊处理。

Createsequence commented 1 year ago

具体的可能还要我试验一下,不过看你的解决办法问题应该还是容器中的 ObjectMapper 不支持序列化 Tree,能把 TreeCustomModule 具体的内容发出来看一下吗?

如果确实是 Spring 对 ObjectMapper 配置的问题,那么我们可能也没有很好的解决方案。因为 hutool 本身只是一个工具类库,我们并不能确认用户是否有在 spring 应用中需要将 Tree 序列化为 json 的需求,因此站在这个角度,我们并不适合去提供一个 starter 或者基于 spring 的特定序列化器好让容器中的 ObjectMapper 支持序列化 Tree

不过,在后续,我们会在文档中强调在这种特殊场景,并且按照你的 issue 给出解决方案,如果用户有这方面的需要,可以直接按照文档解决这个问题。

2023-08-11 补充: 我试了一下,用默认的配置启动(spring-boot-web-starter),注入的 ObjectMapper 是可以序列化 Tree 的:

    @Component
    public static class Bean {

        @Autowired
        private ObjectMapper objectMapper;

        @SneakyThrows
        @PostConstruct
        public void doSomething() {
            List<TreeNode<Integer>> nodeList = CollUtil.newArrayList();
            nodeList.add(new TreeNode<>(1, 1, "杭州市", 0));
            nodeList.add(new TreeNode<>(111, 11, "余杭区", 0));
            nodeList.add(new TreeNode<>(112, 11, "西湖区", 0));
            nodeList.add(new TreeNode<>(113, 11, "拱墅区", 0));
            nodeList.add(new TreeNode<>(2, 0, "山东省", 0));
            nodeList.add(new TreeNode<>(22, 2, "泰安市", 0));
            nodeList.add(new TreeNode<>(222, 22, "泰山区", 0));
            Tree<Integer> tree = TreeUtil.buildSingle(nodeList);
            System.out.println("Tree: " + objectMapper.writeValueAsString(tree));
            // = {"id":0,"children":[{"id":2,"parentId":0,"weight":0,"name":"山东省","children":[{"id":22,"parentId":2,"weight":0,"name":"泰安市","children":[{"id":222,"parentId":22,"weight":0,"name":"泰山区"}]}]}]}
        }
    }
looly commented 1 year ago

这应该是自定义序列化问题。

考虑调用Tree转为JSON然后序列化……

MuShanYu commented 1 year ago

具体的可能还要我试验一下,不过看你的解决办法问题应该还是容器中的 ObjectMapper 不支持序列化 Tree,能把 TreeCustomModule 具体的内容发出来看一下吗?

如果确实是 Spring 对 ObjectMapper 配置的问题,那么我们可能也没有很好的解决方案。因为 hutool 本身只是一个工具类库,我们并不能确认用户是否有在 spring 应用中需要将 Tree 序列化为 json 的需求,因此站在这个角度,我们并不适合去提供一个 starter 或者基于 spring 的特定序列化器好让容器中的 ObjectMapper 支持序列化 Tree

不过,在后续,我们会在文档中强调在这种特殊场景,并且按照你的 issue 给出解决方案,如果用户有这方面的需要,可以直接按照文档解决这个问题。

自定义的serializer

public class TreeDataSerializer extends JsonSerializer<Tree> {
    @Override
    public void serialize(Tree value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeString(JSON.toJSONString(value));
    }
}

自定义的module

@Component
public class TreeCustomModule extends SimpleModule {
    public TreeCustomModule() {
        addSerializer(Tree.class, new TreeDataSerializer());
    }
}

在objectMapper中进行注册

objectMapper.registerModule(new TreeCustomModule());

问题出现在@Autowired注解上面:

    @Autowired
    public RedisTemplate<String, T> redisTemplate;

我在debug时进行跟踪发现最终使用的是stringRedisTemplate,而非已经进行配置的redisTemplate,导致该问题的是spring的依赖注入和泛型擦除问题。 Autowired默认是byType进行注入,java在运行时会进行泛型擦除,在RedisAutoConfiguration中有StringTemplate和RedisTemplate<Object, Object>两个类,在编译后 T 的具体类型信息在运行时会被擦除,所以 Spring 在处理这个字段时只能看到 RedisTemplate<String, ?> 这种类型。spring再对这两个类的选择上会根据具体的类型和泛型擦除后的信息选择一个具体的实现,由于T类型是未知的,所有spring最终会选择更具体的StringTemplate,导致最后注入的是StringTemplate。 StringRedisTemplate 被spring创建时,它的构造方法初始化了所有的Serializer,最终所使用的序列化均为StringRedisSerializer。 最终才发生了xxx类 cast string的异常。

    public StringRedisTemplate() {
        setKeySerializer(RedisSerializer.string());
        setValueSerializer(RedisSerializer.string());
        setHashKeySerializer(RedisSerializer.string());
        setHashValueSerializer(RedisSerializer.string());
    }

redis的自动配置:

@AutoConfiguration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }

}

ConditionalOnMissingBean是指容器中没有redisTemplate名称的bean是将该类进行创建。 正确的注入方式: 1.去除泛型,spring在根据byType注入时,不会因为泛型擦除问题进行选择,会注入RedisTemplate。 2.不使用模糊泛型,指定具体泛型类型RedisTemplate<Object, Object>,进行正确的类型注入。

MuShanYu commented 1 year ago

这不是Hutool框架的问题,给各位开发者带来了不必要的麻烦,非常抱歉!

MuShanYu commented 1 year ago

这应该是自定义序列化问题。

考虑调用Tree转为JSON然后序列化……

直接存string是没问题的,但是我已经配置了Jackson,没有必要在进行重复的json序列化了。

looly commented 1 year ago

@MuShanYu 感谢详细的答疑解惑~~

不过我们讨论问题的核心不在于自定义序列化问题,而在于Tree是否有必要自行实现序列化,而不用用户写那么一大堆~~

MuShanYu commented 1 year ago

具体的可能还要我试验一下,不过看你的解决办法问题应该还是容器中的 ObjectMapper 不支持序列化 Tree,能把 TreeCustomModule 具体的内容发出来看一下吗?

如果确实是 Spring 对 ObjectMapper 配置的问题,那么我们可能也没有很好的解决方案。因为 hutool 本身只是一个工具类库,我们并不能确认用户是否有在 spring 应用中需要将 Tree 序列化为 json 的需求,因此站在这个角度,我们并不适合去提供一个 starter 或者基于 spring 的特定序列化器好让容器中的 ObjectMapper 支持序列化 Tree

不过,在后续,我们会在文档中强调在这种特殊场景,并且按照你的 issue 给出解决方案,如果用户有这方面的需要,可以直接按照文档解决这个问题。

2023-08-11 补充: 我试了一下,用默认的配置启动(spring-boot-web-starter),注入的 ObjectMapper 是可以序列化 Tree 的:

    @Component
    public static class Bean {

        @Autowired
        private ObjectMapper objectMapper;

        @SneakyThrows
        @PostConstruct
        public void doSomething() {
            List<TreeNode<Integer>> nodeList = CollUtil.newArrayList();
            nodeList.add(new TreeNode<>(1, 1, "杭州市", 0));
            nodeList.add(new TreeNode<>(111, 11, "余杭区", 0));
            nodeList.add(new TreeNode<>(112, 11, "西湖区", 0));
            nodeList.add(new TreeNode<>(113, 11, "拱墅区", 0));
            nodeList.add(new TreeNode<>(2, 0, "山东省", 0));
            nodeList.add(new TreeNode<>(22, 2, "泰安市", 0));
            nodeList.add(new TreeNode<>(222, 22, "泰山区", 0));
            Tree<Integer> tree = TreeUtil.buildSingle(nodeList);
            System.out.println("Tree: " + objectMapper.writeValueAsString(tree));
            // = {"id":0,"children":[{"id":2,"parentId":0,"weight":0,"name":"山东省","children":[{"id":22,"parentId":2,"weight":0,"name":"泰安市","children":[{"id":222,"parentId":22,"weight":0,"name":"泰山区"}]}]}]}
        }
    }

非常感谢大佬的调试。应该是我使用的问题。非常感谢回复与帮忙。

MuShanYu commented 1 year ago

@MuShanYu 感谢详细的答疑解惑~~

不过我们讨论问题的核心不在于自定义序列化问题,而在于Tree是否有必要自行实现序列化,而不用用户写那么一大堆~~

了解了,非常感谢各位的回复与解答。