Recientemente hemos tenido que integrar un sistema diseñado exclusivamente para Windows y Visual Basic a una aplicación web desarrollada completamente en Java (con un framework nuestro)… Aunque la teoría del uso de JNI es bastante sencilla hay cosas que no se explican en ningún sitio y que nos hizo perder muchas horas… Os dejo aqui un resumen de cómo hacer las integraciones y cuales son los problemas más importantes que nos podemos encontrar…
El problema: una aplicación desarrollada por terceros en Visual Basic, en la forma de un par de dlls que se deben registrar y que son llamadas como objetos OLE desde las aplicaciones tenía que ser invocada remotamente por una aplicación web en java corriendo en una máquina Linux…
Los problemas eran múltiples:
- Instalar la aplicación «extraña» en una máquina windows para que pudiese funcionar
- Hacer que se pueda llamar a la aplicación windows desde java
- Comunicar la máquina linux con la máquina windows para pasar las llamadas
- Convertir adecuadamente los datos entre las plataformas
Gracias a Dios la mayor parte de los problemas tenían soluciones estandar que cubrir y los que eran más complucados ya teníamos experiencia en resolver… Lo malo es que los problema triviales eran los que nos robaban más tiempo. Pero vayamos por partes… Si yo fuese un consultor de estos que cobran por dejar el papel encima de la mesa, hubiese dicho lo siguiente sobre la solución a aplicar:
Habrá que utilizar una solución SOA basándose en la arquitectura J2EE y elementos de integración JNI combinados con WS/SOAP y utilización de Unicode junto con UTF-8 para armonizar las interacciones
Y sería completamente cierto… Vamos a ver cómo empezar:
Como no nos gustaría tener que escribir demasiado utilizando windows, no creemos demasiado factible generar unos WS en C++ o en .net para acceder a los servicios de los componentes OLE, por lo que el primer paso será hacer que se puedan llamar a estos componentes desde java. Para ellos utilizaremos JNI Java Native Interface.
JNI nos permite hacer llamadas desde Java a programas o librerías que se encuentran sólo de forma nativa para el sistema operativo en el que va a correr la aplicación. Podría parecer contraproducente disponer de este mecanismo en un lenguaje multiplataforma, pero como veremos, es muy útil y bastante sencillo de utilizar.
Lo primero para utilizar JNI es definir las funciones a las que vamos a llamar y generar un «esqueleto» de las funciones nativas equivalentes. De esta manera podremos llamar desde Java a estas funciones y JNI se encargará de instruir a la máquina virtual para localizar la librería en la que se encuentra esta función y hacer la llamada nativa.
import java.util.*; class ReadFile { native byte[] loadFile(String nombre); static { System.loadLibrary("nativelib"); } }
En este caso hemos declarado que vamos a llamar a una función nativa loadFile que recibe un string (nombre) y devuelve un array de bytes… Además, hemos declarado que se encuentra en una librería llamada «nativelib». Lo siguiente es utilizar la utilidad javah para generar un archivo de cabecera (nos sirve igual para c o c++) que será la base sobre la que construiremos la aplicación nativa que compilaremos en nativelib:
javah -jni ReadFile.java
/* * Class: ReadFile * Method: loadFile * Signature: (Ljava/lang/String;)[B */ JNIEXPORT jbyteArray JNICALL Java_ReadFile_loadFile (JNIEnv *, jobject, jstring);
Vemos que se crea una función Java_ReadFile_loadFile (se llamaría Java_clase_función) que recibe como parámetros un JNIEnv, un jobject y un jstring (ese es nuestro parámetro nombre). Implementaríamos en C o C++ el cuerpo de esta función:
#include <jni.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> JNIEXPORT jbyteArray JNICALL Java_ReadFile_loadFile (JNIEnv * env, jobject jobj, jstring name) { caddr_t m; jbyteArray jb; jboolean iscopy; struct stat finfo; const char *mfile = (*env)->GetStringUTFChars( env, name, &iscopy); int fd = open(mfile, O_RDONLY); if (fd == -1) { printf("Could not open %s\n", mfile); } lstat(mfile, &finfo); m = mmap((caddr_t) 0, finfo.st_size, PROT_READ, MAP_PRIVATE, fd, 0); if (m == (caddr_t)-1) { printf("Could not mmap %s\n", mfile); return(0); } jb=(*env)->NewByteArray(env, finfo.st_size); (*env)->SetByteArrayRegion(env, jb, 0, finfo.st_size, (jbyte *)m); close(fd); (*env)->ReleaseStringUTFChars(env, name, mfile); return (jb); }
y lo compilariamos como nativelib.dll usando nuestro compilador favorito (recomendamos Visual Studio si vamos a trabajar con windows).
Luego, cada vez que creemos un objeto de ReadFile y llamemos a loadFile la máquina virtual buscará en el classpath nativelib.dll, la cargaría y ejecutaría la función pasándole los parámetros… ¡ESE PASO ES IMPORTANTE, NO OS OLVIDEIS DE QUE JAVA DEBE ENCONTRAR LA DLL!… Para una referencia más completa mejor mirar esta introducción a JNI.
Para la tercera parte, la de comunicar la aplicación web con la aplicación windows decidimos utilizar WebServices generados mediante AXIS. El mayor problema (no lo voy a detallar en esta entrada) es instalar un tomcat en windows y hacer que arranque automáticamente… Et voila, ya solo tenemos que usar un endpoint para poder realizar llamadas Java-Java mediante SOAP con los webservices generados por AXIS. Es una mecánica más farragosa, pero en nuestro caso teníamos la experiencia suficiente para que no nos diese ningún problema.
Y la última parte, que parece la más trivial, que es convertir datos entre plataformas, nos encontramos con algunos problemas serios… Windows y sus dll esperaban recibir unas cadenas de texto que, suponiendo-suponiendo tenían una codificación ANSI (CP-1252 en nuestra localización de windows) y en Java nuestras cadenas estaban en UTF-8 (que son cómo podemos hacerlas llegar mediante JNI a la parte en C++). UTF-8 es multibyte, es decir, un carácter puede representarse por uno o varios bytes. En el caso de que el carácter sea ASCII, se usa 1 byte, pero si el carácter es «internacional» se usan dos bytes… ¿problema trivial? En Java si, ya que es muy sencillo pasar de un charset a otro en sus Strings, pero en C++ la cosa se complica… El caso es que tardamos como dos días en dar con la solución buena, y que pasa por usar estas dos funciones:
char* Utf8toAnsi( const char * utf8, int len ) { char *ansistr = NULL; int length = MultiByteToWideChar(CP_UTF8, 0, utf8, len, NULL, NULL ); WCHAR *lpszW = NULL; lpszW = new WCHAR[length+1]; ansistr = ( char * ) calloc ( sizeof(char), length+5 ); //this step intended only to use WideCharToMultiByte MultiByteToWideChar(CP_UTF8, 0, utf8, -1, lpszW, length ); //Conversion to ANSI (CP_ACP) WideCharToMultiByte(CP_ACP, 0, lpszW, -1, ansistr, length, NULL, NULL); ansistr[length] = 0; delete[] lpszW; return ansistr; } char *AnsitoUtf8( const char * ansi, int len ) { char *utf8str = NULL; int length = MultiByteToWideChar(CP_ACP, 0, ansi, len, NULL, NULL ); WCHAR *lpszW = NULL; lpszW = new WCHAR[length+1]; MultiByteToWideChar(CP_ACP, 0, ansi, -1, lpszW, length ); int utflen = WideCharToMultiByte(CP_UTF8, 0, lpszW, -1, utf8str, 0, NULL, NULL); utf8str = ( char * ) calloc ( sizeof(char), utflen+1 ); WideCharToMultiByte(CP_UTF8, 0, lpszW, -1, utf8str, utflen, NULL, NULL); utf8str[utflen] = 0; delete[] lpszW; return utf8str; }
Básicamente lo que hacemos es convertir a 16 bits (unicode) cada una de las codificaciones (ANSI o UTF-8) y volver a codificar en la que queremos que sea la resultante.
NOTA: Este código está escrito a vuelapluma… Seguro que encontramos alguna optimización en los próximos días…
Como veis, todo bastante trivial, pero harto dificil de hacer a la primera…