RelProxy v0.8.1 reduce el número de redeploys en GWT-RPC y otros Java web frameworks
RelProxy es un recargador de clases en tiempo de ejecución para Java y Groovy a partir del código fuente, además proporciona la capacidad de scripting a Java, es decir poder ejecutar Java SIN COMPILAR previamente lo cual hace posible ser usado como lenguaje de scripting en shells o embebido como script en aplicaciones con otros lenguajes JVM incluido claro el propio Java.
El foco de RelProxy es Java, aunque también RelProxy ayuda a embeber y recargar dinámicamente Groovy en una aplicación Java.
Recientemente he publicado la versión 0.8.1, en la anterior y primera versión, el enfoque estaba en poder cambiar en producción código fuente sin necesitar recargar la aplicación (normalmente web), esa capacidad obviamente permitía cambiar el código fuente y recargarse en tiempo de desarrollo permitiendo un aumento de productividad, características que han existido de siempre en los lenguajes dinámicos (PHP, Ruby etc). Aunque lo anterior es interesante es insuficiente, en pocos casos nos arriesgamos a cambiar código fuente en caliente, por lo que es interesante el poder al menos ser productivos en tiempo de desarrollo pero sin necesidad de subir código fuente.
En la nueva versión 0.8.1 se ha añadido una novedad que permite poder demarcar que código fuente puede ser recargado automáticamente a partir de los directorios normales en donde ponemos nuestro código fuente en Java, más exactamente RelProxy nos permite excluir aquellos archivos y/o directorios que no queremos (o no podemos) recargar.
JProxy es parecido a JRebel pero en absoluto es tan sofisticado y transparente, ahora bien es totalmente "free".
Antes de leer este artículo es recomendado leer el
artículo previo introductorio de RelProxy publicado aquí en javaHispano.
En este artículo voy a mostrar como añadir recarga automática de clases a un proyecto GWT-RPC en tiempo de desarrollo.
En GWT, JProxy puede ser usado para recargar código Java ejecutado en el servidor, obviamente no en el código Java cliente, es decir el código que va a ser convertido a JavaScript, pues no tiene sentido, por eso nuestro proyecto es un ejemplo GWT-RPC en donde hay comunicación AJAX desde el cliente al servidor.
Instala Eclipse (Eclipse 4.4 Luna fue usado en este ejemplo), instala la versión apropiada del Google Plugin para Eclipse, puedes instalar únicamente las dependencias GWT (no hay necesidad ni de Android ni de Google App Engine en este ejemplo). Copia relproxy-0.8.1.jar bajo WEB-INF/lib y añade este jar como una nueva dependencia en el build-path del proyecto en Eclipse.
Selecciona en Eclipse la opción de menú "New/Other/Google/ Web Application Project" que crea por defecto un proyecto GWT-RPC de ejemplo (Google App Engine no es necesitado).
En este ejemplo creamos el proyecto con:
name: relproxy_ex_gwt
package: com.innowhere.relproxyexgwt
Esta es la estructura del código generado:
relproxy_ex_gwt (carpeta raiz)
src
com
innowhere
relproxyexgwt
client
GreetingService.java
GreetingServiceAsync.java
Relproxy_ex_gwt.java
server
GreetingServiceImpl.java
shared
FieldVerifier.java
Relproxy_ex_gwt.gwt.xml
Sólo vamos a poder recargar clases ejecutadas en el servidor, es decir, clases debajo de la carpeta "server/". Por eso nuestro foco se pondrá en la clase GreetingServiceImpl.java que es el servlet que recibe peticiones del código cliente, este es el código generado inicialmente (el cual cambiaremos a fondo):
GreetingServiceImpl.java
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(">", ">");
}
}
Esta clase es un servlet creado para recibir peticiones RPC (por AJAX desde el cliente) siguiendo elpatrón del interface GreetingService compartido tanto en cliente como en el servidor. Este servlet no lo podemos recargar porque necesitamos exponer de forma pública una interface y tener control del singleton registrado en JProxy, en el caso del servlet no es posible. Por ello llevaremos el código RPC del servlet a una clase nuestra por delegación, registrando un objeto de dicha clase en la inicialización del servlet, dejando GreetingServiceImpl.java de esta forma:
GreetingServiceImpl.java
package com.innowhere.relproxyexgwt.server;
import java.io.File;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaFileObject;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.innowhere.relproxy.RelProxyOnReloadListener;
import com.innowhere.relproxy.jproxy.JProxy;
import com.innowhere.relproxy.jproxy.JProxyConfig;
import com.innowhere.relproxy.jproxy.JProxyDiagnosticsListener;
import com.innowhere.relproxy.jproxy.JProxyInputSourceFileExcludedListener;
import com.innowhere.relproxy.jproxy.JProxyCompilerListener;
import com.innowhere.relproxyexgwt.client.GreetingService;
/**
* The server-side implementation of the RPC service.
*/
@SuppressWarnings("serial")
public class GreetingServiceImpl extends RemoteServiceServlet implements
GreetingService {
protected GreetingServiceDelegate delegate;
public void init(ServletConfig config) throws ServletException {
super.init(config);
ServletContext context = config.getServletContext();
String inputPath = context.getRealPath("/") + "/../src/";
JProxyInputSourceFileExcludedListener excludedListener = new JProxyInputSourceFileExcludedListener()
{
@Override
public boolean isExcluded(File file, File rootFolder) {
String absPath = file.getAbsolutePath();
return absPath.contains(File.separatorChar + "client" + File.separatorChar) ||
absPath.contains(File.separatorChar + "shared" + File.separatorChar) ||
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();
}
}
Revisemos los cambios. GreetingServiceImpl es en la práctica un singleton porque es un servlet, por lo tanto el atributo:
protected GreetingServiceDelegate delegate;
sujetará el recargable singleton registrado en:
this.delegate = JProxy.create(new GreetingServiceDelegateImpl(this),
GreetingServiceDelegate.class);
Como puedes ver, hemos creado la clase GreetingServiceDelegateImpl guardada en el archivo del mismo nombre en el directorio "server" la cual podremos recargar, esta clase implementa la interface GreetingServiceDelegate también guarda en "server". JProxy devuelve un objeto proxy implementando la interface GreetingServiceDelegate este proxy recibirá las llamadas RPC en vez de llamar directamente al objeto GreetingServiceDelegateImpl dando la oportunidad a RelProxy a recompilar y recargar la clase y reinstanciar el singleton así como las demás clases dependendientes que también hayan cambiado.
Echemos un vistazo a este listener:
JProxyInputSourceFileExcludedListener excludedListener =
new JProxyInputSourceFileExcludedListener()
{
@Override
public boolean isExcluded(File file, File rootFolder) {
String absPath = file.getAbsolutePath();
return absPath.contains(File.separatorChar + "client" + File.separatorChar) ||
absPath.contains(File.separatorChar + "shared" + File.separatorChar) ||
absPath.endsWith(GreetingServiceDelegate.class.getSimpleName() + ".java") ||
absPath.endsWith(GreetingServiceImpl.class.getSimpleName() + ".java");
}
};
Registrado en la llamada:
.setJProxyInputSourceFileExcludedListener(excludedListener)
Este listener filtra los archivos Java que deben ser ignorados por RelProxy/JProxy aunque estén en la carpeta de código fuente susceptible de ser recargado que hemos definido en configuración. Hay que tener en cuenta que JProxy cuando detecta un cambio de código fuente crea un nuevo ClassLoader y recarga en él todas las clases recargables.
En este caso las clases dentro de client/ y shared/ no deben ser recargables pues no tiene sentido en GWT, por otra parte GreetingServiceImpl no puede ser recargado por las razones indicadas antes. De esta manera sólamente las clases server/ excepto GreetingServiceImpl podrán ser recargadas.
Este es el código de GreetingServiceDelegate :
package com.innowhere.relproxyexgwt.server;
public interface GreetingServiceDelegate {
public String greetServer(String input) throws IllegalArgumentException;
}
Ejecuta este ejemplo (Run As/Web Application GWT Super Dev Mode) y carga esta URL en tu navegador: http://127.0.0.1:8888/Relproxy_ex_gwt.html
Una pantalla como ésta se mostrará:
Pulsa en "Send to Server":
Pulsa finalmente en "Close".
Ahora vamos a modificar "al vuelo" el código Java de GreetingServiceDelegateImpl, basta cambiar "Hello" por "Hello <b>BROTHER</b>" y salvar:
return "Hello <b>BROTHER</b>, " + input + "!I am running " + serverInfo + ".It looks like you are using:" + userAgent;
De vuelta al navegador, pulsa de nuevo en "Send to Server":
Como puedes ver en este caso no ha sido necesario ni siquiera recargar la página (pues provocaría una recompilación por parte de GWT), puesto que el requisito es llamar al método "proxy" para que la recarga sea efectiva.
En este ejemplo hemos hecho un simple cambio en un método, añadir más métodos no es un problema pero con frecuencia necesitarás añadir, eliminar o cambiar nombres y tipos de los atributos de la clase GreetingServiceDelegateImpl, el problema es que esta clase es el singleton que hemos registrado y JProxy no lo permite. Para superar esta limitación crea una nueva clase evitando el patrón singleton (digamos que usando un estilo más "funcional"), por ejemplo:
GreetingServiceDelegateImpl
...
public String greetServer(String input) throws IllegalArgumentException {
return new GreetingServiceProcessor(this).greetServer(input);
}
...
En GreetingServiceProcessor puedes cambiar atributos sin problema pues esta clase no tiene instancias que necesiten ser recordadas y puede ser recargada e instanciada en cualquier llamada a GreetingServiceDelegateImpl.greetServer().
Este es un ejemplo muy sencillo, en el mundo real necesitarás cambios mucho más complejos, no hay problema mientras se acepten las limitaciones de RelProxy.
Recuerda que hemos configurado una carpeta de código fuente que no existe en producción (por ejemplo en el war generado), por tanto no olvides llamar a JProxyConfig.setEnabled(false) en producción, con JProxy desactivado el impacto en el rendimiento es nulo.
Como RelProxy usa el compilador Java de tu JDK, debería funcionar con las nuevas sintaxis de Java 1.8 sin problema.
¡¡Que lo disfrutes!!