Flow: Investigate JRebel hook

Created on 6 Mar 2020  路  4Comments  路  Source: vaadin/flow

Investigate and document how we can hook into JRebel and call our own methods (for multiplexing reload message to all open UIs). Related to #7740.

investigation

Most helpful comment

Using org.zeroturnaround:jr-sdk and org.zeroturnaround:jr-util from https://repos.zeroturnaround.com/nexus/content/groups/zt-public/
class changes can be hooked into server-side by registering a listener as follows

this.reloadListener = new ClassEventListenerAdapter(0) {
  @Override
  public void onClassEvent(int eventType, Class<?> klass) throws Exception {
    // Notify interested parties of reload
  }
}
ReloaderFactory.getInstance().addClassReloadListener(WeakUtil.weak(this.reloadListener));

The above will register the listener via a weak reference so make sure it is stored in a field so that it doesn't get collected immediately and is tied to the lifetime of some object. Manual deregistration is also possible via ReloaderFactory.getInstance().removeClassReloadListener()

Note that usually many classes change in a short interval so may want to buffer the events if an action like browser reload is desired.

The code is safe to run when JRebel is not present as the factories in jr-sdk return NO-OP instances in that case.

All 4 comments

Using org.zeroturnaround:jr-sdk and org.zeroturnaround:jr-util from https://repos.zeroturnaround.com/nexus/content/groups/zt-public/
class changes can be hooked into server-side by registering a listener as follows

this.reloadListener = new ClassEventListenerAdapter(0) {
  @Override
  public void onClassEvent(int eventType, Class<?> klass) throws Exception {
    // Notify interested parties of reload
  }
}
ReloaderFactory.getInstance().addClassReloadListener(WeakUtil.weak(this.reloadListener));

The above will register the listener via a weak reference so make sure it is stored in a field so that it doesn't get collected immediately and is tied to the lifetime of some object. Manual deregistration is also possible via ReloaderFactory.getInstance().removeClassReloadListener()

Note that usually many classes change in a short interval so may want to buffer the events if an action like browser reload is desired.

The code is safe to run when JRebel is not present as the factories in jr-sdk return NO-OP instances in that case.

@murkaje thank you so much for your comment.

To be able to get notification about class reload one should:

  • add a repository into pom.xml
<repositories>
        <repository>
            <id>JRebel Directory</id>
            <url>https://repos.zeroturnaround.com/nexus/content/groups/zt-public/</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
  • add dependencies:
<dependency>
            <groupId>org.zeroturnaround</groupId>
            <artifactId>jr-sdk</artifactId>
            <version>2020.1.1</version>
        </dependency>

         <dependency>
            <groupId>org.zeroturnaround</groupId>
            <artifactId>jr-utils</artifactId>
            <version>2020.1.1</version>
            <scope>provided</scope>
        </dependency>
  • implement a listener which should be hardreferenced from some class instance:
listener = new ClassEventListenerAdapter(0) {
            @Override
            public void onClassEvent(int eventType, Class<?> klass)
                    throws Exception {
            }
        };
        ReloaderFactory.getInstance()
                .addClassReloadListener(WeakUtil.weak(listener));

I've checked that this code allows to get notifications via onClassEvent method impl in the simplest project (at least).

Now here are my thoughts:

  • It's not really convenient to add a ClassEventListener programatically : you need a dedicated place which should add this listener. And this place is an alien in your project: it has no relation to the business logic.
  • It would be much better to have an annotation in JRebel which marks a class as a listener and automatically finds it and register it as a listener if it's in the project classpath. In that way the class could have been totally separated from any logic related to the project. But I was not able to find such feature (I'm not sure whether it exists but at least in the pointe dependencies there are no such annotation).
  • As a result we have to have a class which is loaded with the application and never GCed to be able to get the notifications. We can't use just a standalone class for that (e.g. just a singleton) : it has to be referenced somehow from the applicaiton. Standalone class will not be ever loaded since it's not used by anyone.

What I suggest:

  • make a separate module for this JRebel integration. It will be possible to avoid this module for production.
  • To be able to register a ClassEventListener we will need some servlet listener . E.g. a ServletContextListener (like an existing ServletDeployer e.g.) or ServletContainerInitializer (like existing DevModeInitializer e.g.). This listener should be properly declared so that it's invoked by the servlet container at the proper time. The listener then will register a ClassEventListener and store it somehow so that it's not GCed.
  • Then it's just about ClassEventListener implementation.

@murkaje in Vaadin 15, ApplicationRouteRegistry.getInstance receives an argument of type VaadinContext instead of ServletContext as is in V14-, thus when running with V15, although single class recompile trigger the onClassEvent, below exception is logged. V14 apps don't trigger any exception, ofcourse.

