Los class loaders del servidor de aplicaciones JBoss/WildFly habitualmente han dado algún problema en la ejecución de las aplicaciones y la carga de clases. En versiones antiguas como la 4 se podían producir conflictos entre las librerías de las aplicaciones y las librerías instaladas en el servidor ya que en JBoss se buscaba las clases por defecto y primero en el class loader del servidor en vez de en el classloader de la aplicación (war). Ya en las últimas versiones como JBoss 7 y WildFly la forma de cargar las clases es más parecido al modelo habitual que se sigue en las aplicaciones Java EE y en servidores como Tomcat buscando primero en el directorio classes WEB-INF/classes y entre las librerías de la carpeta WEB-INF/lib del archivo war. Además, con la inclusión de JBoss Modules se puede seguir un esquema OSGi con lo que incluso podríamos usar simultáneamente en el servidor diferentes versiones de la misma librería.
Sin embargo, a pesar de seguir el esquema estándar de buscar las clases y usar OSGi para que Tapestry encuentre los archivos que necesita, como plantillas, imágenes, literales que pueden estar embebidos en los archivos jar de librerías es necesario hacer algunas modificaciones. En una guía de uso de Tapestry con JBoss se explica como conseguir hacer funcionar una aplicación Tapestry tanto en JBoss 7 como en WildFly 8. La solución consiste en proporcionar una clase para que encuentre correctamente los archivos que Tapestry necesita y esta clase será la que veremos en el siguiente ejemplo.
Con la clase que permite funcionar las aplicaciones Tapestry en JBoss/WildFly junto con un poco de configuración para el contenedor de dependencias definido en un módulo será suficiente. La clase es la siguiente:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
package es.com.blogspot.elblogdepicodev.plugintapestry.misc;
import java.io.File;
import java.net.URL;
import java.net.URLConnection;
import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
import org.jboss.vfs.VFSUtils;
import org.jboss.vfs.VirtualFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WildFlyClasspathURLConverter implements ClasspathURLConverter {
private static final Logger logger = LoggerFactory.getLogger(WildFlyClasspathURLConverter.class);
@Override
public URL convert(final URL url) {
if (url != null && url.getProtocol().startsWith("vfs")) {
try {
final URL realURL;
final String urlString = url.toString();
// If the virtual URL involves a JAR file,
// we have to figure out its physical URL ourselves because
// in JBoss 7.0.2 the JAR files exploded into the VFS are empty
// (see https://issues.jboss.org/browse/JBAS-8786).
// Our workaround is that they are available, unexploded,
// within the otherwise exploded WAR file.
if (urlString.contains(".jar")) {
// An example URL:
// "vfs:/devel/jboss-as-7.1.1.Final/standalone/deployments/myapp.ear/myapp.war/WEB-INF/\
// lib/tapestry-core-5.3.2.jar/org/apache/tapestry5/corelib/components/"
// Break the URL into its WAR part, the JAR part,
// and the Java package part.
final int warPartEnd = urlString.indexOf(".war") + 4;
final String warPart = urlString.substring(0, warPartEnd);
final int jarPartEnd = urlString.indexOf(".jar") + 4;
final String jarPart = urlString.substring(warPartEnd, jarPartEnd);
final String packagePart = urlString.substring(jarPartEnd);
// Ask the VFS where the exploded WAR is.
final URL warURL = new URL(warPart);
final URLConnection warConnection = warURL.openConnection();
final VirtualFile jBossVirtualWarDir = (VirtualFile) warConnection.getContent();
final File physicalWarDir = jBossVirtualWarDir.getPhysicalFile();
final String physicalWarDirStr = physicalWarDir.toURI().toString();
// Return a "jar:" URL constructed from the parts
// eg.
// "jar:file:/devel/jboss-as-7.1.1.Final/standalone/tmp/vfs/deployment40a6ed1db5eabeab/\
// myapp.war-43e2c3dfa858f4d2/WEB-INF/lib/tapestry-core-5.3.2.jar!/org/apache/tapestry5/corelib/components/".
final String actualJarPath = "jar:" + physicalWarDirStr + jarPart + "!" + packagePart;
return new URL(actualJarPath);
} else {
// Otherwise, ask the VFS what the physical URL is...
final URLConnection connection = url.openConnection();
final VirtualFile virtualFile = (VirtualFile) connection.getContent();
realURL = VFSUtils.getPhysicalURL(virtualFile);
}
return realURL;
} catch (final Exception e) {
logger.error("Unable to convert URL", e);
}
}
return url;
}
}
|
WildFlyClasspathURLConverter.java
La configuración adicional para el contenedor de dependencias es para que Tapestry use esta nueva clase:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package es.com.blogspot.elblogdepicodev.plugintapestry.services;
...
public class AppModule {
...
public static void contributeServiceOverride(MappedConfiguration<Class, Object> configuration, @Local HibernateSessionSource hibernateSessionSource) {
configuration.add(HibernateSessionSource.class, hibernateSessionSource);
if (isServidorJBoss(ContextListener.SERVLET_CONTEXT)) {
configuration.add(ClasspathURLConverter.class, new WildFlyClasspathURLConverter());
}
}
...
private static boolean isServidorJBoss(ServletContext context) {
String si = context.getServerInfo();
if (si.contains("WildFly") || si.contains("JBoss")) {
return true;
}
return false;
}
...
}
|
AppModule.java
El ContextListener que nos permite acceder al ServletContext es el siguiente:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package es.com.blogspot.elblogdepicodev.plugintapestry.misc;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class ContextListener implements ServletContextListener {
public static ServletContext SERVLET_CONTEXT;
@Override
public void contextInitialized(ServletContextEvent sce) {
SERVLET_CONTEXT = sce.getServletContext();
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
|
ContextListener.java
Además hemos de incluir en el proyecto un par de librerías y usar al menos la versión 16 de guava si se incluye como dependencia en el war:
1
2
3
4
5
6
7
|
dependencies {
...
compile 'com.google.guava:guava:16.0.1'
providedCompile 'org.jboss:jboss-vfs:3.2.1.Final'
runtime 'org.jboss.logging:jboss-logging:3.1.4.GA'
...
}
|
build.gradle
En la aplicación de ejemplo también deberemos actualizar la versión de guava al menos a la versión 16. Y esta clase y configuración es suficiente para que “mágicamente” se solucionen los problemas de las aplicaciones Tapestry con el servidor de aplicaciones JBoss/WildFly. Si no usamos lo indicado en este artículo al acceder al acceder a la aplicación fallaría con una excepción.
Libro PlugIn Tapestry
Si te interesa Apache Tapestry descarga gratis el libro de más de 300 páginas que he escrito sobre este framework en el formato que prefieras, PlugIn Tapestry: Desarrollo de aplicaciones y páginas web con Apache Tapestry, y el código de ejemplo asociado. En el libro comento detalladamente muchos aspectos que son necesarios en una aplicación web como persistencia, pruebas unitarias y de integración, inicio rápido, seguridad, formularios, internacionalización (i18n) y localización (l10n), AJAX, ... y como abordarlos usando Apache Tapestry.