Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Java Hot Class Reloading with RelProxy in Development Mode, a GWT Example

DZone's Guide to

Java Hot Class Reloading with RelProxy in Development Mode, a GWT Example

· Java Zone
Free Resource

Try Okta to add social login, MFA, and OpenID Connect support to your Java app in minutes. Create a free developer account today and never build auth again.

RelProxy  was presented in a previous  article here at Java DZone, with RelProxy you can change in runtime a subset of your Java source code with no limits to get automatically reloaded with a minimal footprint and avoiding redeploys and the tedious context reloading. 

Reloadable source code can be located in a standard place for RelProxy to help you only in development time or below WEB-INF/ folder to be uploaded to production like any other dynamic language (php, ruby, phyton...).

In this tutorial we are going to see how RelProxy can help you to improve your productivity in this case only in development time, to show this feature we will create a basic GWT-RPC project in Eclipse and will be modified to introduce RelProxy and get automatic class reloading.

Steps:

  • Download and install Eclipse (v4.4 Luna is supposed).
  • Download and install Google Plugin for Eclipse (only GWT stuff, no App Engine or Android is needed).
  • Select in Eclipse the menu option New / Other…​ / Google/ Web Application Project to create a GWT-RPC sample project (do not click in Google App Engine, is not needed). There is no need of disable context reloading, is already disabled in the internal Jetty used by GWT.
  • Download RelProxy distribution file and copy the `relproxy-x.y.z.jar` to `/war/WEB-INF/lib/`.
In our example we have created the project with name relproxy_ex_gwt and package com.innowhere.relproxyexgwt, this is the structure of the generated source code:
relproxy_ex_gwt    (root folder of project)
    src
        com
            innowhere
                relproxyexgwt
                    client
                        GreetingService.java
                        GreetingServiceAsync.java
                        Relproxy_ex_gwt.java
                    server
                        GreetingServiceImpl.java
                    shared
                        FieldVerifier.java
                    Relproxy_ex_gwt.gwt.xml

In RelProxy there are "two worlds", the reloadable and the non-reloadable world.

In GWT-RPC, only the classes executed in server can be reloadable, that is classes under the "server" folder (there is only one class, GreetingServiceImpl.java). The classes under "client" folder cannot be hot reloaded by RelProxy because this Java code is executed in client (previously compiled to JavaScript), the same is applied to "shared", shared classes can be used in both sides, client and server, modifying and reloading a shared class only has effect in the server side and is ignored in client side, so is not recommended.

The only reloadable class is GreetingServiceImpl.java and will be our focus.

This is the generated code by GWT plugin:

package com.innowhere.relproxyexgwt.server;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.innowhere.relproxyexgwt.client.GreetingService;
import com.innowhere.relproxyexgwt.shared.FieldVerifier;


/**
 * The server side implementation of the RPC service.
 */
@SuppressWarnings("serial")
public class GreetingServiceImpl extends RemoteServiceServlet implements GreetingService {

    public String greetServer(String input) throws IllegalArgumentException {
        // Verify that the input is valid.
        if (!FieldVerifier.isValidName(input)) {
            // If the input is not valid, throw an IllegalArgumentException back to
            // the client.
            throw new IllegalArgumentException("Name must be at least 4 characters long");
        }

        String serverInfo = getServletContext().getServerInfo();
        String userAgent = getThreadLocalRequest().getHeader("User-Agent");

        // Escape data from the client to avoid cross-site script vulnerabilities.
        input = escapeHtml(input);
        userAgent = escapeHtml(userAgent);

        return "Hello, " + input + "!<br><br>I am running " + serverInfo + ".<br><br>It looks like you are using:<br>" + userAgent;
    }

    /**
     * Escape an html string. Escaping data received from the client helps to
     * prevent cross-site script vulnerabilities.
     *
     * @param html the html string to escape
     * @return the escaped string
     */
    private String escapeHtml(String html) {
        if (html == null) {
            return null;
        }
        return html.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
    }
}

This class is a servlet created to receive RPC requests from client following the interface pattern of the interface GreetingService shared by client and server code. We are not going to try to reload this servlet because this class has a lifecycle controlled by the servlet container. We need a reloadable singleton implementing an interface registered on JProxy (the key class of RelProxy for Java class reloading), therefore we are deeply transforming GreetingServiceImpl:

package com.innowhere.relproxyexgwt.server;

import java.io.File;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaFileObject;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.innowhere.relproxy.RelProxyOnReloadListener;
import com.innowhere.relproxy.jproxy.JProxy;
import com.innowhere.relproxy.jproxy.JProxyConfig;
import com.innowhere.relproxy.jproxy.JProxyDiagnosticsListener;
import com.innowhere.relproxy.jproxy.JProxyInputSourceFileExcludedListener;
import com.innowhere.relproxy.jproxy.JProxyCompilerListener;
import com.innowhere.relproxyexgwt.client.GreetingService;

/**
 * The server-side implementation of the RPC service.
 */
