springDevTools提供了热部署的工具,按照网上的教程本地可以完美的支持热部署,但是一用到远程热部署(remoteRestart)就失效,热部署失败,抛ClassCastException异常 为了解决这个问题,咱们今天分析一下他的原理,为什么本地可以热部署,远程热部署就会抛异常
技术栈:springBoot + MVC + DUBBO + NACOS + maven打包插件spring-boot-maven-plugin
热部署原理分析 当我们修改class时,springDevTools利用了不同的classLoader重新加载class,并重新启动spring,使其生效 优点就是已经加载过的class并不会重新加载以便节省性能,只针对动态修改的class重新加载即可
已加载的class无法在线卸载,只能用新的classLoader去加载,这样就起到了热部署的效果 旧的classLoader以及对应加载过的class会被GC回收
在springBoot启动的时候,监听启动时的事件 ,然后用自己的热部署classLoader
去重新启动spring(通过反射再次调用main方法)
自己热部署classLoader
并没有遵循双亲委派机制 ,而是优先用最新的class
最新的class是由spring实时监听class文件的变化,如果有修改则会上传到最新的class中,并让devTools重新启动spring 如果想把老的class卸载,前提是对应的类加载器classLoader能被回收,这样当老的classLoader回收时所对应老的class也会一并销毁 如果采用双亲委派机制由JDK加载的话那就都无法回收,都无法重新加载,也就达不到热部署的目的了
在spring重新启动加载class的过程中,优先用新的classLoader加载新的class,以此达到热部署的目的
分析本地热部署 引入devTools的jar包,直接启动即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @Lazy(false) @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.devtools.restart", name = "enabled", matchIfMissing = true) static class RestartConfiguration { private final DevToolsProperties properties; RestartConfiguration(DevToolsProperties properties) { this .properties = properties; } @Bean FileSystemWatcherFactory fileSystemWatcherFactory () { return this ::newFileSystemWatcher; } ... private FileSystemWatcher newFileSystemWatcher () { Restart restartProperties = this .properties.getRestart(); FileSystemWatcher watcher = new FileSystemWatcher(true , restartProperties.getPollInterval(), restartProperties.getQuietPeriod()); String triggerFile = restartProperties.getTriggerFile(); if (StringUtils.hasLength(triggerFile)) { watcher.setTriggerFilter(new TriggerFileFilter(triggerFile)); } List<File> additionalPaths = restartProperties.getAdditionalPaths(); for (File path : additionalPaths) { watcher.addSourceDirectory(path.getAbsoluteFile()); } return watcher; } @Bean ApplicationListener<ClassPathChangedEvent> restartingClassPathChangedEventListener (FileSystemWatcherFactory fileSystemWatcherFactory) { return (event) -> { if (event.isRestartRequired()) { Restarter.getInstance().restart(new FileWatchingFailureHandler(fileSystemWatcherFactory)); } }; } @Bean @ConditionalOnMissingBean ClassPathFileSystemWatcher classPathFileSystemWatcher (FileSystemWatcherFactory fileSystemWatcherFactory, ClassPathRestartStrategy classPathRestartStrategy) { URL[] urls = Restarter.getInstance().getInitialUrls(); ClassPathFileSystemWatcher watcher = new ClassPathFileSystemWatcher(fileSystemWatcherFactory, classPathRestartStrategy, urls); watcher.setStopWatcherOnRestart(true ); return watcher; } ... }
分析远程热部署
用springBoot打包需要maven打包时包含devTools
1 2 3 4 5 6 7 <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > <configuration > <excludeDevtools > false</excludeDevtools > </configuration > </plugin >
配置文件中必须要配置secret spring.devtools.remote.secret=mysecret
启动远端服务器
启动本地代码,并把远端服务器的地址配置即可使用 指定Main Class为org.springframework.boot.devtools.RemoteSpringApplication 指定Program arguments为http://127.0.0.1:8081 即远端服务的地址
restartClassLoader只加载部分class的源码 devTools的热部署classLoader只会加载部分的class,其余的class归JDK加载,JDK加载的class无法实现热部署,那么他是如何只加载部分的class呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 final class ChangeableUrls implements Iterable <URL > { ... private final List<URL> urls; private ChangeableUrls (URL... urls) { DevToolsSettings settings = DevToolsSettings.get(); List<URL> reloadableUrls = new ArrayList<>(urls.length); for (URL url : urls) { if ((settings.isRestartInclude(url) || isDirectoryUrl(url.toString())) && !settings.isRestartExclude(url)) { reloadableUrls.add(url); } } this .urls = Collections.unmodifiableList(reloadableUrls); ... } private boolean isDirectoryUrl (String urlString) { return urlString.startsWith("file:" ) && urlString.endsWith("/" ); } ... @Override public Iterator<URL> iterator () { return this .urls.iterator(); } }
解决远程热部署不生效的问题 我们已经知道了他的原理是用新的classLoader去加载新的class,这样新的class会重新装载,热部署就会生效 根据网上的教程配置好remote远程热部署之后发现修改class、自动上传、从新启动时就会报ClassNotFondException或者会报ClassCastException
ClassNotFondException 配置热部署检测文件改动的时间间隔就行 因为class编译后会重新覆盖,覆盖过程中会先把老的文件给删除,因为有时间差,所以spring误以为是删除而不是更新,这样导致重新启动的时候spring就找不到class了。
那为什么本地的就不会有这个问题,或者有这个问题之后不一会就好了呢? 因为remote是通过http协议传输新class的。在重新启动的过程中http会停止服务,一旦启动不起来就无法和cline交互了,也就永远起不来了 而本地没有通过http,它是在本地内存中直接执行热部署的代码逻辑,所以即使启动失败了也没关系,不一会他能检测到新文件的到来并且会触发事件重新启动,这样就不会报ClassNotFondException异常了
但是因为时间差的原因,还会有几率出现这样的问题,那么只能从源码中下手改代码了,或者用triggerFile触发更新也可以
ClassCastException 原来是maven打包插件spring-boot-maven-plugin惹的祸,通过此插件会把我们的项目打包成jar包。这样spring无法识别是file文件 ,class就被JDK的classLoader加载了 当新的classLoader加载最新的class时,由于没有采用双亲委派机制,导致父和子classLoader都有加载过这个class,但是会优先用子classLoader加载的class,导致class在链接的时候出现了ClassCastException异常
解决办法是在devTools的classLoader加载class的时候,把我们的jar包给指定上就可以了
remoteRestart-ClassCastException异常解决
项目编译时设置特定的标识 maven配置如下插件,在打包后的MANIFEST.MF中添加自定义的属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-jar-plugin</artifactId > <configuration > <archive > <manifestEntries > <reload > true</reload > </manifestEntries > </archive > </configuration > </plugin > </plugins > </build >
编写代码,覆盖spring的getInitialUrls
,指定返回要热加载的jar包
注意通过编写一模一样的包名,放在自己的项目中,当启动的时候会优先用自己编写的,为什么会优先用自己编写的呢?这个和classPath有关
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package org.springframework.boot.devtools.restart;import lombok.extern.slf4j.Slf4j;import org.springframework.boot.devtools.logger.DevToolsLogFactory;import org.springframework.boot.devtools.system.DevToolsEnablementDeducer;import java.net.JarURLConnection;import java.net.URL;import java.net.URLClassLoader;import java.util.Arrays;import java.util.Collection;import java.util.HashSet;import java.util.Set;import java.util.stream.Collectors;@Slf4j public class DefaultRestartInitializer implements RestartInitializer { @Override public URL[] getInitialUrls(Thread thread) { if (!thread.getName().equals("main" )) { return null ; } if (!DevToolsEnablementDeducer.shouldEnable(thread)) { return null ; } Collection<URL> urls = getUrls(thread); DevToolsLogFactory.getLog(DefaultRestartInitializer.class).info("reload urls:" + urls); return urls.toArray(new URL[0 ]); } private Set<URL> getUrls (Thread thread) { HashSet<URL> urls = new HashSet<>(); urls.addAll(ChangeableUrls.fromClassLoader(thread.getContextClassLoader()).toList()); urls.addAll(Arrays.stream(((URLClassLoader) thread.getContextClassLoader()).getURLs()) .filter(t -> t.getPath().contains("dubbo" ) || t.getPath().contains("nacos-spring-context" ) || t.getPath().contains("nacos-config-spring-boot-autoconfigure" ) ).collect(Collectors.toSet())); urls.addAll(Arrays.stream(((URLClassLoader) thread.getContextClassLoader()).getURLs()) .filter(t -> { try { JarURLConnection urlConnection = (JarURLConnection) t.openConnection(); return Boolean.parseBoolean(urlConnection.getMainAttributes().getValue("reload" )); } catch (Exception e) { return false ; } }).collect(Collectors.toSet())); return urls; } }
这样配置好之后,restartClassLoader会加载这些class,不然父的classLoader加载的class无法卸载。也就无法实现热部署的效果了
优化热部署-减少部署时间 优先用agent替换字节码的方式实现热部署 ,秒级生效(如果不违反规范,且可以生效的话) 如果失败,则还跟之前是有的方式一模一样,需要注意的是,需要修改restart文件才会重启(类似triggerFile触发重启,可以避免ClassNotFondException)
使用方式不变,跟springDevToolsRestart一模一样,修改后文件后编译即可,不一样的是服务端做了手脚,增加一层优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 @Slf4j @Configuration @ConditionalOnClass(HttpRestartServer.class) public class DevToolsReloadConfig { @Autowired ResourceLoader resourceLoader; @Bean public HttpRestartServer remoteRestartHttpRestartServer () { return new HttpRestartServer(getRestartServer(new DefaultSourceDirectoryUrlFilter())); } private RestartServer getRestartServer (SourceDirectoryUrlFilter sourceDirectoryUrlFilter) { return new RestartServer(sourceDirectoryUrlFilter) { final ClassLoaderFiles newFile = new ClassLoaderFiles(); Set<String> waitingRestartFileNames = new HashSet<>(); protected synchronized void restart (Set<URL> urls, ClassLoaderFiles newFiles) { for (ClassLoaderFiles.SourceDirectory sourceDirectory : newFiles.getSourceDirectories()) { for (Map.Entry<String, ClassLoaderFile> fileEntry : sourceDirectory.getFilesEntrySet()) { newFile.addFile(sourceDirectory.getName(), fileEntry.getKey(), fileEntry.getValue()); try { if (fileEntry.getKey().endsWith(".class" ) && fileEntry.getValue().getKind() != ClassLoaderFile.Kind.DELETED) { DynamicInstrumentationLoader.waitForInitialized(); String className = fileEntry.getKey() .replace("/" , "." ) .replace(".class" , "" ); Class<?> oldClass = ClassUtils.forName(className, resourceLoader.getClassLoader()); ClassDefinition classDefinition = new ClassDefinition(oldClass, fileEntry.getValue().getContents()); InstrumentationSavingAgent.getInstrumentation().redefineClasses(classDefinition); waitingRestartFileNames.remove(fileEntry.getKey()); log.info("class reload:{}" , fileEntry.getKey()); continue ; } } catch (Throwable e) { log.info(fileEntry.getKey() + "reload failure,Modify the restart file for the restart to take effect:" + e.getClass().getName() + ":" + e.getMessage()); } waitingRestartFileNames.add(fileEntry.getKey()); } } if (waitingRestartFileNames.isEmpty() || !waitingRestartFileNames.contains("restart" )) { log.info("waitingRestartFileNames is empty or no restart file,waitingRestartFileNames:{}" , waitingRestartFileNames); return ; } Restarter restarter = Restarter.getInstance(); restarter.addUrls(urls); restarter.addClassLoaderFiles(newFile); Set<String> persistentUpdatedFileNames = new HashSet<>(waitingRestartFileNames); waitingRestartFileNames = new HashSet<>(); log.warn("spring restart new files:{}" , persistentUpdatedFileNames); restarter.restart(failure -> { if (persistentUpdatedFileNames.isEmpty()) { return FailureHandler.Outcome.ABORT; } try { ClassLoaderFiles classLoaderFiles = (ClassLoaderFiles) FieldUtils.readDeclaredField(restarter, "classLoaderFiles" , true ); @SuppressWarnings("unchecked") Map<String, ClassLoaderFiles.SourceDirectory> sourceDirectories = (Map<String, ClassLoaderFiles.SourceDirectory>) FieldUtils.readDeclaredField(classLoaderFiles, "sourceDirectories" , true ); for (Map.Entry<String, ClassLoaderFiles.SourceDirectory> directoryEntry : sourceDirectories.entrySet()) { @SuppressWarnings("unchecked") Map<String, ClassLoaderFile> files = (Map<String, ClassLoaderFile>) FieldUtils.readDeclaredField(directoryEntry.getValue(), "files" , true ); for (String name : persistentUpdatedFileNames) { files.remove(name); } } log.warn("retry failure,Try to delete the new file and restart:{}" , persistentUpdatedFileNames); persistentUpdatedFileNames.clear(); return FailureHandler.Outcome.RETRY; } catch (IllegalAccessException e) { throw new RuntimeException(e); } }); } }; } }