© Jose María Arranz Santamaría v0.8.8 March 16, 2016
Because Java is also a dynamic language and Groovy can still be a dynamic language in a Java based environment.
What is RelProxy
RelProxy is:
-
A simple Java and Groovy hot class reloader for Java and Groovy providing transparent compilation and class reload on the fly with no need of a bootstrap class loader agent and avoiding context reloading. Reloading happens only in memory. Class reloading can be used in development phase and optionally in production (if source code can be uploaded to production).
-
A scripting environment to execute Java code snippets the same as a shell script. There is no new language, is Java compiled on the fly, code in the initial archive can call to other normal Java files. Optionally .class can be saved in a cache to provide the fastest "scripting" language of the world.
-
Execution of Java code snippets in command line (no need of packaging into an archive).
-
A simple shell to code, edit and execute code snippets in Java interactively.
-
JSR 223 Scripting API implementation for "Java" as the target scripting language. You can embed and execute Java code as scripting into your Java program.
The origins of RelProxy
RelProxy was born as the result of the intent of providing a way to hot class reloading for the ItsNat Java web framework in development and optionally in production time.
ItsNat templates are pure X/HTML/SVG files with no logic executed in server, when a template is changed ItsNat automatically reloads the file and the new version is used in the next attempt of using the template. View logic is coded in Java by using Java W3C DOM APIs modifying ItsNat templates and finally generating the required markup in load time or JavaScript code as the result of AJAX events.
As said before, ItsNat provided hot reloading of markup templates, but view logic, coded in Java, remained too static, if you needed to change the view logic you needed to redeploy the web application, in the best case only the web application context is reloaded. Providing some hot class reloading of view logic (coded in Java or Groovy as we will see later) with no need of context reload (context reload can be quick or very slow depending of the size of the application and session can be lost) would dramatically increase the productivity of ItsNat in development and in an edge case, view logic changes would be possible in production.
Redeploying is not strange to Java web developers when you leave the world of JSPs, JSPs were designed to be hot reloaded including logic coded in Java, server centric frameworks usually provides some kind of hot reloaded templating, but in most of them conventional Java code is not hot reloaded unless context reload or redeploy.
The first attempt to provide hot reload of view logic to ItsNat was trying to use Groovy, many people think Groovy is an "interpreted" language and is not, Groovy is also a language compiled to bytecode, but compilation happens on demand, generated bytecode is usually more flexible ("dynamic") than conventional Java and hot class reloading is built-in. The problem of using Groovy in ItsNat was the need of registering Groovy listeners in ItsNat Java listeners (view processors), when registering on an ItsNat (Java) listener the object registered is "frozen" and cannot be reloaded…
This was the context to invent RelProxy and is the reason of the name, Reload + Proxy, the first feature of RelProxy was creating Java proxies (java.lang.reflect.Proxy
) to wrap Groovy objects before registering
into Java methods (ItsNat listeners), RelProxy proxies ever use the last version of the Groovy class by using the hot class loader of Groovy, by this way we can hot class reload classes in ItsNat… coded in Groovy.
The next step is obvious, Groovy class reloader is also a Groovy "compiler on demand", if we are able to make a "compiler on demand" for Java we can get THE SAME AS Groovy provides… in Java. As you know, as of Java 1.6 we have a Java API to access the compiler of your JDK.
If we are able to compile and load on demand Java source files, we can be more ambitious and be able to execute Java programs with no need of a previous compilation, just put into a file Java code like a UNIX shell script in a similar way Groovy does, this feature provides maybe the fastest "scripting" language of the world.
Why are you going to need Java as a scripting/shell language?
Because:
-
Java is faaaast
-
Java is robust: because there is compilation on the fly, syntax errors are detected with no need of executing the offending code.
-
Java has an enormous amount of libraries
-
class files can be optionally automatically saved to avoid compilation in the next execution
-
Java 8 is around the corner providing a less verbose language
If you can execute uncompiled Java code, can we get something similar to an interactive shell to execute Java code snippets? Yes of course.
Do not confuse Java scripting provided by RelProxy with BeanShell, BeanShell is NOT Java, is a scripting language heavily inspired on Java < 1.5, running on the JVM with a nice integration with real Java, for instance in BeanShell there is no support of Java 1.5 features like generics.
Finally, RelProxy is in some way similar to the famous JRebel, but is not so transparent and has more limitations. The only advantage is the price, zero, and maybe lesser memory footprint and better performance.
That is all, welcome to dynamic Java, welcome to a more dynamic Groovy in a Java environment.
RelProxy: a fast alternative to context reloading for Java (and Groovy)
Most of Java application servers (servlet containers) have the context reloading feature. Context reloading is a great feature of Java servlet/EE application servers,
it reloads the web application usually when a .class is changed in WEB-INF/classes
. This feature is usually used in development with the help of your favourite IDE,
when you change a Java source file the IDE automatically compiles the file and save the .class to WEB-INF/classes, this change triggers the context reloading loading the new
changed class.
Context reloading is enough for small web applications in development phase but it becomes a productivity problem when the applications becomes big because any small change triggers the context reloading, the result is the context reloading being executed most of time making your system slow and producing soon a memory overflow (PermGen exception). Another caveat because the session is lost you must log again etc.
RelProxy-Java uses a different approach, it just recompile in memory the modified class but there is no class reload (which requires a new ClassLoader). When a exposed method of a registered singleton in JProxy (a utility of RelProxy for Java) is called, the class reloading happens. By this way you can make big changes without consuming much memory and CPU, and when changes are done, just with a simple page reload the registered singleton will be called producing the class reload.
RelProxy does not want to be a "full solution" for the problem of automatic class loading, with RelProxy you decide what code can be reloaded, this also reduce the impact of the tool because your are just going to reload just a subset of the source code of your web application (web applications are the target of the class reloading feature of RelProxy but other type of Java applications, for instance desktop, could be used.
RelProxy can be used in development only and/or in production, in the case of production, source code going to be reloaded is included into the war file (recommended of course
under WEB-INF/
), you can modify the Java source file directly in production and automatically changes are detected, recompiled in memory and reloaded when needed, no .class
change is needed, by this way you get the similar experience when you change the source code in production of a PHP, Ruby, JSP etc files without the need of restarting
the application. If you do not want use RelProxy in production just disable it, the performance penalty is zero.
RelProxy also can be used in development only accessing directly to your source files in /src
folder (multiple source folders are allowed) with no need of uploading source code to
production.
RelProxy is an alternative to context reloading, if your application is small and you feel comfortable with context reloading you do not need the class reloading features of RelProxy (RelProxy also offers Java scripting), otherwise you must disable context reloading when RelProxy is enabled.
Requisites and installation
RelProxy requires JDK 1.6 or upper, RelProxy have been tested on Oracle JDK 1.6, 1.7 and OpenJDK 1.7.
Just uncompress the RelProxy distribution file.
The distribution file has two important files:
-
relproxy-X.Y.Z.jar
: needed in classpath to use RelProxy in any form. -
jproxysh
: needed whether command line scripting capabilities in Java are going to be used.
The distribution file includes some example scripts into the folder cmd_examples
to test the shell capabilities or RelProxy Java, executing the script
sh fixesforunix.sh
is recommended to define the appropriated executable permissions, then define the environment variable JAVA_HOME
with the location of your 1.6+ JDK installation.
Add RelProxy jar as Maven or Gradle dependency
As of v0.8.3 RelProxy is registered in Bintray/JCenter and Maven Central repositories.
Add JCenter repository to your POM or Maven settings if you want to import from JCenter, in Gradle just add jcenter()
to repositories
.
Examples for version 0.8.8:
Maven POM:
<dependency>
<groupId>com.innowhere</groupId>
<artifactId>relproxy</artifactId>
<version>0.8.8</version>
</dependency>
To add JCenter to your POM:
<repositories>
<repository>
<id>jcenter</id>
<url>http://jcenter.bintray.com</url>
</repository>
</repositories>
Gradle build:
dependencies {
compile 'com.innowhere:relproxy:0.8.8'
}
To add JCenter:
repositories {
jcenter()
}
Disabling automatic context reload
RelProxy is an alternative to context reloading, use of both has no sense and makes RelProxy useless, therefore we must disable context reloading.
In Eclipse and Tomcat
In this manual Eclipse 4.4 (Luna) has been used, behaviour of previous and future versions may be the same but not tested.
In Eclipse the Tomcat associated can have a configuration controlled only by Eclipse (the default mode), this configuration is only valid inside the Eclipse environment and the original configuration
of Tomcat is untouched. By default Eclipse is configured to "Automatically publish when resources changed" for your concrete associated Tomcat, this option is required, to review this option go to
menu Window / Show View / Servers
, this menu option opens a view listing your servers, double click on the concrete Tomcat to show a configuration panel (if no server is associated
to your Eclipse install a Tomcat back to Eclipse Servers view click the right button and select New / Server
to associate the Tomcat to Eclipse).
In the configuration panel click on Publishing
drop-down and review whether is correct.
Now we are going to disable automatic context reload in a per web application/module basis (we can keep enabled in global configuration in Server Options
).
Click on Modules
tab.
Disable the Auto Reload
feature selecting the required module and clicking Edit…
In NetBeans and Tomcat
In this manual NetBeans 8.0.2 has been used, behaviour of previous and future versions may be the same but not tested.
The author of this manual has not been able to disable context reloading feature of Tomcat in NetBeans environment. The xml archive with the <Context>
descriptor in Tomcat installation is
replaced with the content of META-INF/contex.xml
, in theory just adding reloadable=true
to <Context>
in this file would make the job… no success, is ignored.
We are able of disabling context reload avoiding the automatic synchronization of sources and deployed artifacts, two flags are involved in Project Properties
dialog:
Build / Compile / Compile on Save
Run / Deploy on Save
Just disabling Deploy on Save
makes the job of avoiding .class changes and therefore context reloading.
This is valid for Maven web projects and Ant based projects generated by NetBeans’s wizards.
The price is the lost of automatic synchronization when single source files are changed in runtime. We will explain later how we can workaround this problem.
Configuring an Eclipse web application publishing Java and/or Groovy source code on the final target war
Dynamic Web Project
We are talking about a web application created by New / Dynamic Web Project
(or New / Project… / Web / Dynamic Web Project
) in Eclipse with Java source code
going to be published in production usually in a folder under WEB-INF/
(for obvious privacy reasons).
Because this folder is also a source code folder, you must add it to the project configuration Properties / Java Build Path / Source / Add Folder
.
This only affects to Java source folders, in case of using RelProxy-Groovy (GProxy) there is no need of configuring in Eclipse the folder with Groovy code (Groovy built-in compiler compiles Groovy files with no need of Eclipse).
Unfortunately Eclipse avoids publishing Java files and they are automatically filtered (not the case of .groovy files) in the web application internally deployed, there is no Eclipse configuration to avoid this filtering.
Installing and configuring the Eclipse Filesync Plugin resolves our problem.
Configure Filesync in project Properties
.
In this example the folder relproxy_ex_itsnat/WebContent/WEB-INF/javaex/code
is a source code folder root containing .java files.
The tricky part is the Default target folder
. This folder is the root of the real deployed web application, the final deployed web application is created under your workspace
folder, you can
get the path of this folder executing in your servlet init method:
public void init(ServletConfig config) throws ServletException
{
super.init(config);
ServletContext context = getServletContext();
String realPath = context.getRealPath("/");
String inputPath = realPath + "/WEB-INF/javaex/code/";
The variable inputPath
contains the path to be configured as Default target folder
in this example.
Sometimes the Filesync Plugin seems to fail, in this case you can need to force synchronization:
An alternative is manual synchronization of files under /WEB-INF
executing a custom Ant script like this:
<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="sync_production_src_folders" name="relproxy_ex_itsnat">
<property file="conf/conf_relproxy.properties"/> <!-- defines ${webapp_target} where Eclipse builds the webapp -->
<target name="sync_production_src_folders">
<echo message="Synchronizing (alternative to Filesync plugin) to: ${webapp_target}" />
<property name="webinf_src" value="${basedir}/WebContent/WEB-INF" />
<property name="webinf_target" value="${webapp_target}/WEB-INF" />
<property name="source_java" value="${webinf_src}/javaex/code" />
<property name="source_groovy" value="${webinf_src}/groovyex/code" />
<property name="target_java" value="${webinf_target}/javaex/code" />
<property name="target_groovy" value="${webinf_target}/groovyex/code" />
<sync todir="${target_java}" includeEmptyDirs="true">
<fileset dir="${source_java}"/>
</sync>
<sync todir="${target_groovy}" includeEmptyDirs="true">
<fileset dir="${source_groovy}"/>
</sync>
</target>
</project>
After any modification of source code execute the synchronization task to be copied to the build directory created by Eclipse under workspace
folder explained before.
Maven based Web Project
We can create a web project based on a Maven POM using the Maven archetype maven-archetype-webapp
selecting New / Other / Maven / Maven Project
, set in Filter
the value maven-archetype-webapp
.
In project Properties / Java Build Path / Libraries / Add Library
add the server for instance Apache Tomcat v7
.
The header of web.xml generated by Maven is a bit old (based on DOCTYPE
), replace it with something like this (servlet 2.5, 3.0 is also valid for Tomcat v7):
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
...
</web-app>
Follow the same steps described for an Eclipse Dynamic Web Project, yes in spite of most of things are defined in Maven we must repeat the same on Eclipse Project Properties
dialogs,
(for instance register the extra source code folders).
Because we are going to publish source code files (located in some place under WEB-INF/
) to the final war we need to explain to Maven where are located these extra folders
to be also included in compilation and copied to the final war, this configuration is done adding the extra source code folders as <directory> resources.
At the time of writing ItsNat is not in Maven Central repository, you must manually include it in your dependencies as a system
dependency, and copy the jar to the WEB-INF/lib
folder.
The following POM is a simple example of a ItsNat web application using RelProxy (v0.8.8) and including a published source code folder, src/main/webapp/WEB-INF/code
, able to
contain reloadable source code (remember to add ItsNat-1.3.1.jar
to the WEB-INF/lib
folder):
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.innowhere</groupId>
<artifactId>relproxy_ex_itsnat_maven</artifactId>
<packaging>war</packaging>
<version>0.1-SNAPSHOT</version>
<name>relproxy_ex_itsnat_maven Maven Webapp</name>
<url>https://github.com/jmarranz/relproxy/</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<!-- http://stackoverflow.com/tags/jstl/info http://stackoverflow.com/questions/2276083/include-jstl-dependency-with-maven -->
<!--
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
-->
<dependency>
<groupId>com.innowhere</groupId>
<artifactId>relproxy</artifactId>
<version>0.8.8</version>
</dependency>
<dependency>
<groupId>ItsNat</groupId>
<artifactId>ItsNat-jar</artifactId>
<version>1.3.1</version>
<scope>system</scope>
<systemPath>${basedir}/src/main/webapp/WEB-INF/lib/ItsNat-1.3.1.jar</systemPath>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-dom</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-xml</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-util</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.12</version>
</dependency>
<dependency>
<groupId>xalan</groupId>
<artifactId>serializer</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.1.6</version>
</dependency>
</dependencies>
<build>
<finalName>relproxy_ex_itsnat_maven</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/webapp/WEB-INF/groovyex/code</directory>
</resource>
<resource>
<directory>src/main/webapp/WEB-INF/javaex/code</directory>
</resource>
</resources>
</build>
</project>
In Eclipse there is no need of using org.codehaus.mojo:build-helper-maven-plugin plug-in (Maven support is "especial" in Eclipse), folders specified in <resources> are recognized as source folders.
If you have several folders with source code (RelProxy supports multiple hot reloadable root folders), add more <resource> elements, for instance:
<resources>
<resource>
<directory>src/main/webapp/WEB-INF/groovyex/code</directory>
</resource>
<resource>
<directory>src/main/webapp/WEB-INF/javaex/code</directory>
</resource>
<resource>
<directory>src/main/webapp/WEB-INF/javaex/code2</directory>
</resource>
</resources>
Because we need to synchronize source code into /WEB-INF
Filesync or this Ant file will help us:
<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="sync_production_src_folders" name="relproxy_ex_itsnat_maven">
<property file="conf/conf_relproxy.properties"/> <!-- defines ${webapp_target} where Eclipse builds the webapp -->
<target name="sync_production_src_folders">
<echo message="Synchronizing (alternative to Filesync plugin) to: ${webapp_target}" />
<property name="webinf_src" value="${basedir}/src/main/webapp/WEB-INF" />
<property name="webinf_target" value="${webapp_target}/WEB-INF" />
<property name="source_java" value="${webinf_src}/javaex/code" />
<property name="source_groovy" value="${webinf_src}/groovyex/code" />
<property name="target_java" value="${webinf_target}/javaex/code" />
<property name="target_groovy" value="${webinf_target}/groovyex/code" />
<sync todir="${target_java}" includeEmptyDirs="true">
<fileset dir="${source_java}"/>
</sync>
<sync todir="${target_groovy}" includeEmptyDirs="true">
<fileset dir="${source_groovy}"/>
</sync>
</target>
</project>
Configuring a NetBeans web application publishing Java and/or Groovy source code on the final target war
Remember you must avoid context reloading disabling first Run / Deploy on Save
in Project Properties
dialog.
Web Project based on Ant created by NetBeans
A conventional (not Maven) web project is created in NetBeans selecting the menu File / New Project / Java Web / Web Application
.
By default he generated Ant file filters .java files under WEB-INF/
when deploying, to avoid this filtering just add to the build.xml:
<target name="-pre-dist">
<copy todir="${build.web.dir}/WEB-INF" preservelastmodified="true">
<fileset dir="${webinf.dir}" />
</copy>
</target>
Fortunately in this type of Ant based project, if the option Project Properties / Build / Compile / Compile on Save
is enabled, NetBeans takes (by using Ant) care of automatic
synchronization of resources of /web/WEB-INF
and the same folder under /build
, this includes .java files because we have avoided this filtering. There is no need
of explicit synchronization by using Ant tasks.
Web Project based on Maven
A Maven web project is created in NetBeans selecting the menu File / New Project / Maven / Web Application
.
Take a look again to the chapter about Eclipse Maven based web applications, the Maven POM structure is the same for NetBeans, remember you must specify extra source code folders
under WEB-INF/
adding them as `<resource>`s.
Because the default structure of Maven, on development time when deploying a web application, Maven deploys under the target/projectname
folder
the final web application. Changed source files under src/webapp
in runtime are not detected by RelProxy because the real files being used
are really below target/projectname
unless NetBeans automatically synchronizes files between both file trees. Effectively NetBeans automatically copies the modified file
to the target/projectname
, but unfortunately excluding .java (and .groovy) files.
In theory enabling Project Properties / Run / Deploy on Save
does the job but enables automatic context reloading.
One simple solution is adding a special Ant task to synchronize the source files to the same files in target/projectname
. Call this task after hot source code modification.
For instance:
<project basedir="." default="sync_production_src_folders" name="relproxy_ex_itsnat_maven_netbeans">
<target name="sync_production_src_folders">
<property name="webinf_src" value="${basedir}/src/main/webapp/WEB-INF" />
<property name="webinf_target" value="${basedir}/target/relproxy_ex_itsnat_maven_netbeans-1.0-SNAPSHOT/WEB-INF" />
<echo message="Synchronizing (alternative to Filesync plugin) to: ${webinf_target}" />
<property name="source_java" value="${webinf_src}/javaex/code" />
<property name="source_groovy" value="${webinf_src}/groovyex/code" />
<property name="target_java" value="${webinf_target}/javaex/code" />
<property name="target_groovy" value="${webinf_target}/groovyex/code" />
<sync todir="${target_java}" includeEmptyDirs="true">
<fileset dir="${source_java}"/>
</sync>
<sync todir="${target_groovy}" includeEmptyDirs="true">
<fileset dir="${source_groovy}"/>
</sync>
</target>
</project>
Configuring a NetBeans or Eclipse web application NOT publishing Java and/or Groovy source code on the final target war
As you have seen adding source code files under WEB-INF/
is very problematic, we have needed some tricks and workarounds to synchronize these source folders to the final deployed
web application.
If you are not going to publish source code to production and you just need hot reload source code under your /src/main/java
folder in Maven or src/java
in NetBeans or /src
in Eclipse, that is only in development phase, you do not need synchronization tricks because you tell RelProxy to directly access to the original source being modified for you
and RelProxy recompiles and loads your changes in memory.
Both types of web applications, publishing source code to war and direct access to normal source code, are going to be shown you using concrete examples.
GProxy or how to be able to reload Groovy classes on the Java environment
com.innowhere.relproxy.gproxy.GProxy
is the main Java class of RelProxy to provide this feature, with GProxy
you can create Java proxies for Groovy objects because
a java.lang.reflect.Proxy
wrapper is passed instead of the original Groovy object, the original Groovy object is retained under the hood and method calls to the proxy
are redirected to the real object calling the corresponding method using reflection. When the source code of the Groovy class changes GProxy automatically reloads the Groovy
class and creates a new object to replace the old one, the fields of the original object are got and re-set to the new object to keep the state (number of fields and types
must be the same otherwise reloading is not possible and a redeploy is required).
The following code is an example of how to use RelProxy along with ItsNat web framework, this code is included in the examples of RelProxy (relproxy_ex_itsnat
or relproxy_ex_itsnat_maven
).
The servlet
variable is a servlet object containing a just configured groovy.util.GroovyScriptEngine
, the setting up of this utility object is omitted:
package example.groovyex;
import org.itsnat.core.http.ItsNatHttpServlet;
import org.itsnat.core.tmpl.ItsNatDocumentTemplate;
import org.itsnat.core.event.ItsNatServletRequestListener;
import groovy.util.GroovyScriptEngine;
import java.lang.reflect.Method;
import com.innowhere.relproxy.RelProxyOnReloadListener;
import com.innowhere.relproxy.gproxy.GProxy;
import com.innowhere.relproxy.gproxy.GProxyGroovyScriptEngine;
import com.innowhere.relproxy.gproxy.GProxyConfig;
GroovyScriptEngine groovyEngine = servlet.getGroovyScriptEngine();
def gproxyGroovyEngine = {
String scriptName -> return (java.lang.Class)groovyEngine.loadScriptByName(scriptName)
} as GProxyGroovyScriptEngine;
def reloadListener = {
Object objOld,Object objNew,Object proxy, Method method, Object[] args ->
println("Reloaded " + objNew + " Calling method: " + method)
} as RelProxyOnReloadListener;
def gpConfig = GProxy.createGProxyConfig();
gpConfig.setEnabled(true)
.setRelProxyOnReloadListener(reloadListener)
.setGProxyGroovyScriptEngine(gproxyGroovyEngine);
GProxy.init(gpConfig);
String pathPrefix = context.getRealPath("/") + "/WEB-INF/groovyex/pages/";
def docTemplate;
docTemplate = itsNatServlet.registerItsNatDocumentTemplate("groovyex","text/html", pathPrefix + "groovyex.html");
def db = new FalseDB();
ItsNatServletRequestListener listener = GProxy.create(new example.groovyex.GroovyExampleLoadListener(db), ItsNatServletRequestListener.class);
docTemplate.addItsNatServletRequestListener(listener);
Let’s explain the previous code:
def gproxyGroovyEngine = {
String scriptName -> return (java.lang.Class)groovyEngine.loadScriptByName(scriptName)
} as GProxyGroovyScriptEngine;
Defines a listener needed by GProxy
to indirectly call the groovy.util.GroovyScriptEngine
to load classes, take a look to the signature of GProxyGroovyScriptEngine
there is no dependency with groovy.*
packages, this is why you can use RelProxy in pure Java projects with no Groovy dependency in spite of Groovy support.
def reloadListener = {
Object objOld,Object objNew,Object proxy, Method method, Object[] args ->
println("Reloaded " + objNew + " Calling method: " + method)
} as RelProxyOnReloadListener;
Defines an optional RelProxyOnReloadListener
listener to be called when Groovy classes have been reloaded because some change has happened in the source code managed by RelProxy.
An object implementing this interface may be registered on RelProxy to listen when a method of the proxy object has been called (this example only includes one method exposed by the interface, but nothing prevents of adding more methods to the interface/implementation) and the class of the original object associated has been reloaded (a new "original" object based on the new class was created to replace it).
When you perform a source code change in source code managed by RelProxy the first time this method is called is the signal that changes has been detected and reloaded accordingly.
This interface and behavior is not GProxy
specific and will be also used in JProxy
for Java.
def gpConfig = GProxy.createGProxyConfig();
gpConfig.setEnabled(true)
.setRelProxyOnReloadListener(reloadListener)
.setGProxyGroovyScriptEngine(gproxyGroovyEngine);
GProxy.init(gpConfig);
Configures GProxy
, now it is ready to proxy Groovy objects.
Take a look to the optional setEnabled(true)
configuration call, GProxy
is enabled by default, this means proxied Groovy objects are instrumented for hot reload.
Calling setEnabled(false)
tells GProxy
to ignore any other configuration, GProxy
is disabled and no proxy is created calling GProxy.create
, the original Groovy
objects will be returned with absolutely no performance penalty, this is the preferred configuration in production whether you do not want hot class reload in production.
The final code:
def db = new FalseDB();
ItsNatServletRequestListener listener = GProxy.create(new example.groovyex.GroovyExampleLoadListener(db), ItsNatServletRequestListener.class);
docTemplate.addItsNatServletRequestListener(listener);
is an example of proxying a example.groovyex.GroovyExampleLoadListener
object and registering the returned Java proxy into the ItsNat infrastructure.
The class example.groovyex.GroovyExampleLoadListener
implements the ItsNat standard interface ItsNatServletRequestListener
implementing the method
processRequest(ItsNatServletRequest request, ItsNatServletResponse response)
this method is called by ItsNat when a page rendered by the template is loaded, the proxy object receives this call and forwards this call
to the latest class loaded, we are going to see more details later.
Let’s go to take a look to example.groovyex.GroovyExampleLoadListener
:
package example.groovyex;
import org.itsnat.core.event.ItsNatServletRequestListener;
import org.itsnat.core.ItsNatServletRequest;
import org.itsnat.core.ItsNatServletResponse;
import example.groovyex.FalseDB;
class GroovyExampleLoadListener implements ItsNatServletRequestListener
{
def db
GroovyExampleLoadListener()
{
}
GroovyExampleLoadListener(FalseDB db) // Explicit type tells Groovy to reload FalseDB class when changed
{
this.db = db;
}
void processRequest(ItsNatServletRequest request, ItsNatServletResponse response)
{
println("GroovyExampleLoadListener 4 ");
new example.groovyex.GroovyExampleDocument(request.getItsNatDocument(),db);
}
}
To understand this code let’s to explain how ItsNat works, the method processRequest
is called every time a page is loaded specifying the same ItsNat template,
because this listener was registered as its load processor.
When RelProxy (through groovy.util.GroovyScriptEngine
) detects the source code of the class GroovyExampleLoadListener
or dependent classes like GroovyExampleDocument
have changed, all classes with associated hot reloadable source, are reloaded and a new ClassLoader
is created for them, next calls to GroovyExampleLoadListener
proxy
will use the new loaded class and the same with dependent classes.
However a concrete GroovyExampleLoadListener
object was used to register, how can we reload a class with one live object already created?
The GroovyExampleLoadListener
object was the one proxied, the class of this object is reloaded when a source change is detected (or any related class) because this is the
objective of RelProxy, but this object can have fields pointing to objects usually loaded before registering/proxying the GroovyExampleLoadListener
object. The classes
of these attribute objects may be also reloaded but the new version is not effective because referenced objects are usually being used in other places, if we re-create these
objects we are creating new instances for instance of objects designed to be singletons. This is the case of the db
attribute of
class FalseDB
, this attribute references a concrete FalseDB
object not able to be automatically reloaded in spite of the Groovy FalseDB
class could be reloaded.
This is why in case of the proxied object GroovyExampleLoadListener
, RelProxy recreates the object based on the new loaded class by calling the default constructor and
re-setting the attributes, by this way the new object is based on the new class containing the same attribute objects defined before, you cannot add, remove or change
the type of attributes, if you do so RelProxy will not be able to hot reload and a new redeploy is needed.
The proxied class usually creates new objects based on dependent classes to execute some task, if no object of these dependent classes is "saved" and/or used outside of proxied environment RelProxy can reload dependent classes with no problem.
This is the case of the class GroovyExampleDocument
and dependent classes (see the source code).
Other classes and interfaces like ItsNatServletRequest
or ItsNatServletResponse
are not reloaded in this example because they are ItsNat based and source code is not
present in Groovy environment. FalseDB
class could be reloaded but reloading will fail because the proxied object (GroovyExampleLoadListener
) holds an attribute db
of this
class, RelProxy will say you the reloading process has been failed and a redeploy is recommended to effectively use the new version of the class.
In summary, in this ItsNat example, when source code of GroovyExampleLoadListener
or dependent classes with source code controlled by RelProxy change, all of these classes are recompiled
and reloaded by Groovy when changed. When the processRequest
method of the proxied GroovyExampleLoadListener
object is called because an end user is reloading the related web page, GProxy
detects
the singleton has been reloaded and recreates the GroovyExampleLoadListener
object with the new Class re-setting the fields and finally
the processRequest
method is called and method processing is done using the new version of dependent classes.
Finally we have been able to reload Groovy classes mixed in a Java environment.
JProxy or how to be able to reload Java classes without using Java agents
Java hot reloadable proxies are very similar to Groovy support of RelProxy, in this case the task of detecting source changes, recompiling and reloading is fully done by RelProxy
(in case of Groovy provided groovy.util.GroovyScriptEngine
does most of this work).
com.innowhere.relproxy.jproxy.JProxy
is the main Java class of RelProxy for hot reload of pure Java, with JProxy
you can create Java java.lang.reflect.Proxy
proxies wrapping
your original objects to be passed to listeners, the original object is retained under the hood and method calls to the proxy are redirected to the real object calling the
corresponding method using reflection. When the source code of a monitored Java file is changed, it is automatically recompiled in memory. When the processRequest
method of
the proxied JProxyExampleLoadListener
object is called because an end user is reloading the related web page, JProxy
detects something has changed and reload all monitored
classes with a new ClassLoader
, because the singleton class has been reloaded JProxy recreates the JProxyExampleLoadListener
object with the new Class re-setting the fields
to keep the state (number of fields and types must be the same otherwise reloading is not possible and a redeploy is required) and finally the processRequest
method is called and method processing
is done using the new version of dependent classes.
As you can see reloading only happens when hot reloadable classes are going to be used, only recompiling is done when some file is changed, this is a performance and memory improvement over the typical "context reloading per file save".
The following code is an example of how to use JProxy
along with ItsNat web framework, this code is part of the RelProxy examples (relproxy_ex_itsnat
or relproxy_ex_itsnat_maven
)
basically doing the same as the Groovy example:
package example.javaex;
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.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaFileObject;
import org.itsnat.core.event.ItsNatServletRequestListener;
import org.itsnat.core.http.HttpServletWrapper;
import org.itsnat.core.tmpl.ItsNatDocumentTemplate;
import com.innowhere.relproxy.RelProxyOnReloadListener;
import com.innowhere.relproxy.jproxy.JProxy;
import com.innowhere.relproxy.jproxy.JProxyCompilerListener;
import com.innowhere.relproxy.jproxy.JProxyConfig;
import com.innowhere.relproxy.jproxy.JProxyDiagnosticsListener;
import com.innowhere.relproxy.jproxy.JProxyInputSourceFileExcludedListener;
/**
*
* @author jmarranz
*/
public class JProxyExampleServlet extends HttpServletWrapper
{
public JProxyExampleServlet()
{
}
@Override
public void init(ServletConfig config) throws ServletException
{
super.init(config);
ServletContext context = getServletContext();
String realPath = context.getRealPath("/");
String inputPath = realPath + "/WEB-INF/javaex/code/";
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() {
@Override
public void onReload(Object objOld, Object objNew, Object proxy, Method method, Object[] args) {
System.out.println("Reloaded " + objNew + " Calling method: " + method);
}
};
JProxyInputSourceFileExcludedListener excludedListener = new JProxyInputSourceFileExcludedListener()
{
@Override
public boolean isExcluded(File file, File rootFolderOfSources)
{
return false;
}
};
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()
{
@Override
public void onDiagnostics(DiagnosticCollector<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);
String pathPrefix = context.getRealPath("/") + "/WEB-INF/javaex/pages/";
ItsNatDocumentTemplate docTemplate;
docTemplate = itsNatServlet.registerItsNatDocumentTemplate("javaex","text/html", pathPrefix + "javaex.html");
FalseDB db = new FalseDB();
ItsNatServletRequestListener listener = JProxy.create(new example.javaex.JProxyExampleLoadListener(db), ItsNatServletRequestListener.class);
docTemplate.addItsNatServletRequestListener(listener);
}
}
There is more code than Groovy code because GroovyScriptEngine
setting up was omitted (not specific of RelProxy) and now some configuration options are shown in spite of they
may be optional.
Let’s explain the previous code:
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 is an example of JProxy configuration.
-
setEnabled(boolean)
configuration method is the same asGProxy
, when setting to false other configuration options are ignored, there is no hot reload and proxying and performance penalty is zero. -
setRelProxyOnReloadListener(proxyListener)
is the same asGProxy
in fact the same interfaceRelProxyOnReloadListener
is shared betweenGProxy
andJProxy
. -
setInputPath(inputPath)
defines where the source code files of hot reloadable classes is. The variant methodsetInputPaths(String[])
allows registering several root folders andsetJProxyInputSourceFileExcludedListener(JProxyInputSourceFileExcludedListener)
allows excluding concrete files. -
setJProxyInputSourceFileExcludedListener(excludedListener)
optionally defines whether the specified folder or file is excluded in the recompiling detection. In this example nothing is excluded. -
setScanPeriod(scanPeriod)
defines the period (in ms) between checks of timestamps of source code files to detect changes. -
setClassFolder(classFolder)
optionally defines where to save, as .class files, the bytecode resulting of re-compiling modified source files in runtime. By this way the next time the application is started .class files are aligned with source files and no runtime compilation is needed (class folder of course must be in classpath). -
setCompilationOptions(compilationOptions)
optionally sets the list of options you want for compiling phase, these are the same kind of options you would provide to the javac command, internally the Java compiler API receives this parameters and the reason of the required format. -
setJProxyCompilerListener(compilerListener)
optionally registers a listener to be called when a file is going to be compiled. -
setJProxyDiagnosticsListener(diagnosticsListener)
optionally registers theJProxyDiagnosticsListener
listener to be executed when some warning or error happens compiling Java code, when providing null or not called RelProxy uses a default listener very similar to this example.
The final code:
FalseDB db = new FalseDB();
ItsNatServletRequestListener listener = JProxy.create(new example.javaex.JProxyExampleLoadListener(db), ItsNatServletRequestListener.class);
docTemplate.addItsNatServletRequestListener(listener);
Is symmetric to Groovy counterpart, it is the same example and the same expected behavior of RelProxy but all in Java.
Anyway this is the code of JProxyExampleLoadListener
:
package example.javaex;
import org.itsnat.core.event.ItsNatServletRequestListener;
import org.itsnat.core.ItsNatServletRequest;
import org.itsnat.core.ItsNatServletResponse;
import org.itsnat.core.html.ItsNatHTMLDocument;
public class JProxyExampleLoadListener implements ItsNatServletRequestListener
{
protected FalseDB db;
public JProxyExampleLoadListener()
{
}
public JProxyExampleLoadListener(FalseDB db)
{
this.db = db;
}
public void processRequest(ItsNatServletRequest request, ItsNatServletResponse response)
{
System.out.println("JProxyExampleLoadListener 4 " + this.getClass().getClassLoader().hashCode());
new example.javaex.JProxyExampleDocument(request,(ItsNatHTMLDocument)request.getItsNatDocument(),db);
}
}
As of version 0.8.6 RelProxy also supports hot reload of inner classes including anonymous inner classes. An example of ItsNat code:
EventListener listener = new EventListener()
{
@Override
public void handleEvent(Event evt)
{
...
}
};
Element buttonElem = doc.getElementById("buttonId");
((EventTarget)buttonElem).addEventListener("click", JProxy.create(listener, EventListener.class) ,false);
Obviously the container (enclosing) class of the inner class must be reloadable.
Use of JProxy with multiple servlets
In the previous JProxy example we have supposed one single servlet requiring class reloading and context reloading disabled.
If you have more servlets or you are a purist developer, you can use a ServletContextListener
:
...
public class JProxyServletContextListener implements ServletContextListener
{
@Override
public void contextInitialized(ServletContextEvent sce)
{
System.out.println("ServletContextListener contextInitialized");
ServletContext context = sce.getServletContext();
...
JProxy.init(jpConfig);
}
@Override
public void contextDestroyed(ServletContextEvent sce)
{
System.out.println("ServletContextListener contextDestroyed");
JProxy.stop();
}
}
Registered on your web.xml
:
<web-app ...
<listener>
<listener-class>
example.javaex.JProxyServletContextListener
</listener-class>
</listener>
</web-app>
The stop()
call is used to stop the timer checker of source code changes, useful to avoid memory leaks when the context is reloaded and avoid warnings when the
servlet container is stopping.
Finally in your servlet classes only register your singletons:
@Override
public void init(ServletConfig config) throws ServletException
{
super.init(config);
ServletContext context = config.getServletContext();
String pathPrefix = context.getRealPath("/") + "/WEB-INF/javaex/pages/";
ItsNatDocumentTemplate docTemplate;
docTemplate = itsNatServlet.registerItsNatDocumentTemplate("javaex","text/html", pathPrefix + "javaex.html");
FalseDB db = new FalseDB();
ItsNatServletRequestListener listener = JProxy.create(new example.javaex.JProxyExampleLoadListener(db), ItsNatServletRequestListener.class);
docTemplate.addItsNatServletRequestListener(listener);
}
Starting and stopping class change detection and reloading in JProxy
You can reduce to zero the footprint of RelProxy in production setting setEnabled(boolean)
to false, however if you are a brave guy or girl and you want to make also hot changes
in production… JProxy.start()
and JProxy.stop()
methods are for you.
Remember we must to define the period between source files checking for changes calling setScanPeriod(scanPeriod)
, RelProxy defines behind the scene a java.util.Timer
for this
task, of course every time source code is checked for changes needs some time and CPU use, because there are synchronizations between source code checking and proxies use, a very small performance penalty
happens when checking source code. This is why we can stop
source code checking if we are not able to make source code changes anymore reducing performance penalty to minimum, and we can
call start
again to detect any source change.
You can call several times to JProxy.start()
and JProxy.stop()
methods, if nothing is going to be done nothing is done without errors (both methods return true
when a state change was effective), and they are thread safe.
Call ever to stop()
in your ServletContextListener
in the contextDestroyed(ServletContextEvent)
method.
Using JProxy only in development time with no need of publishing code (a GWT example)
In previous chapters we have added new source code folders below WEB-INF/
folder, this configuration is very useful in production to be able to hot change
your Java code, of course in production time you can remove these folders avoiding publishing source code before packaging to war
and with a simple call setEnabled(false)
disable JProxy with zero performance penalty, this makes JProxy helpful in development only but as you can easily figure out, adding source code under WEB-INF/
folder
is not a good idea if you are not going to use this code in production.
With JProxy is not necessary to put the source code going to be reloaded under WEB-INF/
, you can modify Java source code and reload it located in conventional source code folders.
Because JProxy is going to directly access to original source code, the folder synchronizing problems are gone in this use case.
We are going to illustrate this capability with a GWT RPC example using Eclipse. Besides how to use JProxy in a GWT-RPC project, in this chapter we are going to learn how we can exclude source files from the hot class reload system of RelProxy/JProxy because we are going to need this feature.
This example is for development phase only, nothing prevents of appliying both strategies because JProxy allows multiple source folders to monitor by using JProxyConfig.setInputPaths(String[])
.
In GWT, JProxy only can be used to reload Java code executed in server, this is why we are going to apply JProxy to a GWT-RPC project (that is a client-server web application).
Install Eclipse (Eclipse 4.4 Luna was used for this example), install the Google Plugin for Eclipse (version 4.4 was used), only install GWT dependencies if you want (there is no need of Android and Google App Engine parts).
Download RelProxy distribution file and copy the relproxy-x.y.z.jar
to /war/WEB-INF/lib/
.
Select in Eclipse the menu option New / Other… / Google/ Web Application Project
to create a GWT-RPC sample project (Google App Engine is not needed).
There is no need of disabling context reloading, it seems is already disabled in the default configuration of GWT.
In this 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
We are only be able to reload classes executed in server, that is, classes below server/
folder. This why the class GreetingServiceImpl.java
is our focus,
this is the generated code:
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 to use JProxy we need a reloadable singleton implementing an interface registered in JProxy, 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/";
if (!new File(inputPath).exists())
{
System.out.println("RelProxy is disabled, detected production mode ");
return;
}
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 = 200;
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
is a singleton in practice 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, implementing
the interface GreetingServiceDelegate
. JProxy returns a proxy object "implementing" GreetingServiceDelegate
exposed to the non-reloadable world.
Take a look to this listener, in the previous example it was trivial, in this case is very important:
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, child files and folders into 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
.
In this example there is only specified a root source code folder, RelProxy allows several root source code folder JProxyConfig.setInputPahts(String[])
instead of JProxyConfig.setInputPaht(String)
,
the parameter File rootFolder
specify the root source code folder containing the folder or file to apply excluding rules, this parameter can help you to define very complex
excluding scenarios with several root source code folders. The web application relproxy_test_itsnat
used to test RelProxy contains a complex excluding scenario with three root source folders.
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.
Solving jar manifest configuration problems in JProxy
When developing a Liferay 6.2 example using RelProxy a weird problem was manifested, RelProxy (JProxy) internally makes a call like this:
Enumeration<URL> res = classLoader.getResources("javax/portlet");
This call is expected to return a list of URL-JAR pointing to .class
files with package javax.portlet
, in Liferay 6.2 the jar containing these classes is portlet.jar
. In spite
of this jar is defined in the classpath of the ClassLoader
, nothing is returned. To fix this problem we just need to decompress this jar and add Name: javax/portlet
to the file
META-INF/MANIFEST.MF
and package again.
As you have realized, this solution is "dirty" and problematic with "jar downloaders" like any build tool downloading Maven artifacts. This is why JProxy provides a workaround allowing direct specification of the problematic jars (use absolute paths), when necessary RelProxy uses the "brute force" to locate the .class of the required package "manually" inspecting the provided jars. JProxy adds a new configuration method to specify these jars:
JProxyConfig.setRequiredExtraJarPaths(String[] inputJarPaths)
This problem has been found in a jar in Liferay, but nothing prevents similar problematic jars in other tools. To see this problem fixed in Liferay take a look to the JProxyServletContextListener.java
file of Liferay Example included in RelProxy Examples.
This problem is uncommon, if JProxy throws an error of a class not found and this class is in a jar known by the ClassPath
used to load JProxy,
suspect this problem and try to add explicitly the path of the problematic jar.
Identity of returned proxies
A proxy object implements the specified interfaces (in practice only one is important), you can call to any methods of the interface, this call is forwarded by RelProxy
to the original or reloaded object. Because a proxy object is also an Object
, the methods equals(Object)
and hashCode()
may be also called, these calls
are forwarded to the wrapped object of the proxy.
When calling equals(Object)
method RelProxy detects the special case of passing a proxy parameter, in this case RelProxy obtains the current associated object to the proxy parameter
and passes this object to the equals method. By this way we are able to check the identity of two objects indirectly using two proxy objects. This feature
is very useful in collections when registering a proxy instead of the original object, if another proxy object associated to the same original object is used for instance to remove
from collection, the equals(Object)
method is correctly called passing the original object.
Example:
ItsNatServletRequestListener original = new example.javaex.JProxyExampleLoadListener(db);
ItsNatServletRequestListener proxy = JProxy.create(original, ItsNatServletRequestListener.class);
ItsNatServletRequestListener proxy2 = JProxy.create(original, ItsNatServletRequestListener.class);
System.out.println("EQUALS TEST (true if not reloaded): " + (proxy.equals(proxy2)));
A new shell scripting language named Java
When we think on a shell scripting language we think on sh
or csh
, or maybe on the scripting language of Windows Console (based on the old MSDOS), or maybe you know
your preferred conventional dynamic language usually can be executed like another shell language, for instance Groovy,
Python, Ruby
or JavaScript.
But when you think Java like a new shell scripting language sure you say "it’s impossible".
No, it is possible, RelProxy includes a tool named jproxysh
to make possible executing pure Java code like another shell scripting language.
The principle is simple and is very similar to Groovy scripting, Groovy compiles on the fly Groovy code saving in memory the compiled bytecode, by this way developers think Groovy script is interpreted and is not, the same approach is applied to Java through RelProxy. In the case of RelProxy, bytecode can be optionally saved as .class files to avoid compiling on the fly every time the script is executed. When the JVM is able to load .class files instead of compiling, code execution maybe extremely faster than conventional scripting languages interpreted line by line from sources, this is why the affirmation of Java as the fastest scripting language of the world is accurate.
Because pure Java is used and the standard compiler API, nothing prevents using Java scripting in the less verbose Java 8 (v1.8).
Defining a Java based shell scripting file
Let’s see the first example (some background of UNIX shell is required):
#!/usr/bin/env jproxysh
String msg = args[0] + args[1];
System.out.println(msg);
System.out.println("example_java_shell 1 ");
example.javashellex.JProxyShellExample.exec();
The best way to think this script is like the content of the standard method main
of a class with some invented name in the default package (no package), in fact,
this is how it is managed internally by RelProxy.
We could use /bin/jproxysh
or /usr/local/bin/jproxysh
but we are forced to install RelProxy in a concrete fixed place, by using /usr/bin/env
the command jproxysh
will be located using the current PATH.
Save this file in a folder root of the dependent classes. The dependent class in this example is JProxyShellExample
.
The hierarchy is:
<root_folder> example_java_shell (file) example (folder) javashellex (folder) JProxyShellExample.java (file)
Yes, you are right, mentally add the .java extension to example_java_shell
and you get the typical file hierarchy of a JavaSE program, in fact JProxy is ready to execute a conventional
JavaSE program with no explicit compilation, this will be shown later.
The first requisite is that jproxysh
must be accessible by the environment variable PATH
, anyway executing this script is not direct, it requires some previous configuration:
-
First of all the
JAVA_HOME
environment variable is required. -
The
CLASSPATH
environment variable must locate therelproxy-X.X.jar
file and other folders and jars required by your Java application, conventions are the same than a typical JavaSE program. -
Optionally you may specify
JAVA_OPTS
to provide runtime options for the JVM.
There are other optional environment variables in this case RelProxy specific:
-
JPROXYSH_CACHE_CLASS_FOLDER : defines where to save the .class files resulting of compiling on the fly the scripting code, this folder is automatically added to the class path, so when the script is loaded the second time the .class files are used instead of source code according to the typical source-binary timestamp rules (if source code is more recent the class is ignored and replaced with a new file).
-
JPROXYSH_COMPILATION_OPTIONS : compilations passed to the JDK compiler, the format is the same as the command line
javac
.
The following is an example of shell code (into a script file) to execute the previous example_java_shell
, this example is included in RelProxy distribution:
#!/bin/sh
RELPROXY_JAR=relproxy-0.8.8.jar
PROJECT=`dirname $0`/..
# set PROJECT env as absolute path
TMP_PWD=`pwd`
cd $PROJECT
PROJECT=`pwd`
cd $TMP_PWD
if [ -z "$JAVA_HOME" ]; then
echo Missing JAVA_HOME environment variable, exiting...
exit 1
fi
export PATH=$PATH:$PROJECT/bin
export CLASSPATH=$PROJECT/lib/$RELPROXY_JAR
export JAVA_OPTS="-client -Xmx100m"
# Nothing really required in JAVA_OPTS, just to test
export JPROXYSH_CACHE_CLASS_FOLDER="$PROJECT/tmp/java_shell_test_classes"
export JPROXYSH_COMPILATION_OPTIONS="-source 1.6 -target 1.6"
$PROJECT/cmd_examples/code/example_java_shell "HELLO " "WORLD!"
Because example_java_shell
is a jproxysh
based script, nothing prevents of being executed directly using jproxysh
:
jproxysh $PROJECT/cmd_examples/code/example_java_shell "HELLO " "WORLD!"
Defining a complete Java class
As you have seen in example_java_shell
example, you can access to other Java "scripting" classes from the initial scripting file, this is really interesting when your scripts
become too large and you need state (attributes) more methods and so on, that is, you need more classes.
If you need your scripting code more structured, you have the option of defining a conventional class in the scripting main file.
Take a look to this example also included in RelProxy distribution (slightly modified):
#!/usr/bin/env jproxysh
import example.javashellex.JProxyShellExample;
public class example_java_shell_complete_class
{
public static void main(String[] args)
{
String msg = args[0] + args[1];
System.out.println(msg);
System.out.println("example_java_shell_complete_class 1 ");
JProxyShellExample.exec();
}
}
example_java_shell_complete_class
is a conventional class, you can add methods, attributes and so on, the only limitation is the name of the class, it must be the same as the
container file (in this case the container file has not the .java extension).
You can execute this script by the same ways we executed example_java_shell
, directly or as a parameter of jproxysh
.
Scripting conventional JavaSE source code
The differences between the example_java_shell_complete_class
script and a conventional Java source file are just the extension (missing) and the hash bang to execute jproxysh.
We can remove the hashbang and add the .java
extension to the main scripting file, in this scenario the source code is the same as a conventional JavaSE application.
Instead of compiling with javac
and executing with java
command, you just must execute it with jproxysh
jproxysh $PROJECT/cmd_examples/code/example_normal_class.java "HELLO " "WORLD!"
Executing a Java code snippet
We have seen how much overcomplex can be our scripting files, what if you just need to execute one, or two or three sentences…
You don’t need to create a Java shell scripting file, you can write down your script as a parameter and execute. RelProxy through jproxy
allows executing Java code snippets
on the fly.
The following is a shell script included in RelProxy distribution which executes a simple code snippet (the param -c
indicates you are going to execute inline code):
#!/bin/sh
RELPROXY_JAR=relproxy-0.8.8.jar
PROJECT=`dirname $0`/..
# set PROJECT env as absolute path
TMP_PWD=`pwd`
cd $PROJECT
PROJECT=`pwd`
cd $TMP_PWD
if [ -z "$JAVA_HOME" ]; then
echo Missing JAVA_HOME environment variable, exiting...
exit 1
fi
export PATH=$PATH:$PROJECT/bin
export CLASSPATH=$PROJECT/lib/$RELPROXY_JAR
export JAVA_OPTS="-client -Xmx100m"
# Nothing really required in JAVA_OPTS, just to test
export JPROXYSH_COMPILATION_OPTIONS="-source 1.6 -target 1.6"
jproxysh -c 'System.out.print("This code snippet says: ");' \
'System.out.println("Hello World!!");'
You can execute a single code block (into a string parameter) or several blocks in several lines separated with "\", every block can contain several Java sentences.
Alternatively you can execute a complete class with a standard main
method, RelProxy detects when you are specifying a sentence block or a complete class, in this case because there
is no file holding the code, the class name must be known by default and must be jproxyMainClass
. For instance:
jproxysh -c 'public class _jproxyMainClass_ { ' \
' public static void main(String[] args) { ' \
' System.out.print("This code snippet says: ");' \
' System.out.println("Hello World!!");' \
' }' \
'}'
The interactive Java shell
When you need something more interactive, just like the Groovy shell, RelProxy provides a simple interactive shell.
To launch the interactive shell define the required environment variables like the code snippet example and execute jproxysh
with no parameters:
jproxysh
A message info is shown and a prompt is shown waiting for your commands and or code. Write help
to know the shell options, if the text written is not recognized like a command
it is interpreted as Java code and saved in a buffer to be executed when you want writing the 'exec' command.
The interactive Java shell accepts a block of sentences or a complete class with a standard main
method and name jproxyMainClass
.
How to use RelProxy shell scripting in Windows
RelProxy does not provide a jproxysh
version for Windows because you can easily build a mini-Linux/Unix in your Windows box with MinGW/MSYS.
Install MinGW/MSYS, you must be able to locate the shell launcher msys.bat
in a folder like C:\MinGW\msys\1.0\
(exact location may change according to your installation folder).
Execute msys.bat
and you will get a simple Linux shell environment, in this environment you can execute your typical Linux commands like ls
, ps
, find
etc and of course
launch the previous script files documented in this manual and included in RelProxy distribution.
You can go to your required folder in MSYS with a cd
command like this:
cd "C:\Program Files\MyProgram"
Or using a Unix format:
cd "/c/Program Files/MyProgram"
MSYS console is enough for most of purposes, if you can also install mintty
using the MinGW GUI or command based installer, calling mintty&
in MSYS opens an even more sophisticated
Linux console. Mintty has some problem with some keyboard characters editing Java code in the RelProxy interactive console, back to basic MSYS console when necesssary.
If you need to execute Linux shell scripts (for instance RelProxy based) from Windows without a Linux like interactive console, do something like this in your Windows script or console:
set PATH=C:\MinGW\msys\1.0\bin;%PATH%
sh <path to the shell file>
Where <path to the shell file>
can have Windows or Linux format (e.g. /c/development/relproxy/cmd_examples/ex_java_shell_launcher.sh
).
Java Scripting API implementation
RelProxy implements the official JSR-223 Java Scripting API as found in Java 1.6.
The following Java code shows how to create the Java Scripting factory, get an engine instance and execute some code:
// ...
JProxyConfig jpConfig = JProxy.createJProxyConfig();
jpConfig.setEnabled(true)
.setRelProxyOnReloadListener(proxyListener)
.setInputPath(inputPath)
.setScanPeriod(scanPeriod)
.setClassFolder(classFolder)
.setCompilationOptions(compilationOptions)
.setJProxyDiagnosticsListener(diagnosticsListener);
JProxyScriptEngineFactory factory = JProxyScriptEngineFactory.create();
ScriptEngineManager manager = new ScriptEngineManager();
manager.registerEngineName("Java", factory);
manager.getBindings().put("msg","HELLO GLOBAL WORLD!");
ScriptEngine engine = (JProxyScriptEngine)manager.getEngineByName("Java");
((JProxyScriptEngine)engine).init(jpConfig);
Bindings bindings = engine.createBindings();
bindings.put("msg","HELLO ENGINE SCOPE WORLD!");
StringBuilder code = new StringBuilder();
code.append( " javax.script.Bindings bindings = context.getBindings(javax.script.ScriptContext.ENGINE_SCOPE); \n");
code.append( " String msg = (String)bindings.get(\"msg\"); \n");
code.append( " System.out.println(msg); \n");
code.append( " bindings = context.getBindings(javax.script.ScriptContext.GLOBAL_SCOPE); \n");
code.append( " msg = (String)bindings.get(\"msg\"); \n");
code.append( " System.out.println(msg); \n");
code.append( " example.javashellex.JProxyShellExample.exec(engine); \n");
code.append( " return \"SUCCESS\";");
String result = (String)engine.eval( code.toString() , bindings);
System.out.println("RETURNED: " + result);
((JProxyScriptEngine)engine).stop(); // Necessary if scanPeriod > 0 was defined
As you can see initialization code is the same as in JProxy
examples, returned ScriptEngine
implements JProxyScriptEngine
, this interface defines the same methods you are going to find in JProxy
,
the main difference between JProxy
and JProxyScriptEngine
(implementing ScriptEngine
) is that JProxy
use is based on static methods and JProxyScriptEngine
in practice
is a singleton. In theory you get a new ScriptEngine
instance every time you call manager.getEngineByName("Java")
, just call once, use the returned object as a singleton
and you will get a similar environment to JProxy
plus the capability of executing code snipets, otherwise concurrent conflicting can happen when competing several ScriptEngine
objects (unless configured folders are different).
Inside the eval
method, compilation phase is thread safe but not code execution, you can use several threads to call eval
and execute concurrent lengthy tasks without
execution blocking.
The last line:
((JProxyScriptEngine)engine).stop(); // Necessary if scanPeriod > 0 was defined
The interface JProxyScriptEngine
defines the same methods you are going to find in JProxy
, for instance
the stop()
method, this method is necessary whether you define a scanPeriod
and you want to dispose the ScriptEngine
(otherwise the ScriptEngine
is looking for
source changes forever), you can also register reloadable singletons calling JProxyScriptEngine.create(…)
like in JProxy`.
The scripting code can be the content of a main
method with this signature:
public static Object main(javax.script.ScriptEngine engine,javax.script.ScriptContext context)
Or optionally you can define a complete Java class containing the previous main
method and name jproxyMainClass
, for instance:
public class _jproxyMainClass_ {
public static Object main(javax.script.ScriptEngine engine,javax.script.ScriptContext context) {
javax.script.Bindings bindings = context.getBindings(javax.script.ScriptContext.ENGINE_SCOPE);
// ...
}
}
You can directly call the JProxyScriptEngineFactory.getScriptEngine()
method without registering on a ScriptEngineManager
, in this case avoid calling
ServiceContext.getBindings(javax.script.ScriptContext.GLOBAL_SCOPE)
because the default global scope Bindings
object is not defined. In fact the method
ScriptEngineManager.getEngineByName(String)
calls JProxyScriptEngineFactory.getScriptEngine()
and may return null if the JProxyScriptEngineFactory.getScriptEngine()
method throws an exception for instance when some configuration data is wrong, because there is no log info of this exception you have no way to know what is happening, in
this case directly call JProxyScriptEngineFactory.getScriptEngine()
to know what is happening.
Example:
// ...
JProxyScriptEngineFactory factory = JProxyScriptEngineFactory.create();
ScriptEngine engine = factory.getScriptEngine();
((JProxyScriptEngine)engine).init(jpConfig);
Bindings bindings = engine.createBindings();
bindings.put("msg","HELLO ENGINE SCOPE WORLD!");
StringBuilder code = new StringBuilder();
code.append( " javax.script.Bindings bindings = context.getBindings(javax.script.ScriptContext.ENGINE_SCOPE); \n");
code.append( " String msg = (String)bindings.get(\"msg\"); \n");
code.append( " System.out.println(msg); \n");
code.append( " example.javashellex.JProxyShellExample.exec(engine); \n");
code.append( " return \"SUCCESS\";");
String result = (String)engine.eval( code.toString() , bindings);
System.out.println("RETURNED: " + result);
((JProxyScriptEngine)engine).stop(); // Necessary if scanPeriod > 0 was defined
If you need two or more different configurations, create two or more JProxyScriptEngine
with different configurations.
Embedding RelProxy Java in your Java framework to provide hot reload
RelProxy use requires some RelProxy explicit code to provide hot class reloading. If you are a developer of a Java framework, or an autonomous Java service module in general, you can built-in RelProxy Java in your framework to transparently provide code autoreload to end user code using your framework without explicit use of the RelProxy API in your end user code beyond some required configuration.
There are two kind of APIs for using the Java part of RelProxy:
-
JProxy class and related: mainly static methods
-
Java Scripting API: based on interfaces
The second option is preferred to embed RelProxy Java in your Java framework because it is based on interfaces, no public class of RelProxy is needed to expose
in your API, because bootstrap can be executed into your framework. We are going to use the simplified version of the API using JProxyScriptEngineFactory.create()
.
The JProxyScriptEngine
has been designed to provided the same functionality than JProxy
, that is, the same methods, but in this case using a pure interface.
A simple example is the best way to show how to embed RelProxy, this example, RelProxyBuiltin (project relproxy_builtin_ex), is included in the RelProxy Examples repository. It defines two listeners to be implemented and registered by end user code, one listener is to show the options and the other to execute the corresponding selected action.
This mini framework and use example is developed using NetBeans and Maven.
There are two packages:
-
com.innowhere.relproxy_builtin_ex
: the mini framework. The subpackagecom.innowhere.relproxy_builtin_ex.impl
contains the only non-public class of the framework. -
com.innowhere.relproxy_builtin_ex_main
: a simple use example.
The mini framework (public class and interfaces):
package com.innowhere.relproxy_builtin_ex;
import com.innowhere.relproxy_builtin_ex.impl.RelProxyBuiltinImpl;
public class RelProxyBuiltinRoot
{
private final static RelProxyBuiltinImpl SINGLETON = new RelProxyBuiltinImpl();
public static RelProxyBuiltin get()
{
return SINGLETON;
}
}
package com.innowhere.relproxy_builtin_ex;
import com.innowhere.relproxy.jproxy.JProxyScriptEngine;
import java.io.InputStream;
import java.io.PrintStream;
public interface RelProxyBuiltin
{
public JProxyScriptEngine getJProxyScriptEngine();
public void addOutputListener(OutputListener listener);
public void removeOutputListener(OutputListener listener);
public int getOutputListenerCount();
public void addCommandListener(CommandListener listener);
public void removeCommandListener(CommandListener listener);
public int getCommandListenerCount();
public void runLoop(InputStream in,PrintStream out);
}
package com.innowhere.relproxy_builtin_ex;
import java.io.PrintStream;
public interface OutputListener
{
public void write(PrintStream out);
}
package com.innowhere.relproxy_builtin_ex;
import java.io.PrintStream;
public interface CommandListener
{
public void execute(String command,String input,PrintStream out);
}
Now the implementation details, this class shows how simple is to built-in RelProxy:
package com.innowhere.relproxy_builtin_ex.impl;
import com.innowhere.relproxy.jproxy.JProxyScriptEngine;
import com.innowhere.relproxy.jproxy.JProxyScriptEngineFactory;
import com.innowhere.relproxy_builtin_ex.CommandListener;
import com.innowhere.relproxy_builtin_ex.OutputListener;
import com.innowhere.relproxy_builtin_ex.RelProxyBuiltin;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.LinkedHashSet;
import java.util.Scanner;
public class RelProxyBuiltinImpl implements RelProxyBuiltin
{
protected JProxyScriptEngine jProxyEngine = null;
protected LinkedHashSet<OutputListener> outListeners = new LinkedHashSet<OutputListener>();
protected LinkedHashSet<CommandListener> commandListeners = new LinkedHashSet<CommandListener>();
@Override
public JProxyScriptEngine getJProxyScriptEngine()
{
if (jProxyEngine == null) jProxyEngine = (JProxyScriptEngine)JProxyScriptEngineFactory.create().getScriptEngine();
return jProxyEngine;
}
public JProxyScriptEngine getJProxyScriptEngineIfConfigured()
{
if (jProxyEngine == null || !jProxyEngine.isEnabled())
return null;
return jProxyEngine;
}
@Override
public void addOutputListener(OutputListener listener)
{
JProxyScriptEngine jProxy = getJProxyScriptEngineIfConfigured();
if (jProxy != null)
{
listener = jProxy.create(listener,OutputListener.class);
}
outListeners.add(listener);
}
@Override
public void removeOutputListener(OutputListener listener)
{
JProxyScriptEngine jProxy = getJProxyScriptEngineIfConfigured();
if (jProxy != null)
{
listener = jProxy.create(listener,OutputListener.class);
}
outListeners.remove(listener);
}
@Override
public int getOutputListenerCount()
{
return outListeners.size();
}
@Override
public void addCommandListener(CommandListener listener)
{
JProxyScriptEngine jProxy = getJProxyScriptEngineIfConfigured();
if (jProxy != null)
{
listener = jProxy.create(listener,CommandListener.class);
}
commandListeners.add(listener);
}
@Override
public void removeCommandListener(CommandListener listener)
{
JProxyScriptEngine jProxy = getJProxyScriptEngineIfConfigured();
if (jProxy != null)
{
listener = jProxy.create(listener,CommandListener.class);
}
commandListeners.remove(listener);
}
@Override
public int getCommandListenerCount()
{
return commandListeners.size();
}
@Override
public void runLoop(InputStream in,PrintStream out)
{
Scanner scanner = new Scanner(in);
while(true)
{
out.print("Enter phrase:");
String input = scanner.nextLine();
out.println("Command list:");
for(OutputListener listener : outListeners)
listener.write(out);
out.print("Enter command (or quit):");
String command = scanner.nextLine();
if ("quit".equals(command))
break;
for(CommandListener listener : commandListeners)
listener.execute(command,input,out);
}
}
}
These three methods are enough to explain how to bootstrap RelProxy Java engine and how easy is to instrument listener registering hot reloadable:
@Override
public JProxyScriptEngine getJProxyScriptEngine()
{
if (jProxyEngine == null) jProxyEngine = (JProxyScriptEngine)JProxyScriptEngineFactory.create().getScriptEngine();
return jProxyEngine;
}
public JProxyScriptEngine getJProxyScriptEngineIfConfigured()
{
if (jProxyEngine == null || !jProxyEngine.isEnabled())
return null;
return jProxyEngine;
}
@Override
public void addOutputListener(OutputListener listener)
{
JProxyScriptEngine jProxy = getJProxyScriptEngineIfConfigured();
if (jProxy != null)
{
listener = jProxy.create(listener,OutputListener.class);
}
outListeners.add(listener);
}
The public method RelProxyBuiltin.getJProxyScriptEngine()
must be called in starting time to configure RelProxy. If RelProxy is not configured and enabled there is no performance penalty.
Remember that the proxy object returned by create(…)
method correctly calls the method hashCode()
and equals(Object)
in the internal true listener object for
identity purposes required by the listener collection/registry.
This is the example code of this console based program (names are inspired on JUnit but is not really a JUnit test example):
package com.innowhere.relproxy_builtin_ex_main;
import com.innowhere.relproxy.RelProxyOnReloadListener;
import com.innowhere.relproxy.jproxy.JProxy;
import com.innowhere.relproxy.jproxy.JProxyCompilerListener;
import com.innowhere.relproxy.jproxy.JProxyConfig;
import com.innowhere.relproxy.jproxy.JProxyDiagnosticsListener;
import com.innowhere.relproxy.jproxy.JProxyInputSourceFileExcludedListener;
import com.innowhere.relproxy.jproxy.JProxyScriptEngine;
import com.innowhere.relproxy_builtin_ex.CommandListener;
import com.innowhere.relproxy_builtin_ex.RelProxyBuiltin;
import com.innowhere.relproxy_builtin_ex.RelProxyBuiltinRoot;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaFileObject;
public class Main
{
public static void main(String[] args) throws Exception
{
new Main();
}
public Main()
{
// Note: NetBeans Console window works bad (no input) with Maven Test tasks http://stackoverflow.com/questions/3035351/broken-console-in-maven-project-using-netbeans
// this is why is not a really JUnit test.
setUp();
try
{
mainTest();
}
finally
{
tearDown();
}
System.exit(0);
}
public void setUp()
{
URL res = this.getClass().getResource("/"); // .../target/classes/
// Use example of RelProxy in development time:
String inputPath = res.getFile() + "/../../src/main/java/";
if (new File(inputPath).exists())
{
System.out.println("RelProxy to be enabled, development mode detected");
}
else
{
System.out.println("RelProxy disabled, production mode detected");
return;
}
JProxyInputSourceFileExcludedListener excludedListener = new JProxyInputSourceFileExcludedListener()
{
@Override
public boolean isExcluded(File file, File rootFolderOfSources)
{
String absPath = file.getAbsolutePath();
if (file.isDirectory())
{
return absPath.endsWith(File.separatorChar + "relproxy_builtin_ex");
}
else
{
return absPath.endsWith(File.separatorChar + Main.class.getSimpleName() + ".java");
}
}
};
String classFolder = null; // Optional
Iterable<String> compilationOptions = Arrays.asList(new String[]{"-source","1.6","-target","1.6"});
long scanPeriod = 1000;
RelProxyOnReloadListener proxyListener = new RelProxyOnReloadListener() {
@Override
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()
{
@Override
public void onDiagnostics(DiagnosticCollector<JavaFileObject> diagnostics)
{
List<Diagnostic<? extends JavaFileObject>> diagList = diagnostics.getDiagnostics();
int i = 1;
for (Diagnostic 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++;
}
}
};
RelProxyBuiltin rpbRoot = RelProxyBuiltinRoot.get();
JProxyScriptEngine engine = rpbRoot.getJProxyScriptEngine();
JProxyConfig jpConfig = JProxy.createJProxyConfig();
jpConfig.setEnabled(true)
.setRelProxyOnReloadListener(proxyListener)
.setInputPath(inputPath)
.setJProxyInputSourceFileExcludedListener(excludedListener)
.setScanPeriod(scanPeriod)
.setClassFolder(classFolder)
.setCompilationOptions(compilationOptions)
.setJProxyCompilerListener(compilerListener)
.setJProxyDiagnosticsListener(diagnosticsListener);
engine.init(jpConfig);
System.out.println("RelProxy running");
}
public void tearDown()
{
RelProxyBuiltin rpbRoot = RelProxyBuiltinRoot.get();
JProxyScriptEngine engine = rpbRoot.getJProxyScriptEngine();
engine.stop();
System.out.println("RelProxy stopped");
}
public void mainTest()
{
RelProxyBuiltin rpbRoot = RelProxyBuiltinRoot.get();
TestListener listener = new TestListener();
rpbRoot.addOutputListener(listener);
assertTrue(rpbRoot.getOutputListenerCount() == 1);
rpbRoot.removeOutputListener(listener);
assertTrue(rpbRoot.getOutputListenerCount() == 0);
rpbRoot.addOutputListener(listener);
CommandListener commandListener = listener.getCommandListener();
rpbRoot.addCommandListener(commandListener);
assertTrue(rpbRoot.getCommandListenerCount() == 1);
rpbRoot.removeCommandListener(commandListener);
assertTrue(rpbRoot.getCommandListenerCount() == 0);
rpbRoot.addCommandListener(commandListener);
rpbRoot.runLoop(System.in,System.out);
}
private static void assertTrue(boolean res)
{
if (!res) throw new RuntimeException("Unexpected Error");
}
}
Take a look to this piece of code:
URL res = this.getClass().getResource("/"); // .../target/classes/
// Use example of RelProxy in development time:
String inputPath = res.getFile() + "/../../src/main/java/";
if (new File(inputPath).exists())
{
System.out.println("RelProxy to be enabled, development mode detected");
}
else
{
System.out.println("RelProxy disabled, production mode detected");
return;
}
JProxyInputSourceFileExcludedListener excludedListener = new JProxyInputSourceFileExcludedListener()
{
@Override
public boolean isExcluded(File file, File rootFolderOfSources)
{
String absPath = file.getAbsolutePath();
if (file.isDirectory())
{
return absPath.endsWith(File.separatorChar + "relproxy_builtin_ex");
}
else
{
return absPath.endsWith(File.separatorChar + Main.class.getSimpleName() + ".java");
}
}
};
We get and later register the root folder of the source code of our application, this code "may be" reloadable.
We need to exclude the source code of the framework because is not obviously end user code (not reloadable), and the Main.java
file containing
the test code, this class is not reloadable, only the class TestListener.java
(in the same folder than Main.java) will be (and must be) reloadable.
We could simplify this isExcluded
method just excluding anything different to the file TestListener.java
, but excluding whole folders is recommended to
speed the traversing of the source code file tree because otherwise all not reloadable files, into not reloadable folders, are checked.
Finally the class TestListener.java
containing both listeners, the CommandListener
implemented is forced to be an anonymous inner class just for demonstrative purposes:
package com.innowhere.relproxy_builtin_ex_main;
import com.innowhere.relproxy_builtin_ex.CommandListener;
import com.innowhere.relproxy_builtin_ex.OutputListener;
import java.io.PrintStream;
public class TestListener implements OutputListener
{
@Override
public void write(PrintStream out)
{
out.println("uppercase");
out.println("lowercase");
}
public CommandListener getCommandListener()
{
return new CommandListener()
{
@Override
public void execute(String command,String text,PrintStream out)
{
if ("uppercase".equals(command))
out.println(text.toUpperCase());
else if ("lowercase".equals(command))
out.println(text.toLowerCase());
else
out.println("Unknown command:" + command);
}
};
}
}
Execute the Main class and try the predefined options. To check if RelProxy is working fine try to add a new option "same" without stopping the program:
@Override
public void write(PrintStream out)
{
out.println("uppercase");
out.println("lowercase");
out.println("same"); // NEW
}
public CommandListener getCommandListener()
{
return new CommandListener()
{
@Override
public void execute(String command,String text,PrintStream out)
{
if ("uppercase".equals(command))
out.println(text.toUpperCase());
else if ("lowercase".equals(command))
out.println(text.toLowerCase());
else if ("same".equals(command)) // NEW
out.println(text); // NEW
else
out.println("Unknown command:" + command);
}
};
}
}
The next phrase to process includes now the "same" action with no need of stopping the console application.
Note: use RelProxy 0.8.7 or upper, this release adds an improvement when embedding by this way.