2020-03-13 10:53:23 JRebel: ERROR Class 'com.vaadin.flow.server.startup.RouteRegistryInitializer' could not be processed by org.zeroturnaround.jrebel.vaadin.cbp.RouteRegistryInitializerCBP@org.eclipse.jetty.webapp.WebAppClassLoader@2be2e0ac: org.zeroturnaround.bundled.javassist.CannotCompileException: [source error] getInstance(javax.servlet.ServletContext) not found in com.vaadin.flow.server.startup.ApplicationRouteRegistry
    at org.zeroturnaround.bundled.javassist.CtNewMethod.make(SourceFile:84)
    at org.zeroturnaround.bundled.javassist.CtNewMethod.make(SourceFile:50)
    at org.zeroturnaround.bundled.javassist.CtMethod.make(SourceFile:140)
    at org.zeroturnaround.jrebel.vaadin.cbp.RouteRegistryInitializerCBP.process(RouteRegistryInitializerCBP.java:23)
    at org.zeroturnaround.javarebel.integration.support.JavassistClassBytecodeProcessor.process(SourceFile:111)
    at org.zeroturnaround.javarebel.integration.support.CacheAwareJavassistClassBytecodeProcessor.process(SourceFile:34)
    at org.zeroturnaround.javarebel.integration.support.JavassistClassBytecodeProcessor.process(SourceFile:75)
    at com.zeroturnaround.javarebel.zl.a(SourceFile:377)
    at com.zeroturnaround.javarebel.zl.a(SourceFile:304)
    at com.zeroturnaround.javarebel.SDKIntegrationImpl.runBytecodeProcessors(SourceFile:43)
    at com.zeroturnaround.javarebel.wp.transform(SourceFile:134)
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:42009)
    at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:174)
    at java.base/java.net.URLClassLoader.defineClass(URLClassLoader.java:545)
    at java.base/java.net.URLClassLoader.access$100(URLClassLoader.java:83)
    at java.base/java.net.URLClassLoader$1.run(URLClassLoader.java:453)
    at java.base/java.net.URLClassLoader$1.run(URLClassLoader.java:447)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:446)
    at org.eclipse.jetty.webapp.WebAppClassLoader.foundClass(WebAppClassLoader.java:670)
    at org.eclipse.jetty.webapp.WebAppClassLoader.loadAsResource(WebAppClassLoader.java:639)
    at org.eclipse.jetty.webapp.WebAppClassLoader.loadClass(WebAppClassLoader.java:545)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:496)
    at java.base/java.lang.Class.forName0(Native Method)
    at java.base/java.lang.Class.forName(Class.java:375)
    at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.nextProviderClass(ServiceLoader.java:1204)
    at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.hasNextService(ServiceLoader.java:1215)
    at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.hasNext(ServiceLoader.java:1259)
    at java.base/java.util.ServiceLoader$2.hasNext(ServiceLoader.java:1294)
    at java.base/java.util.ServiceLoader$3.hasNext(ServiceLoader.java:1379)
    at org.eclipse.jetty.annotations.AnnotationConfiguration.getNonExcludedInitializers(AnnotationConfiguration.java:838)
    at org.eclipse.jetty.annotations.AnnotationConfiguration.configure(AnnotationConfiguration.java:348)
    at org.eclipse.jetty.webapp.WebAppContext.configure(WebAppContext.java:517)
    at org.eclipse.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1454)
    at org.eclipse.jetty.server.handler.ContextHandler.doStart(ContextHandler.java:854)
    at org.eclipse.jetty.servlet.ServletContextHandler.doStart(ServletContextHandler.java:278)
    at org.eclipse.jetty.webapp.WebAppContext.doStart(WebAppContext.java:545)
    at org.eclipse.jetty.maven.plugin.JettyWebAppContext.doStart(JettyWebAppContext.java:428)
    at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:68)
    at org.eclipse.jetty.maven.plugin.JettyRunMojo.restartWebApp(JettyRunMojo.java:526)
    at org.eclipse.jetty.maven.plugin.JettyRunMojo$1.onPathWatchEvents(JettyRunMojo.java:378)
    at org.eclipse.jetty.util.PathWatcher.notifyEvents(PathWatcher.java:1366)
    at org.eclipse.jetty.util.PathWatcher.run(PathWatcher.java:1190)
    at java.base/java.lang.Thread.run(Thread.java:844)
Caused by: compile error: getInstance(javax.servlet.ServletContext) not found in com.vaadin.flow.server.startup.ApplicationRouteRegistry
    at org.zeroturnaround.bundled.javassist.compiler.TypeChecker.atMethodCallCore(SourceFile:777)
    at org.zeroturnaround.bundled.javassist.compiler.TypeChecker.atCallExpr(SourceFile:723)
    at org.zeroturnaround.bundled.javassist.compiler.JvstTypeChecker.atCallExpr(SourceFile:170)
    at org.zeroturnaround.bundled.javassist.compiler.ast.CallExpr.accept(SourceFile:49)
    at org.zeroturnaround.bundled.javassist.compiler.CodeGen.doTypeCheck(SourceFile:266)
    at org.zeroturnaround.bundled.javassist.compiler.CodeGen.atDeclarator(SourceFile:773)
    at org.zeroturnaround.bundled.javassist.compiler.ast.Declarator.accept(SourceFile:103)
    at org.zeroturnaround.bundled.javassist.compiler.CodeGen.atStmnt(SourceFile:383)
    at org.zeroturnaround.bundled.javassist.compiler.ast.Stmnt.accept(SourceFile:53)
    at org.zeroturnaround.bundled.javassist.compiler.CodeGen.atStmnt(SourceFile:383)
    at org.zeroturnaround.bundled.javassist.compiler.ast.Stmnt.accept(SourceFile:53)
    at org.zeroturnaround.bundled.javassist.compiler.CodeGen.atMethodBody(SourceFile:321)
    at org.zeroturnaround.bundled.javassist.compiler.CodeGen.atMethodDecl(SourceFile:303)
    at org.zeroturnaround.bundled.javassist.compiler.ast.MethodDecl.accept(SourceFile:47)
    at org.zeroturnaround.bundled.javassist.compiler.Javac.compileMethod(SourceFile:175)
    at org.zeroturnaround.bundled.javassist.compiler.Javac.compile(SourceFile:102)
    at org.zeroturnaround.bundled.javassist.CtNewMethod.make(SourceFile:79)
    ... 43 more
Was this page helpful?
0 / 5 - 0 ratings