Tuesday, June 2, 2020

Hot reloading the resources file for changes in Grails.


How to hot reload the resources file when the changes happened in Grails.


I was working on Grails 3.3.0 and hot reloading was not working while changing in the resources files. So, I found some workaround which is worth sharing here. There are the following steps to resolve the issue.

  1. Configure to watch the directory.
  2. Watch the directory for code changes
  3. Reload spring resources config
  4. Load from the application

Configure to watch the directory:




Here I am creating a class BeanWatcher.groovy to watch the config file changes.


import grails.spring.BeanBuilder
import grails.util.Environment
import grails.util.Holders
import groovy.util.logging.Slf4j
import org.grails.core.exceptions.GrailsConfigurationException
import org.grails.spring.DefaultRuntimeSpringConfiguration
import org.grails.spring.RuntimeSpringConfigUtilities
import org.springframework.beans.factory.support.BeanDefinitionRegistry

import java.nio.file.FileSystems
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.SimpleFileVisitor
import java.nio.file.StandardWatchEventKinds
import java.nio.file.WatchEvent
import java.nio.file.WatchKey
import java.nio.file.WatchService
import java.nio.file.attribute.BasicFileAttributes
import java.util.concurrent.atomic.AtomicBoolean
    
    class BeanWatcher extends Thread{
    
    
    private final WatchService watchService
    private long sleepTime = 1000
    private AtomicBoolean stop = new AtomicBoolean(false)
    
    public BeanWatcher(Path path){
        watchService = FileSystems.getDefault().newWatchService()
        walkAndRegisterDirectories(path)

    }
    

    private void walkAndRegisterDirectories(final Path start){
        // register directory and sub-directories
        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs){
                registerDirectory(dir)
                return FileVisitResult.CONTINUE
            }
        })
    }
    

    private void registerDirectory(dir){

        dir.register(
                watchService,
                StandardWatchEventKinds.ENTRY_MODIFY)
    }
    
    
     @Override
    void run() {

       
    }
    
     static void configureBeanWatcher(){
        Environment environment = Environment.current
        File baseDir = new File(environment.getReloadLocation()).canonicalFile
        String location = baseDir.canonicalPath
        File watchDir = new File(location, "grails-app/conf/spring")
        Path path = watchDir.toPath()
        BeanWatcher beanWatcher = new BeanWatcher(path)
        beanWatcher.start()
    }
    
    
    }

I want to watch the config spring file so I provided the path: "grails-app/conf/spring" under the project directory. As we are running in the thread so the current thread will not be interrupted for each time file changes. Here, we are registering directories and sub-directories.


Watch the directory for code changes:

    @Override
    void run() {

        try {
            WatchKey key
            try {
                while ((key = watchService.take()) != null) {
                    List<WatchEvent <?>> watchEvents = key.pollEvents()
                    for (WatchEvent <?> event : watchEvents) {
                        WatchEvent.Kind <?> kind = event.kind()
                        WatchEvent <Path> pathWatchEvent = cast(event)
                        Path name = pathWatchEvent.context()
                        Path dir = (Path) key.watchable()
                        Path child = dir.resolve(name).toAbsolutePath()
                        File childFile = child.toFile()
                        if(kind == StandardWatchEventKinds.ENTRY_MODIFY){
                            onChange(childFile)
                        }
                    }
                    key.reset()
                }
            } catch (InterruptedException e) {
                e.printStackTrace()
            }
        } catch (IOException e) {
            e.printStackTrace()
        }

    }
        

    @SuppressWarnings("unchecked")
    private static <T> WatchEvent<T> cast(WatchEvent<?> event) {
        return (WatchEvent<T>)event
    }
        
The above code will listen to the file changes and call the method onChange for each time the file changes. Now, it's time to reload our resources file while changes occurred.


Reload spring resources config:

   private static void onChange(File file) { // the changed file 
        processBeanDefinitionRegistry()
    }
    
    public static void processBeanDefinitionRegistry(){
        def springConfig = new DefaultRuntimeSpringConfiguration()
        def application = Holders.grailsApplication
        def context = application.mainContext
        def beanResources = context.getResource(RuntimeSpringConfigUtilities.SPRING_RESOURCES_GROOVY)
        if (beanResources?.exists()) {
            def gcl = new GroovyClassLoader(application.classLoader)
            try {
                RuntimeSpringConfigUtilities.reloadSpringResourcesConfig(springConfig, application, gcl.parseClass(new GroovyCodeSource(beanResources.URL)))
            } catch (Throwable e) {
                throw new GrailsConfigurationException("Error loading spring/resources.groovy file: ${e.message}", e)
            }
        }
        def bb = new BeanBuilder(null, springConfig, application.classLoader)
        bb.registerBeans((BeanDefinitionRegistry)application.getMainContext())
    }
    


This is the code snippet that I found in the grails where they used to reload the file. This will reload and re-configure the resources file.






Load from the application:

We set up all the necessary config and necessary code inside BeanWatcher.groovy now lets load the file from application for this add the following code.

Application.groovy

    if (Environment.current == Environment.DEVELOPMENT){
            BeanWatcher.configureBeanWatcher()
        }
    
This is only for the development env so we did the same.
Share:

0 comments: