Spring Devtools Core Process
Define many classes that need to be initialized. When the program starts, it will scan and register the event listener RestartApplicationListener.
# Application Initializers =\ # Application Listeners =\ ,\ # Auto Configure =\ ,\ ,\ # Environment Post Processors =\ ,\
2. RestartApplicationListener
After listening to the ApplicationStartingEvent event, another thread is started and restarted the main function. Use the RestartClassLoader to load the class and hold the original main thread.
onApplicationStartingEvent(ApplicationStartingEvent event)
- Restarter#initialize
- Restarter#immediateRestart
private void immediateRestart() { try { // A new thread will be started to execute runnable. After the execution is completed, it will join() and hold the main thread getLeakSafeThread().callAndWait(() -> { // start-》doStart-》relaunch, RestartLauncher starts a new thread to execute the main function of the SpringApplication class start(); // After Spring initialization is successful, clean up the relevant cache first cleanupCaches(); return null; }); } catch (Exception ex) { ("Unable to initialize restarter", ex); } // This will be executed after the program is finished, throw an exception and terminate the main thread (); }
public class RestartLauncher extends Thread { // ....Omitted @Override public void run() { try { Class<?> mainClass = getContextClassLoader().loadClass(); Method mainMethod = ("main", String[].class); (null, new Object[] { }); } catch (Throwable ex) { = ex; getUncaughtExceptionHandler().uncaughtException(this, ex); } } }
3. Restart after listening to the file changes
- ClassPathFileSystemWatcher calls to create a listener during initialization and starts the folder monitoring thread.
public class ClassPathFileSystemWatcher implements InitializingBean, DisposableBean, ApplicationContextAware { ...... @Override public void afterPropertiesSet() throws Exception { if ( != null) { FileSystemWatcher watcherToStop = null; if () { watcherToStop = ; } ( new ClassPathFileChangeListener(, , watcherToStop)); } (); } ...... }
- File Monitor FileSystemWatcher thread class, which is to add the directory to be monitored, start the thread to continuously scan the folder diff. If there is Changefiles, notify the listener fireListeners
private void scan() throws InterruptedException { ( - ); Map<File, FolderSnapshot> previous; Map<File, FolderSnapshot> current = ; do { previous = current; current = getCurrentSnapshots(); (); } while (isDifferent(previous, current)); if (isDifferent(, current)) { // Get the changeSet, and then fireListeners notify the listener updateSnapshots(()); } }
- FireListeners is called
- publishEvent(new ClassPathChangedEvent(this, changeSet, restart))
- Listen to the ClassPathChangedEvent event and call().restart to restart
- publishEvent(new ClassPathChangedEvent(this, changeSet, restart))
static class RestartConfiguration implements ApplicationListener<ClassPathChangedEvent> { ....... @Override public void onApplicationEvent(ClassPathChangedEvent event) { if (()) { ().restart(new FileWatchingFailureHandler(fileSystemWatcherFactory())); } } ...... }
public void restart(FailureHandler failureHandler) { getLeakSafeThread().call(() -> { // Call() to destroy all beans, clean cache, gc (); // Restart again (failureHandler); return null; }); }
Priority is given to loading updated classes from 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); = updatedFiles; = logger; } @Override public Enumeration<URL> getResources(String name) throws IOException { // Parent class classLoader loads resources Enumeration<URL> resources = getParent().getResources(name); // Change resource ClassLoaderFile file = (name); if (file != null) { // Assume that we're replacing just the first item if (()) { (); } if (() != ) { // Put the updated ClassLoaderFile class in front of the URL, and the URL of the changed class is loaded first return new CompoundEnumeration<>(createFileUrl(name, file), resources); } } return resources; } @Override public URL getResource(String name) { // Load the URL of the change class first ClassLoaderFile file = (name); if (file != null && () == ) { return null; } URL resource = findResource(name); if (resource != null) { return resource; } return getParent().getResource(name); } @Override public URL findResource(String name) { final ClassLoaderFile file = (name); if (file == null) { return (name); } if (() == ) { return null; } return ((PrivilegedAction<URL>) () -> createFileUrl(name, file)); } @Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { String path = ('.', '/').concat(".class"); ClassLoaderFile file = (path); if (file != null && () == ) { 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 = ('.', '/').concat(".class"); final ClassLoaderFile file = (path); if (file == null) { return (name); } if (() == ) { throw new ClassNotFoundException(name); } return ((PrivilegedAction<Class<?>>) () -> { byte[] bytes = (); return defineClass(name, bytes, 0, ); }); } 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. Clean up the cache
Restarter#cleanupCaches is similar to other hot updates, cleans up caches
private void cleanupCaches() throws Exception { (); cleanupKnownCaches(); } private void cleanupKnownCaches() throws Exception { (); cleanCachedIntrospectionResultsCache(); (); clearAnnotationUtilsCache(); if (!().isEqualOrNewerThan()) { clear("", "propertiesCache"); } } private void cleanCachedIntrospectionResultsCache() throws Exception { clear(, "acceptedClassLoaders"); clear(, "strongClassCache"); clear(, "softClassCache"); } private void clearAnnotationUtilsCache() throws Exception { try { (); } catch (Throwable ex) { clear(, "findAnnotationCache"); clear(, "annotatedInterfaceCache"); } } private void clear(String className, String fieldName) { try { clear((className), fieldName); } catch (Exception ex) { if (()) { ("Unable to clear field " + className + " " + fieldName, ex); } } } private void clear(Class<?> type, String fieldName) throws Exception { try { Field field = (fieldName); (true); Object instance = (null); if (instance instanceof Set) { ((Set<?>) instance).clear(); } if (instance instanceof Map) { ((Map<?, ?>) instance).keySet().removeIf(this::isFromRestartClassLoader); } } catch (Exception ex) { if (()) { ("Unable to clear field " + type + " " + fieldName, ex); } } }
Remote update
Server side
RemoteDevToolsAutoConfiguration configuration, only initializes after setting
- Added GET/interface for health checks
- Added the POST /restart interface for remote hot update, deserialize the body body to get the changed class resource ClassLoaderFiles, and then call RestartServer#restart to restart
- Added the request header of the AccessManager verification interface, the secret passed by X-AUTH-TOKEN
@Configuration // Initialize only after setting@ConditionalOnProperty(prefix = "", name = "secret") public class RemoteDevToolsAutoConfiguration { ...... @Bean @ConditionalOnMissingBean public AccessManager remoteDevToolsAccessManager() { // Permission value is empty, verify whether the key is consistent based on the X-AUTH-TOKEN of the request header RemoteDevToolsProperties remoteProperties = (); return new HttpHeaderAccessManager((), ()); } // Add a root path interface GET / for monitoring and checking, and use HttpStatusHandler to return one to determine that the service has been started successfully @Bean public HandlerMapper remoteDevToolsHealthCheckHandlerMapper() { Handler handler = new HttpStatusHandler(); Servlet servlet = (); String servletContextPath = (() != null) ? () : ""; return new UrlHandlerMapper(servletContextPath + ().getContextPath(), handler); } @Bean @ConditionalOnMissingBean public DispatcherFilter remoteDevToolsDispatcherFilter(AccessManager accessManager, Collection<HandlerMapper> mappers) { // The above definition HandlerMapper uses AccessManager to intercept permissions Dispatcher dispatcher = new Dispatcher(accessManager, mappers); return new DispatcherFilter(dispatcher); } @Configuration @ConditionalOnProperty(prefix = "", name = "enabled", matchIfMissing = true) static class RemoteRestartConfiguration { ...... // Add an interface POST /restart to update and restart resources. @Bean @ConditionalOnMissingBean(name = "remoteRestartHandlerMapper") public UrlHandlerMapper remoteRestartHandlerMapper(HttpRestartServer server) { Servlet servlet = (); RemoteDevToolsProperties remote = (); String servletContextPath = (() != null) ? () : ""; String url = servletContextPath + () + "/restart"; ("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 = (); (urls); //Preferentially load classes from changed classes (files); (); }
Client
RemoteClientConfiguration Configuration Initialization
- RemoteRestartClientConfiguration
- ClassPathFileSystemWatcher Directory Monitoring Initialization
- ClassPathChangeUploader listens to the ClassPathChangedEvent event, calls the remote service http://remoteUrl/restart interface to upload the updated classLoaderFiles serialized byte data
- Listen to the ClassPathChangedEvent event. After the file is changed, sleep shutdownTime time, and call the http://remoteUrl/ interface in a loop to determine whether the remote service is restarted successfully. After the startup is successful () automatically update the browser.
This is the end of this article about Devtools source code analysis in Spring. For more related Devtools source code analysis content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!