背景
前段时间向测试部门提交了一个接口测试的需要,测试在调试接口的过程中时不时的就呈现查问不到数据的状况,然而测试流程很显著都还没测到我提交的接口,测试自己也晓得,然而他也是纳闷了半天不晓得什么状况,没方法测试我的接口只能来向我求助,而后我放下手头工作大抵看了下发现只有是申请条件不变异样必先。报错信息也很清晰:java.lang.ClassCastException
。
问题定位
从下面的剖析,能够看出谬误并非必现,然而有着显著的法则:查问条件不变就能必现。这样看来很显著是命中缓存就会有问题。依据异样堆栈信息定位到报错的代码行,有两点重大发现:
- 获取数据的办法应用了spring-cache的注解:
@Cacheable
- 被强转的数据是从Redis缓存中获取
这么也看不出个所以然,只能本地跑起来看能不能复现debug看看吧,而后就发现在没命中缓存的时候被强转的类的类加载器是org.springframework.boot.devtools.restart.classloader
,而命中缓存后的类加载器就变成sun.misc.Launcher$AppClassLoader
。这么看来问题的锋芒指向了热部署插件springboot devtools, 那就先Bing一下,搜寻一下关键字:springboot devtools 类型转换异样
:
看来有不少人都遇到过了,轻易点了几个进去,一色的提供的解决方案都是将被转换的类所在的jar包,从springboot devtools热部署中排除掉,这显然不是解决问题正确思路呀,首先如果该类并不是在独立的jar内呢,难道为了这么个问题我要独自搞了jar吗?而后如果真的是这样是不是意味着springboot devtools是有debug的呢?多年的开发教训带给我的直觉是没有正确的应用spring-cache,带着纳闷的角度我筹备翻翻springboot-devtools
和spring-cache
的源码一探到底!
问题排查
之前也没有浏览过这两个工具的源码,在不知如何下手的状况下,只能猜想摸索着后退。那就从SpringApplication.run()
办法动手吧,至多之前看过springboot的源码,还算相熟。
来看看run办法:
//跟本次问题无关的代码都去除了public ConfigurableApplicationContext run(String... args) { SpringApplicationRunListeners listeners = getRunListeners(args); // 关键点就在这里, 看类名就能晓得该类是干什么的,监听Spring程序启动的 listeners.starting(bootstrapContext, this.mainApplicationClass); listeners.started(context, timeTakenToStartup); allRunners(context, applicationArguments); return context;}
沿着SpringApplicationRunListeners.starting一路向下找到org.springframework.boot.context.event.EventPublishingRunListener#starting
,
@Override public void starting(ConfigurableBootstrapContext bootstrapContext) { this.initialMulticaster .multicastEvent(new ApplicationStartingEvent(bootstrapContext, this.application, this.args)); }
一眼扫去就晓得这是在播送应用程序启动事件:ApplicationStartingEvent
,既然这里有播送那必定就有监听这个事件的,持续往下找通过SimpleApplicationEventMulticaster.multicastEvent->invokeListener->doInvokeListener
这一路调用下来来到listener.onApplicationEvent(event);
,这个相熟spring事件模型的应该比较清楚了吧。
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) { listener.onApplicationEvent(event);}
进onApplicationEvent看下,哎吆吓一跳,实现那么多:
这怎么找,方才不是有个RestartClassLoader吗,搜下:restart试试
成果很显著还真有,必定就是RestartApplicationListener
了,进去看看:
这外面一共监听了四种事件,还记得方才咱们播送的是什么事件吧?第一个就是
public void onApplicationEvent(ApplicationEvent event) { // 这个就是咱们明天的配角 if (event instanceof ApplicationStartingEvent) { onApplicationStartingEvent((ApplicationStartingEvent) event); } if (event instanceof ApplicationPreparedEvent) { onApplicationPreparedEvent((ApplicationPreparedEvent) event); } if (event instanceof ApplicationReadyEvent || event instanceof ApplicationFailedEvent) { Restarter.getInstance().finish(); } if (event instanceof ApplicationFailedEvent) { onApplicationFailedEvent((ApplicationFailedEvent) event); } }
在onApplicationStartingEvent()
办法中调用Restarter.initialize()
后咱们就进入到springboot-devtools偷天换日的外围地带了,先说下大抵流程:
- 启动一个新线程:restartMain,并创立一个RestartClassLoader绑定到线程上下文中
- 在新线程中从新调用springboot应用程序的main办法
- 抛弃Main线程
局部要害源码贴下:
private Throwable doStart() throws Exception { // 创立restartClassLoader ClassLoader classLoader = new RestartClassLoader(this.applicationClassLoader, urls, updatedFiles, this.logger); return relaunch(classLoader); } protected Throwable relaunch(ClassLoader classLoader) throws Exception { // 创立新线程:restartedMain RestartLauncher launcher = new RestartLauncher(classLoader,this.mainClassName, this.args,this.exceptionHandler); launcher.start(); launcher.join(); return launcher.getError(); }
RestartLauncher
源码:
RestartLauncher(ClassLoader classLoader, String mainClassName, String[] args, UncaughtExceptionHandler exceptionHandler) { this.mainClassName = mainClassName; this.args = args; // restartedMain线程名称就是在这类设置的 setName("restartedMain"); setUncaughtExceptionHandler(exceptionHandler); setDaemon(false); setContextClassLoader(classLoader); } @Override public void run() { try { // 应用restartClassLoader从新加载蕴含main办法的类 Class<?> mainClass = getContextClassLoader().loadClass(this.mainClassName); // 找到main办法 Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); //从新执行main办法 mainMethod.invoke(null, new Object[] { this.args }); } catch (Throwable ex) { this.error = ex; getUncaughtExceptionHandler().uncaughtException(this, ex); } }
回过头来在Restarter类中immediateRestart办法中doStart()办法调用之后,调用SilentExitExceptionHandler.exitCurrentThread()
静默抛弃咱们的Main线程。
private void immediateRestart() { try { // 上文中的doStart办法就是从这里进去的 getLeakSafeThread().callAndWait(() -> { start(FailureHandler.NONE); cleanupCaches(); return null; }); } catch (Exception ex) { this.logger.warn("Unable to initialize restarter", ex); } SilentExitExceptionHandler.exitCurrentThread(); }
SilentExitExceptionHandler
源码:
public static void exitCurrentThread() { throw new SilentExitException(); } // 运行时异样什么也不做,人不知;鬼不觉中把Jvm调配给咱们的主线程给替换了 private static class SilentExitException extends RuntimeException { }
总结:
到这里咱们理清了RestartClassLoader是如何替换AppClassLoader的,那依照失常的逻辑前面应用程序中所有的本地类都应该由RestartClassLoader加载。实时状况的确是,在没有命中缓存的时候报强制类型转换异样的类的classLoader的确是RestartClassLoader,命中缓存的就不是了,那问题是否是出在缓存层了呢。来看下spring-cache是如何应用的:
配置CacheManage:
@Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory){ // 默认的缓存配置 RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig(); Set<String> cacheNames = new HashSet<>(); cacheNames.add("cache_test"); // 对每个缓存空间利用不同的配置 Map<String, RedisCacheConfiguration> configMap = new HashMap<>(); configMap.put("cache_test", defaultCacheConfig); RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(defaultCacheConfig) .initialCacheNames(cacheNames) .withInitialCacheConfigurations(configMap) .build(); return cacheManager; }
看代码很显著他应用了默认的RedisCacheConfiguration的配置RedisCacheConfiguration.defaultCacheConfig()
源码
public static RedisCacheConfiguration defaultCacheConfig() { return defaultCacheConfig(null); } public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader classLoader) { DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); registerDefaultConverters(conversionService); return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(), SerializationPair.fromSerializer(RedisSerializer.string()), SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), conversionService); }
从RedisCacheConfiguration#defaultCacheConfig
源码能够看出两个点:
- 存在重载办法反对传入ClassLoader
- 默认提供的redis的Value序列化形式是:
RedisSerializer.java(classLoader)->new JdkSerializationRedisSerializer(classLoader)
到这里稍有教训的程序员应该都晓得JDK的序列化是由java.io.ObjectInputStream
来实现的。
我这里就不贴JdkSerializationRedisSerializer的源码了,代码比较简单,反正最初做反序列化这个工作的是ObjectInputStream的子类org.springframework.core.ConfigurableObjectInputStream
,该类重写了resolveClass()
办法,实现上首先判断是否存在ClassLoader,有的话间接用该ClassLoader加载该类。否则就调用父类的同名办法。而ObjectInputStream获取ClassLoader的形式则是调用VM.latestUserDefinedLoader()
,不理解latestUserDefinedLoader的能够本人百度下。到这里问题就很清晰了吧
那咱们改下代码,传入以后线程的ClassLoader试试,向上面这样:
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig(Thread.currentThread().getContextClassLoader())
果然能够了。这是为什么呢?因为在springboot-devtools中曾经替换了主线程,同时更换了与线程绑定的ClassLoader为RestartClassLoader,所以咱们这里从以后线程中取到的ClassLoader也是RestartClassLoader:
那么在命中缓存后反序列化就会应用咱们传入的这个RestartClassLoader而不是去从VM.latestUserDefinedLoader()
这里获取。
其实到这里第二个解决方案也就浮出水面了,咱们能够给RedisCacheConfiguration指定一个序列化工具,比方用fastjson作为spring-cache的序列化组件,向上面这样:
final RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()));)
来看下fastjson是如何做的:com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer#deserialize
的源码
public Object deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length == 0) { return null; } try { return JSON.parseObject(new String(bytes, IOUtils.UTF8), Object.class, defaultRedisConfig); } catch (Exception ex) { throw new SerializationException("Could not deserialize: " + ex.getMessage(), ex); } }
从JSON.parseObject
往下始终找到很深处,会在com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean)
中找到如下代码,看到了吧它也是从以后线程上下文中取ClassLoader
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) { .....去除一大段保障代码 try{ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if(contextClassLoader != null && contextClassLoader != classLoader){ clazz = contextClassLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; } } catch(Throwable e){ // skip } .....去除一大段保障代码 }
来看下这个ClassLoader是什么类型:
这里居然不是RestartClassLoader而是org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader
? 为什么这里不像配置CacheManager那里一样是RestartClassloader呢?因为这里的以后线程是用户申请线程,用户申请线程是由Web容器创立的,而配置CacheManager的代码是由springboot程序启动线程执行的:restartMain线程。而实际上TomcatEmbeddedWebappClassLoader的父ClassLoader就是RestartClassLoader,依据类加载双亲委派机制可知实际上最终还是由RestartClassLoader负责加载工作:
总结
问题实质:
- Springboot devtools更换了主线程及类加载器为RestartClassLoader
- spring-cache的缓存配置应用了默认的序列化配置:JdkSerializationRedisSerializer,且没有指定ClassLoader
解决方案:
- 在RedisCacheConfiguration缓存配置里指定以后线程的ClassLoader
- 或者不应用默认的序列化组件,更换序列化器组件:GenericFastJsonRedisSerializer