@SuppressWarnings("serial")
public class GreetingServiceImpl extends RemoteServiceServlet implements
		GreetingService {

    protected GreetingServiceDelegate delegate;

    public void init(ServletConfig config) throws ServletException {

        super.init(config);

        ServletContext context = config.getServletContext();

        String inputPath = context.getRealPath("/") + "/../src/";

        JProxyInputSourceFileExcludedListener excludedListener = 
           new JProxyInputSourceFileExcludedListener()
        {
            @Override
            public boolean isExcluded(File file, File rootFolder) {
                String absPath = file.getAbsolutePath();
                if (file.isDirectory())
                {
                    return absPath.endsWith(File.separatorChar + "client") ||
                           absPath.endsWith(File.separatorChar + "shared");
                }
                else
                {
                    return absPath.endsWith(GreetingServiceDelegate.class.getSimpleName() + ".java") ||
                           absPath.endsWith(GreetingServiceImpl.class.getSimpleName() + ".java");
                }
            }
        };

        String classFolder = null; // Optional: context.getRealPath("/") + "/WEB-INF/classes";
        Iterable<String> compilationOptions = 
              Arrays.asList(new String[]{"-source","1.6","-target","1.6"});
        long scanPeriod = 300;

        RelProxyOnReloadListener proxyListener = 
           new RelProxyOnReloadListener() {
            public void onReload(Object objOld, Object objNew, Object proxy, Method method, Object[] args) {
                System.out.println("Reloaded " + objNew + " Calling method: " + method);
            }
        };

        JProxyCompilerListener compilerListener = 
            new JProxyCompilerListener(){
            @Override
            public void beforeCompile(File file)
            {
                System.out.println("Before compile: " + file);
            }

            @Override
            public void afterCompile(File file)
            {
                System.out.println("After compile: " + file);
            }
        };

        JProxyDiagnosticsListener diagnosticsListener = 
          new JProxyDiagnosticsListener()
        {
            public void onDiagnostics(DiagnosticCollector<javax.tools.JavaFileObject> diagnostics)
            {
                List<Diagnostic<? extends JavaFileObject>> diagList = diagnostics.getDiagnostics();
                int i = 1;
                for (Diagnostic<? extends JavaFileObject> diagnostic : diagList)
                {
                   System.err.println("Diagnostic " + i);
                   System.err.println("  code: " + diagnostic.getCode());
                   System.err.println("  kind: " + diagnostic.getKind());
                   System.err.println("  line number: " + diagnostic.getLineNumber());
                   System.err.println("  column number: " + diagnostic.getColumnNumber());
                   System.err.println("  start position: " + diagnostic.getStartPosition());
                   System.err.println("  position: " + diagnostic.getPosition());
                   System.err.println("  end position: " + diagnostic.getEndPosition());
                   System.err.println("  source: " + diagnostic.getSource());
                   System.err.println("  message: " + diagnostic.getMessage(null));
                   i++;
                }
            }
        };

        JProxyConfig jpConfig = JProxy.createJProxyConfig();
        jpConfig.setEnabled(true)
                .setRelProxyOnReloadListener(proxyListener)
                .setInputPath(inputPath)
                .setJProxyInputSourceFileExcludedListener(excludedListener)
                .setScanPeriod(scanPeriod)
                .setClassFolder(classFolder)
                .setCompilationOptions(compilationOptions)
                .setJProxyCompilerListener(compilerListener)
                .setJProxyDiagnosticsListener(diagnosticsListener);

        JProxy.init(jpConfig);

        this.delegate = JProxy.create(new GreetingServiceDelegateImpl(this),
                              GreetingServiceDelegate.class);

    }   // init

    public String greetServer(String input) throws IllegalArgumentException
    {
            try
            {
                    return delegate.greetServer(input);
            }
            catch(IllegalArgumentException ex)
            {
                    ex.printStackTrace();
                    throw ex;
            }
            catch(Exception ex)
            {
                    ex.printStackTrace();
                    throw new RuntimeException(ex);
            }
    }

    public HttpServletRequest getThreadLocalRequestPublic()
    {
            return getThreadLocalRequest();
    }
}

Let’s review this JProxy-ready class. GreetingServiceImpl in practice is a singleton  because is a servlet, therefore this attribute:

protected GreetingServiceDelegate delegate;

which hold the reloadable singleton registered on:

this.delegate = JProxy.create(new GreetingServiceDelegateImpl(this), 
      GreetingServiceDelegate.class);

As you can see we have created the Java file GreetingServiceDelegateImpl.java the class to hold the singleton going to be reloaded and the bridge/door to the reloadable world implementing the interface GreetingServiceDelegate. JProxy returns a proxy object implementing GreetingServiceDelegate interface exposed to the non-reloadable world.

