spring-boot-devtools和redis同时存在引起的类强转失败问题

我的项目中用了redis缓存用户数据,同时我想使用spring-boot-devtools进行热部署开发,但是报错如下:

java.lang.ClassCastException: com.pd.modules.security.service.dto.OnlineUserDto cannot be cast to com.pd.modules.security.service.dto.OnlineUserDto
	at com.pd.modules.security.service.OnlineUserService.getOne(OnlineUserService.java:150)
	at com.pd.modules.security.service.OnlineUserService$$FastClassBySpringCGLIB$$95c3de24.invoke()
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:684)
	at com.pd.modules.security.service.OnlineUserService$$EnhancerBySpringCGLIB$$b2c1c0e2.getOne()
	at com.pd.modules.security.security.TokenFilter.doFilter(TokenFilter.java:74)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215)
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178)
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:357)
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:270)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:712)
	at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:461)
	at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:384)
	at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:312)
	at org.apache.catalina.core.StandardHostValve.custom(StandardHostValve.java:394)
	at org.apache.catalina.core.StandardHostValve.status(StandardHostValve.java:253)
	at org.apache.catalina.core.StandardHostValve.throwable(StandardHostValve.java:348)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:173)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:770)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:748)

参考:
https://stackoverflow.com/questions/37977166/java-lang-classcastexception-dtoobject-cannot-be-cast-to-dtoobject

https://github.com/spring-projects/spring-boot/issues/11822

https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-devtools-known-restart-limitations

https://github.com/spring-projects/spring-boot/issues/9444

https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#using-boot-devtools

修改代码打印了强转类的类加载器,发现id不同,是两个不同的类加载器。网上百度了一堆中文的解决方案,大部分人都放弃了spring-boot-devtools,有一个人用反射来解决问题,但是也很丑很麻烦。

于是去外网搜索,springboot团队果然也早就碰到这个问题。他们给出的建议如下:
As explained in the documentation that I linked to above, DevTools uses two separate ClassLoaders: the app class loader and a restart class loader. The classes in your application (rediscache module) are loaded by the restart class loader so that they can be quickly reloaded as you make changes.

In your RedisCache class, you’re creating a JdkSerializationRedisSerializer without specifying a ClassLoader. As a result, it uses the app class loader. This means that you end up with a User loaded by the app class loader being cast to the User type loaded by the restart class loader.

To fix the problem you need to specify a class loader when you create the serializer. For example:

RedisSerializer serializer = new JdkSerializationRedisSerializer(getClass().getClassLoader());

这段话的意思是说redis使用的序列化工具没有指定类加载器,于是appClassloader的user类被强转为restartClassloader的user类,导致报错(但是我打印出的是两个不同的restartClassloader的User类,这一点不同,导致它的解决方案我也行不通)。

分析:
spring-boot-devtools原理是使用appClassloader的子类restartClassloader重新加载少部分类,实现快速重启的效果。理想的办法是,把需要缓存的实体类都让appClassloader加载,就没有这个问题了。于是仔细查看springboot的文档,寻找这样的属性。先是发现文档建议开发时关闭缓存功能,这样这个问题也就不存在了,这也是一个解决办法,但是我用的框架已经和redis绑定了,没法一个开关解决问题。

然后我用了spring.devtools.restart.exclude配置来排除被强转的类还是不行,发现这个配置也比较坑爹,我想的是,这应该是说排出这个类不被restartClassloader加载的意思,但是这个配置的真实作用仅仅是:
这个类被修改后不触发重启,但是它还是被restartClassloader加载了。

总结一下,就是一定要改的话,那我所有的需要缓存的实体类就要放到另一个子模块里面,然后用排除jar的配置来排除这个jar不被restartClassloader加载。新增类路径下的配置文件spring-devtools.properties,里面新增配置:

restart.exclude.companycommonlibs=/mycorp-common-[\\w-]+\.jar
restart.include.projectcommon=/mycorp-myproj-[\\w-]+\.jar

并且还没完,还要指定用appClassloader来序列化和反序列化redis对象,这样从redis中取出对象的类加载器和强转所用的类加载器都一致了都是appappClassloader:

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        ClassLoader classLoader = ClassUtils.class.getClassLoader();
        JdkSerializationRedisSerializer redisSerializer = new JdkSerializationRedisSerializer(classLoader);
        RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
        configuration = configuration.serializeValuesWith(RedisSerializationContext.
                SerializationPair.fromSerializer(redisSerializer)).entryTtl(Duration.ofHours(6));
        return configuration;
    }

    @SuppressWarnings("all")
    @Bean(name = "redisTemplate")
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate template = new RedisTemplate<>();
        //序列化
        ClassLoader classLoader = ClassUtils.class.getClassLoader();
        JdkSerializationRedisSerializer redisSerializer = new JdkSerializationRedisSerializer(classLoader);
        // value值的序列化采用fastJsonRedisSerializer
        template.setValueSerializer(redisSerializer);
        template.setHashValueSerializer(redisSerializer);
        // 全局开启AutoType,这里方便开发,使用全局的方式
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        // 建议使用这种方式,小范围指定白名单
        // ParserConfig.getGlobalInstance().addAccept("com.pd.domain");
        // key的序列化采用StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

改完之后,这个地方终于不报错了,然后悲剧的是,httpmessageconverter也用了fastjson来做json和类的互转,然后又报错了。我已经疯了,最后放弃了用devtools,还是搞个盗版jrebel吧。这个问题表明,任何的框架设计缓存的时候最后搞个开关,开发的时候把缓存都关了,就可以用devtools了。

你可能感兴趣的:(java,spring,boot)