Spring中的Devtools源码解析
作者:项哥
这篇文章主要介绍了Spring中的Devtools源码解析,Spring中的Devtools是一个开发工具,旨在提高开发人员的生产力和开发体验,它提供了一系列功能,包括自动重启、热部署、远程调试等,使开发人员能够更快速地进行代码修改和调试,需要的朋友可以参考下
Spring Devtools 核心流程
1.spring.factories
定义了很多需要被初始化的类,程序启动的时候会扫描spring.factories并注册事件监听者RestartApplicationListener
# Application Initializers org.springframework.context.ApplicationContextInitializer=\ org.springframework.boot.devtools.restart.RestartScopeInitializer # Application Listeners org.springframework.context.ApplicationListener=\ org.springframework.boot.devtools.restart.RestartApplicationListener,\ org.springframework.boot.devtools.logger.DevToolsLogFactory.Listener # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.devtools.autoconfigure.DevToolsDataSourceAutoConfiguration,\ org.springframework.boot.devtools.autoconfigure.LocalDevToolsAutoConfiguration,\ org.springframework.boot.devtools.autoconfigure.RemoteDevToolsAutoConfiguration # Environment Post Processors org.springframework.boot.env.EnvironmentPostProcessor=\ org.springframework.boot.devtools.env.DevToolsHomePropertiesPostProcessor,\ org.springframework.boot.devtools.env.DevToolsPropertyDefaultsPostProcessor
2. RestartApplicationListener
监听到 ApplicationStartingEvent 事件以后,另外启动一个线程重新启动main函数使用 RestartClassLoader 来加载类,将原来的主线程hold住
onApplicationStartingEvent(ApplicationStartingEvent event)
- Restarter#initialize
- Restarter#immediateRestart
private void immediateRestart() { try { // 新启动一个线程执行runnable, 执行完成以后会join(),hold住主线程 getLeakSafeThread().callAndWait(() -> { // start-》doStart-》relaunch,RestartLauncher新启动一个线程执行SpringApplication类的main函数 start(FailureHandler.NONE); // 当Spring初始化成功以后,先清理相关缓存 cleanupCaches(); return null; }); } catch (Exception ex) { this.logger.warn("Unable to initialize restarter", ex); } // 这里是在程序结束以后才会执行到,抛出异常,终止主线程 SilentExitExceptionHandler.exitCurrentThread(); }
public class RestartLauncher extends Thread { // ....省略 @Override public void run() { try { Class<?> mainClass = getContextClassLoader().loadClass(this.mainClassName); Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); mainMethod.invoke(null, new Object[] { this.args }); } catch (Throwable ex) { this.error = ex; getUncaughtExceptionHandler().uncaughtException(this, ex); } } }
3.监听文件变化后进行重启
- ClassPathFileSystemWatcher 在初始化的时候调用了fileSystemWatcher.addListener创建了一个监听者,并且启动了文件夹监控线程
public class ClassPathFileSystemWatcher implements InitializingBean, DisposableBean, ApplicationContextAware { ...... @Override public void afterPropertiesSet() throws Exception { if (this.restartStrategy != null) { FileSystemWatcher watcherToStop = null; if (this.stopWatcherOnRestart) { watcherToStop = this.fileSystemWatcher; } this.fileSystemWatcher.addListener( new ClassPathFileChangeListener(this.applicationContext, this.restartStrategy, watcherToStop)); } this.fileSystemWatcher.start(); } ...... }
- 文件监控 FileSystemWatcher 线程类,就是将要监控的目录加入到this.folders, 启动线程不断的扫描文件夹的diff, 如果有ChangedFiles,通知监听者fireListeners
private void scan() throws InterruptedException { Thread.sleep(this.pollInterval - this.quietPeriod); Map<File, FolderSnapshot> previous; Map<File, FolderSnapshot> current = this.folders; do { previous = current; current = getCurrentSnapshots(); Thread.sleep(this.quietPeriod); } while (isDifferent(previous, current)); if (isDifferent(this.folders, current)) { // 得到changeSet,然后fireListeners通知监听者 updateSnapshots(current.values()); } }
- ClassPathFileChangeListener.onChange fireListeners时被调用
- publishEvent(new ClassPathChangedEvent(this, changeSet, restart))
- LocalDevToolsAutoConfiguration.RestartConfiguration监听到ClassPathChangedEvent事件然后调用Restarter.getInstance().restart重启
- publishEvent(new ClassPathChangedEvent(this, changeSet, restart))
static class RestartConfiguration implements ApplicationListener<ClassPathChangedEvent> { ....... @Override public void onApplicationEvent(ClassPathChangedEvent event) { if (event.isRestartRequired()) { Restarter.getInstance().restart(new FileWatchingFailureHandler(fileSystemWatcherFactory())); } } ...... }
public void restart(FailureHandler failureHandler) { getLeakSafeThread().call(() -> { // 调用rootContexts.close()销毁所有的bean,清理缓存,gc Restarter.this.stop(); // 再重启 Restarter.this.start(failureHandler); return null; }); }
4.RestartClassLoader
优先从 updatedFiles 中取更新过的类进行加载 ClassLoaderFiles#getFile
public class RestartClassLoader extends URLClassLoader implements SmartClassLoader { ...... private final ClassLoaderFileRepository updatedFiles; public RestartClassLoader(ClassLoader parent, URL[] urls, ClassLoaderFileRepository updatedFiles, Log logger) { super(urls, parent); this.updatedFiles = updatedFiles; this.logger = logger; } @Override public Enumeration<URL> getResources(String name) throws IOException { // 父类classLoader加载资源 Enumeration<URL> resources = getParent().getResources(name); // 变更类的资源 ClassLoaderFile file = this.updatedFiles.getFile(name); if (file != null) { // Assume that we're replacing just the first item if (resources.hasMoreElements()) { resources.nextElement(); } if (file.getKind() != Kind.DELETED) { // 将更新过的ClassLoaderFile类放在URL的前面,优先加载变更类的URL return new CompoundEnumeration<>(createFileUrl(name, file), resources); } } return resources; } @Override public URL getResource(String name) { // 先加载变更类的URL ClassLoaderFile file = this.updatedFiles.getFile(name); if (file != null && file.getKind() == Kind.DELETED) { return null; } URL resource = findResource(name); if (resource != null) { return resource; } return getParent().getResource(name); } @Override public URL findResource(String name) { final ClassLoaderFile file = this.updatedFiles.getFile(name); if (file == null) { return super.findResource(name); } if (file.getKind() == Kind.DELETED) { return null; } return AccessController.doPrivileged((PrivilegedAction<URL>) () -> createFileUrl(name, file)); } @Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { String path = name.replace('.', '/').concat(".class"); ClassLoaderFile file = this.updatedFiles.getFile(path); if (file != null && file.getKind() == Kind.DELETED) { throw new ClassNotFoundException(name); } synchronized (getClassLoadingLock(name)) { Class<?> loadedClass = findLoadedClass(name); if (loadedClass == null) { try { loadedClass = findClass(name); } catch (ClassNotFoundException ex) { loadedClass = getParent().loadClass(name); } } if (resolve) { resolveClass(loadedClass); } return loadedClass; } } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String path = name.replace('.', '/').concat(".class"); final ClassLoaderFile file = this.updatedFiles.getFile(path); if (file == null) { return super.findClass(name); } if (file.getKind() == Kind.DELETED) { throw new ClassNotFoundException(name); } return AccessController.doPrivileged((PrivilegedAction<Class<?>>) () -> { byte[] bytes = file.getContents(); return defineClass(name, bytes, 0, bytes.length); }); } private URL createFileUrl(String name, ClassLoaderFile file) { try { return new URL("reloaded", null, -1, "/" + name, new ClassLoaderFileURLStreamHandler(file)); } catch (MalformedURLException ex) { throw new IllegalStateException(ex); } } ...... }
5.清理缓存
Restarter#cleanupCaches 和其他热更新类似,清理缓存
private void cleanupCaches() throws Exception { Introspector.flushCaches(); cleanupKnownCaches(); } private void cleanupKnownCaches() throws Exception { ResolvableType.clearCache(); cleanCachedIntrospectionResultsCache(); ReflectionUtils.clearCache(); clearAnnotationUtilsCache(); if (!JavaVersion.getJavaVersion().isEqualOrNewerThan(JavaVersion.NINE)) { clear("com.sun.naming.internal.ResourceManager", "propertiesCache"); } } private void cleanCachedIntrospectionResultsCache() throws Exception { clear(CachedIntrospectionResults.class, "acceptedClassLoaders"); clear(CachedIntrospectionResults.class, "strongClassCache"); clear(CachedIntrospectionResults.class, "softClassCache"); } private void clearAnnotationUtilsCache() throws Exception { try { AnnotationUtils.clearCache(); } catch (Throwable ex) { clear(AnnotationUtils.class, "findAnnotationCache"); clear(AnnotationUtils.class, "annotatedInterfaceCache"); } } private void clear(String className, String fieldName) { try { clear(Class.forName(className), fieldName); } catch (Exception ex) { if (this.logger.isDebugEnabled()) { this.logger.debug("Unable to clear field " + className + " " + fieldName, ex); } } } private void clear(Class<?> type, String fieldName) throws Exception { try { Field field = type.getDeclaredField(fieldName); field.setAccessible(true); Object instance = field.get(null); if (instance instanceof Set) { ((Set<?>) instance).clear(); } if (instance instanceof Map) { ((Map<?, ?>) instance).keySet().removeIf(this::isFromRestartClassLoader); } } catch (Exception ex) { if (this.logger.isDebugEnabled()) { this.logger.debug("Unable to clear field " + type + " " + fieldName, ex); } } }
远程更新
服务端
RemoteDevToolsAutoConfiguration 配置,只有设置了spring.devtools.remote.secret才初始化
- 增加了GET /接口进行健康检查
- 增加了POST /restart接口进行远程热更新,将body体反序列化得到变更的类资源ClassLoaderFiles,然后调用RestartServer#restart进行重启
- 增加了AccessManager验证接口的请求头X-AUTH-TOKEN传递的secret
@Configuration // 只有设置了spring.devtools.remote.secret才初始化 @ConditionalOnProperty(prefix = "spring.devtools.remote", name = "secret") public class RemoteDevToolsAutoConfiguration { ...... @Bean @ConditionalOnMissingBean public AccessManager remoteDevToolsAccessManager() { // 权限空值,根据请求头的X-AUTH-TOKEN来验证密钥是否一致 RemoteDevToolsProperties remoteProperties = this.properties.getRemote(); return new HttpHeaderAccessManager(remoteProperties.getSecretHeaderName(), remoteProperties.getSecret()); } // 增加一个根路径接口GET /来进行监控检查,使用HttpStatusHandler返回一个HttpStatus.OK来判定服务已经启动成功 @Bean public HandlerMapper remoteDevToolsHealthCheckHandlerMapper() { Handler handler = new HttpStatusHandler(); Servlet servlet = this.serverProperties.getServlet(); String servletContextPath = (servlet.getContextPath() != null) ? servlet.getContextPath() : ""; return new UrlHandlerMapper(servletContextPath + this.properties.getRemote().getContextPath(), handler); } @Bean @ConditionalOnMissingBean public DispatcherFilter remoteDevToolsDispatcherFilter(AccessManager accessManager, Collection<HandlerMapper> mappers) { // 上面定义HandlerMapper使用AccessManager进行拦截权限 Dispatcher dispatcher = new Dispatcher(accessManager, mappers); return new DispatcherFilter(dispatcher); } @Configuration @ConditionalOnProperty(prefix = "spring.devtools.remote.restart", name = "enabled", matchIfMissing = true) static class RemoteRestartConfiguration { ...... // 增加一个接口POST /restart进行资源更新和重启, @Bean @ConditionalOnMissingBean(name = "remoteRestartHandlerMapper") public UrlHandlerMapper remoteRestartHandlerMapper(HttpRestartServer server) { Servlet servlet = this.serverProperties.getServlet(); RemoteDevToolsProperties remote = this.properties.getRemote(); String servletContextPath = (servlet.getContextPath() != null) ? servlet.getContextPath() : ""; String url = servletContextPath + remote.getContextPath() + "/restart"; logger.warn("Listening for remote restart updates on " + url); Handler handler = new HttpRestartServerHandler(server); return new UrlHandlerMapper(url, handler); } } }
RestartServer#restart
protected void restart(Set<URL> urls, ClassLoaderFiles files) { Restarter restarter = Restarter.getInstance(); restarter.addUrls(urls); // 优先从变更的类中加载类 restarter.addClassLoaderFiles(files); restarter.restart(); }
客户端
RemoteClientConfiguration配置初始化
- RemoteRestartClientConfiguration
- ClassPathFileSystemWatcher 目录监控初始化
- ClassPathChangeUploader 监听ClassPathChangedEvent事件,调用远程服务http://remoteUrl/restart接口上传更新的classLoaderFiles序列化字节数据
- 监听ClassPathChangedEvent事件,当文件变更以后,休眠shutdownTime时间,循环调用http://remoteUrl/接口,判断远端服务是否重启成功,启动成功以后this.liveReloadServer.triggerReload()自动更新浏览器
到此这篇关于Spring中的Devtools源码解析的文章就介绍到这了,更多相关Devtools源码解析内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!