Java Hot Class Reloading with RelProxy in Development Mode, a GWT Example
Join the DZone community and get the full member experience.
Join For Freerelproxy 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/`.
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(">", ">"); } }
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":
public string greetserver(string input) throws illegalargumentexception { return new greetingserviceprocessor(this).greetserver(input); }
you can find the complete source code of this example here:
https://github.com/jmarranz/relproxy_examples/tree/master/relproxy_ex_gwt
enjoy!!
Opinions expressed by DZone contributors are their own.
Comments