Take a look to this listener:

        JProxyInputSourceFileExcludedListener excludedListener = 
            new JProxyInputSourceFileExcludedListener()
        {
            @Override
            public boolean isExcluded(File file, File rootFolder) {
                String absPath = file.getAbsolutePath();
                if (file.isDirectory())
                {
                    return absPath.endsWith(File.separatorChar + "client") ||
                           absPath.endsWith(File.separatorChar + "shared");
                }
                else
                {
                    return absPath.endsWith(GreetingServiceDelegate.class.getSimpleName() + ".java") ||
                           absPath.endsWith(GreetingServiceImpl.class.getSimpleName() + ".java");
                }
            }
        };

Registered on:

.setJProxyInputSourceFileExcludedListener(excludedListener)

This listener filters the Java source files that must be ignored by RelProxy/JProxy even when modified.

Because JProxy creates a new ClassLoader and reloads on it all hot-reloadable classes when someone is modified, classes inside client/ and shared/ folders must not be reloadable because has no sense in GWT.

When a folder inside a declared source folder of reloadable classes specified in configuration is going to be inspected for changed classes, the method isExcluded is called to check whether the complete folder must be excluded, this is very useful for big projects with a lot of not reloadable files. In this case classes inside client/ or shared/ are excluded. If a folder is not excluded, files and folders in this folder are asked for excluding calling isExcluded, the class GreetingServiceImpl cannot be reloaded because it is a servlet and cannot be registered in JProxy because is already in use by the JavaEE servlet system. Finally GreetingServiceDelegate.java cannot be reloaded because is the interface exposed to the non-reloadable world. 

In summary only server/ classes can be reloaded excluding the servlet class GreetingServiceImpl.java and GreetingServiceDelegate.java.

This is the code of GreetingServiceDelegate :

package com.innowhere.relproxyexgwt.server;

public interface GreetingServiceDelegate {

    public String greetServer(String input) throws IllegalArgumentException;

}

And the code of GreetingServiceDelegateImpl.java, basically a copy/paste of the original servlet code:

package com.innowhere.relproxyexgwt.server;

import com.innowhere.relproxyexgwt.shared.FieldVerifier;

public class GreetingServiceDelegateImpl implements GreetingServiceDelegate
{
    protected GreetingServiceImpl parent;

    public GreetingServiceDelegateImpl() // needed by JProxy
    {
    }

    public GreetingServiceDelegateImpl(GreetingServiceImpl parent)
    {
        this.parent = parent;
    }

    public String greetServer(String input) throws IllegalArgumentException {

        // Verify that the input is valid.
        if (!FieldVerifier.isValidName(input)) {
          // If the input is not valid, throw an IllegalArgumentException back to
          // the client.
           throw new IllegalArgumentException("Name must be at least 4 characters long");
        }

        String serverInfo = parent.getServletContext().getServerInfo();
        String userAgent = parent.getThreadLocalRequestPublic().getHeader("User-Agent");

        // Escape data from the client to avoid cross-site script vulnerabilities.
        input = escapeHtml(input);
        userAgent = escapeHtml(userAgent);

        return "Hello, " + input + "!<br><br>I am running " + serverInfo
                        + ".<br><br>It looks like you are using:<br>" + userAgent;
    }

    /**
     * Escape an html string. Escaping data received from the client helps to
     * prevent cross-site script vulnerabilities.
     *
     * @param html the html string to escape
     * @return the escaped string
     */
    private String escapeHtml(String html) {
            if (html == null) {
                    return null;
            }
            return html.replaceAll("&", "&").replaceAll("<", "<")
                            .replaceAll(">", ">");
    }
}
Run this example (Run As/Web Application GWT Super Dev Mode), open this URL  http://127.0.0.1:8888/Relproxy_ex_gwt.html  in your browser and a screen like this is shown:


Click on Send to Server:


Click on the Close button.

Now we are going to modify on the fly the Java code of GreetingServiceDelegateImpl, just change "Hello" by "Hello <b>BROTHER</b>" and save:

        return "Hello <b>BROTHER</b>, " + input + "!<br><br>I am running " + serverInfo + ".<br><br>It looks like you are using:<br>" + userAgent;

Back to browser, click again on "Send to Server":


As you can see in this case no page reload has been necessary because the requisite is to call the proxied method to reload classes, this call was made by a AJAX/RPC call.
In this example we made a very simple method change, adding more methods is not a problem but most of the time you will need to add new fields related to new classes, because GreetingServiceDelegateImpl is a singleton we cannot add, remove or change names and types of the fields of this class, to overcome this severe limitation create new classes avoiding the singleton pattern and move the code to them. Code something like this:
	public String greetServer(String input) 
          throws IllegalArgumentException {
    		return new GreetingServiceProcessor(this).greetServer(input);
	}
Declared fields of GreetingServiceProcessor can change with no problem because this class can be reloaded and is instantiated by any call to GreetingServiceDelegateImpl.greetServer() with fresh data.

You can find the complete source code of this example here:

https://github.com/jmarranz/relproxy_examples/tree/master/relproxy_ex_gwt

Enjoy!!

Build and launch faster with Okta’s user management API. Register today for the free forever developer edition!

Topics:

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}