vendredi 24 septembre 2010

Rediriger des requêtes de Apache vers Tomcat

Il est d'usage de ne pas exposer son serveur Tomcat (server Servlet+JSP) à l'insécurité régnant sur le WEB. On considère, à juste titre sans doute, que ce bel édifice Java a la cuirasse un peu fine pour survivre bien longtemps au flot de requêtes plus ou moins bienveillantes qui s'abat immédiatement sur tout serveur référencé par Google.
Il n'en est pas de même pour son grand frère Apache qui résiste à tout du moment que son administrateur ait survécu aux affres de la configuration.
Une fois cela posé, on comprend pourquoi les applications Java sont habituellement servies par un serveur Tomcat causant sur un port privé (8080 par exemple) et s'abritant derriere un serveur Apache causant sur sur le classique port 80.
La question est de savoir comment dire à Apache de rediriger correctement les requêtes destinées à Tomcat.
Il existe deux méthodes: Le proxy et AJP.

Le proxy:
Cette méthode n'est pas dédiées à Tomcat. Elle peut être utilisée pour rediriger des requêtes vers n'importe quel serveur HTTP. Tout se passe au niveau du fichier de config d'Apache httpd.conf (habituellement dans /etc/httpd/conf sous Linux).

1) S'assurer que les bon modules sont chargés: Vérifier que les modules suivants sont bien dans /etc/httpd/modules, sinon les charger avec yum, apt-get ou même depuis le site apache (http://httpd.apache.org/modules/). Le code de configuration ci-dessous demande à Apache de charger ces modules au démarrage

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
LoadModule proxy_ftp_module modules/mod_proxy_ftp.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so

Supposons que Apache tourne sur une machine nommée frontale et que Tomcat tourne sur une machine nommée tomcat.
Supposons que l'URLeffective de l'application soit http://tomcat:8080/APPLI mais que vue de l'extérieur, elle soit référencée par l'URL http://frontale/appli. Supposons également que cette application utilise des cookies.
Il suffit d'insérer le bloc suivant dans le fichier httpd.conf et de relancer Apache (et Tomcat)

ProxyPass /appli http://tomcat:8080/APPLI
ProxyPassReverse /appli http://tomcat:8080/APPLI
ProxyPassReverseCookiePath /APPLI /appli
ProxyPassReverseCookieDomain tomcat frontale

Cette méthode est très simple mais elle n'est pas très performante car elle demande à Apache de transférer tous les paquets d'un serveur HTTP à un autre (transfert a niveau application). Cela demande une double prise en charge du protocole HTTP.

Il est bien plus efficace d'effectuer le transfert des paquets TCP/IP au niveau des sockets directement. Tomcat implémente par défaut un serveur de sockets (port 8009) dédié cette fonction. Apache dispose d'un module (mod_jk) effectuant ce transfert. C'est le protocole AJP (version 1.3). Il est implémenté par la plus part des serveusr HTTP et des serveurd d'applications.

La méthode AJP:

L'idée est de déclarer un connecteur nommé (monworker dans notre exemple) qui sera en charge des redirections pour une ou plusieurs applications. Dans l'exemple ci-dessous, on suppose que Tomcat et Apache tournent sur le même serveur.

Il faut d'abord configurer Tomcat pour qu'il charge le module mod_jk au démarrage:

LoadModule jk_module modules/mod_jk-1.2.30-httpd-2.0.X.so

Vérifier que le module est bien dans /etc/httpd/modules, sinon le charger avec yum, apt-get ou depuis le site apache (http://mirror.mkhelif.fr/apache//tomcat/tomcat-connectors/jk/binaries/).

Il faut ensuite déclarer le connecteur (worker) dans un fichier. Ce fichier est habituellement localisé dans /etc/httpd/conf/workers.properties.

worker.list=monworker
worker.monworker.port=8009
worker.monworker.host=localhost
worker.monworker.type=ajp13

Dans notre exemple, le nom du worker est monworker, il utilise le port 8009, il est hébergé sur la machine locale et il utilise le protocole AJP 1.3.

Il faut ensuite donner quelques indications à Tomcat pour qu'il utilise le module AJP.
Les règles suivantes sont à mettre dans httpd.conf. Elles parlent d'elles mêmes.

JkWorkersFile /etc/httpd/conf/workers.properties
JkLogFile /var/log/httpd/mod_jk.log
JkShmFile /var/log/httpd/mod_jk.shm
JkLogLevel info

Une fois cela fait il ne reste plus qu'à déclarer un hôte virtuel pour mon application Tomcat dans httpd.conf:

JkMount /appli monworker
JkMount /appli/* monworker

Noter que dans ce cas, l'application garde le même nom (appli).
Une fois cela fait et les serveurs redemarrés, les requête du type http://frontale/appli/.. seront exécutées par Tomcat comme http://frontale:8080/appli/..

On verra plus tard comment comment faire avec deux serveurs distincts.

vendredi 28 mai 2010

Mettre des librairies natives dans un jar, et les utiliser

Certaines applications Java peuvent utiliser du code natif C ou C++ grâce au framework JNI. On parlera pas ici de la manière d'utiliser l'interface JNI, mais de la manière de l'emballer dans l'application finale pour que l'utilisateur n'ait pas à s'en soucier.

En général, une fois le développement terminé, on place les classes dans un fichier jar et on délivre l'application sous cette forme, avec ou sans enrobage supplémentaire tel qu'un installeur.
Avec JNI, outre les classes, on se retrouve avec des librairies natives: une par plateforme supportée.


La question est de savoir que faire de ces librairies. On peut bien sûr les mettre à côté, mais cela oblige à spécifier leur emplacement au moyen de paramètres sur la ligne de commande Java. exemple: java -Dava.library.path=lib_dir .....
Au delà de cette lourdeur, il reste la question de l'identification de la librairie associée à la plateforme. On va voir comment affranchir l'utilisateur de tous ces problèmes.

Dans un premier temps, comme la présence de librairies natives à coté des JAR fait désordre, on va profiter du fait que le jar n'est rien d'autre que tar compressé. On peut y mettre ce que l'on veut, on va donc y mettre nos librairies.

1) Bien nommer les librairies:
On va d'abord renommer nos librairies afin d'identifier clairement l'architecture à laquelle elles se rattachent.
Il existe 2 propriétés java indiquant l'architecture sur laquelle tourne la machine java:
  • os.name: retourne le nom de l'OS (Linux, Win ou Mac)
  • os.arch: retourne l'architecture matérielle (i386, universal...)
On va renommer nos librairies sous la forme suivante: os.name_os.arch.lib, le tout en minuscules afin d'éviter les problèmes de casse. Ainsi, sous Linux 32bits, ma librairie s'appellera linux_i686.lib.

2) Placer les libraires à la racine du fichier JAR.
Cela n'est pas obligatoire, mais ca facilite. Il faut comprendre que le fichier JAR est vu par la JVM comme un répertoire accessible pour rechercher des ressources. On peut donc y organiser nos fichiers à notre guise (sauf pour les .class qui doivent respecter la hiérarchie des paquetages).

exemple: jar cvf mon_application.jar *.lib .....

3) Intégrer dans le code java le chargement des librairies natives.
Avant de faire appel à des méthodes natives, il faut charger le code natif. Le loader de Java sais bien le faire à condition de lui indiquer où chercher.
Cela se fait par étapes:
  1. Construire le nom de la librairie à partir des ressources os.*
  2. Localiser la ressource.
  3. Extraire la bonne librairie
  4. Charger la bonne librairie.
En bon java cela donne (on suppose que la classe native est sqlite.tools.ASCIIDataFileLoader):

public static void loadNativeLibrary() throws Exception {
if( LIB_LOADED == false ) {
String libname = System.getProperty("os.name")
+ "_"
+ System.getProperty("os.arch")
+ ".lib";
ClassLoader cl = Class.forName("sqlite.tools.ASCIIDataFileLoader")
.getClassLoader();
InputStream in = cl.getResourceAsStream(libname);
if (in == null) {
throw new Exception("libname: "
+ libname
+" not found (supposed to be in sqliteimporter.jar)");
}
/*
* Extract the lib file and link it with the app
*/
File tmplib = File.createTempFile("libsqlitejdbc-", ".lib");
tmplib.deleteOnExit();
OutputStream out = new FileOutputStream(tmplib);
byte[] buf = new byte[1024];
for (int len; (len = in.read(buf)) != -1;) {
out.write(buf, 0, len);
}
in.close();
out.close();
System.load(tmplib.getAbsolutePath());

LIB_LOADED = Boolean.TRUE;
}
}


Il faut ensuite lier l'appel de cette méthode à l'appel de chaque méthode native:


class .......
.....
static int importTSV(String table, String file, String db_file)
throws Exception {
ASCIIDataFileLoader.loadNativeLibrary();
return importASCIIFile(table, file, "\t", db_file);
}

native static int importASCIIFile(String table, String file
, String separ, String db_file) throws Exception;
}

Cela ne marche bien que si notre application n'utilise qu'une seule librairie native. Dans le cas contraire, il faut un peu compliquer le nommage afin d'y inclure le nom de la classe ou du paquetage ou de n'importe quoi d'autre évitant toute ambiguïté.

jeudi 27 mai 2010

Faire du JNI sous Eclipse

Rappel
Un petit rappel: JNI (Java Native Interface) est une interface de programmation permettant à des classes JAVA d'inclure du code C ou C++. Il peut paraître paradoxal de vouloir ainsi perdre la portabilité du langage. Il y a toutefois de nombreux cas pour lesquels cette interface est très utile. D'une manière générale, elle permet de récupérer des codes éprouvés, difficile voire impossible à porter en JAVA.

JNI c'est bien mais..
La conséquence de l'utilisation de JNI est qu'il faut (re)mettre les mains dans la programmation C.
Il faut aussi se rapeller que la classe JAVA en question devra être accompagnée des librairies pour toutes les plateformes sur lesquelles elle est susceptible de tourner (MAC, Window, Linux, 32/64bits..). On verra dans un prochain post comment s'y prendre.

Je veux pas quitter Eclipse
Comme beaucoup des utilisateurs d'Eclipse ont du mal à revenir au bon vieux couple emacs/gcc, on va voir comment mettre au point un projet JNI dans Eclipe.

1) Installer CDT sur Eclipse
CDT est le plugin C/C++ d'Eclipse.
- Aller dans Help->Install New Software
- Cliquer sur Available Software Sites
- Cliquer sur add et remplir les champs suivants:
    name = CDT
    URL = http://download.eclipse.org/tools/cdt/releases/galileo

- Selectionner CDT dans le combobox Work With
- Continuer l'installation avec Next/Next/Finish

2) La partie Java
- Créer un projet Java dans Eclipse (JSQLITE dans mon cas).
- Déclarer les méthodes natives là ou bon vous semble. Tant que les classes possédant ce genre de méthodes ne sont pas appelées, tout va bien. Autrement, le linker vous gratifie d'un java.lang.UnsatisfiedLinkError signifiant qu'il n'a pas trouvé le code natif.
- Créer dans le projet un répertoire pour les futurs fichiers headers (jni_headers par exemple). Cela se fait par le menu File->New->Directory.

Il faut maintenant créer les fichiers header contenant les prototypes C des méthodes natives. Pour cela, on peut créer un tache d'exécution en cliquant sur le bouton run surmontant une petite valise rouge.
- Clique droit sur Program sélectionner New et remplir les champs de la manière suivante:
    name: javah
    location: /usr/lib/jvm/jdk1.6.0_16/bin/javah
Il s'agit du chemin d'accès à l'utilitaire Java créant les fichiers headers. Il se trouve dans votre JDK.
    Argument -classpath build -d jni_headers wrapper.SQLiteDataImporter. On suppose que wrapper.SQLiteDataImporter est notre classe avec des méthodes natives

L'exécution de cette tâche doit produire les fichiers d'entêtes dans le répertoire jni_header.

3) La partie C
- Créer un projet C de type Shared Library: menu File->New->C Project, puis, Shared Library->Empty Project. Dans note exemple, ce projet s'appelle JSQLITENative.
- Importer les headers JNI: Clique droit sur le projet, puis Properties->C/C++ General->Paths and Symbols.
- Sélectioner le langage GNU C du tab Includes
- Cliquer sur Add->Workspace, puis sélectionner JSQLITE->jni_headers, valider.
- De la même manière ajouter les répertoire JAVA_HOME/include et JAVA_HOME/include/platform afin d'inclure les headers JNI dans les règles de compilation.
- Vos headers JNI doivent apparaitre dans le dossier Includes
- Créer votre source C: menu File->New->Source File
- Ecriver et compiler votre code. Attention, CDT ne pratique pas la compilation à la volée comme en Java. Il vous faut faire un CTRL B pour compiler. En cas d'erreurs persistentes, un peu de nettoyage peut arranger les choses: menu Project->Clean....
- La librairie doit être placée dans le répertoire Binaries.

4) Faire tourner le code Java.
Il faut d'abord indiquer ou se trouve la librairie C.
- Ouvrir la configuration de lancement de votre application Java: Bouton Run puis Run Configuration.
- Sélectionner votre application.
- Ouvrir le tab Argument
- Mettre la ligne suivant dans la zone VM Arguments: -Djava.library.path="${workspace_loc:SQLITENative/Debug}".
Attention le répertoire Debug est le nom du répertoire de votre Workspace ou se trouve la librairie C. Il ne s'agit pas de son avatar figurant dans l'explorateur de projets qui est Binaries.
- N'oublier pas charger la librairie dans le main de votre application: System.loadLibrary("SQLITENative"); pour un libraire dont le nom est libSQLITENative.so.


Voilà, c'est sommaire, mais ca peut aider. N'oublier pas de refaire cette manip sur chacune des plateforme pour lesquelles vous voulez implémenter vos classes JNI.

jeudi 18 février 2010

Mode curseur et transactions avec JDBC (PostgresQL)

Lorsque l'on travaille en mode curseur sous JDBC, le mode auto-commit du connecteur est nécessairement dévalidé:

connection.setAutoCommit(false);

La conséquence est que le postmaster garde en mémoire le log des requêtes.
Si l'on fait plusieurs requêtes avec le même connecteur JDBC, on reste connecté au même Postmaster et ce dernier explose en mémoire.

J'ai essayé d'insérer des commits entre les requêtes, mais cela a produit l'erreur suivante:

DECLARE CURSOR pourrait seulement être utilisé dans des blocs de transaction

J'avoue en pas très bien comprendre le sens de ce message et je me suis résolu à créer un nouveau connecteur à chaque requête en mode curseur:


public static ResultSet runLargeQuerySQL(String sql) throws FatalException {
try {
closeLargeQueryConnection();
jdbc_large_connection = DriverManager.getConnection(Database.getConnector().getJdbc_url(),Database.getConnector().getJdbc_reader(), Database.getConnector().getJdbc_reader_password());
Statement _stmts =jdbc_large_connection.createStatement(ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
jdbc_large_connection.setAutoCommit(false);
_stmts.setFetchSize(1000);
if( Messenger.debug_mode ) Messenger.printMsg(Messenger.DEBUG, "Select large query: " + sql);
/*
* Trailing semicolumns make the statement to run in non-cursor mode
*/
return _stmts.executeQuery(sql.replaceAll(";", ""));
} catch (Exception e) {
Messenger.printMsg(Messenger.ERROR, "Query: " + sql);
Messenger.printStackTrace(e);
FatalException.throwNewException(SaadaException.DB_ERROR, e);
}
return null;
}

/**
* @throws SQLException
*/
public static void closeLargeQueryConnection() throws SQLException {
if( jdbc_large_connection != null ) {
if( Messenger.debug_mode ) Messenger.printMsg(Messenger.DEBUG, "Close connection for large queries ");
jdbc_large_connection.close();
jdbc_large_connection = null;
}

}

mercredi 17 février 2010

Quand le mode curseur de JDBC ne fonctionne pas sur PostgresQL

Imaginer la requete suivante sur une grosse table (10.000.000 lignes):

SELECT oidprimary,oidsecondary FROM Counterparts

Comme vous ne voulez pas que votre application Java vous fasse un OutOfMemoryError, vous traitez votre requête en mode curseur (voir ici)):

connection().setAutoCommit(false);
Statement _stmts = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
_stmts.setFetchSize(1000);
ResultSet rs = _stmts.executeQuery("SELECT oidprimary,oidsecondary FROM Counterparts");
....

Et ca marche, mais attention, si à la suite d'un couper/coller malicieux par exemple, votre requête se voit suivie d'un ";", vous risquez de vous retrouver avec cette erreur:

Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space

En effet, le pilote JDBC de PostgresQL considère alors que votre requête comporte une suite de requêtes séparées par des ";" et dans ce cas il quitte le mode curseur et tente de recopier tout les résultat en mémoire.

jeudi 19 février 2009

PostgresQL vs MySQL

Mon application a été construite sur PostgresQL, système de base de données auquel on accède par JDBC. Dès le départ il a été envisagé d'utiliser d'autres SGDBs. Nous avons toutefois attendu une demande formelle avant de passer à l'acte.
C'est ce qui s'est passé récemment avec un utilisateur confessant que notre Saada serait encore plus génial s'il pouvait utiliser MySQL en lieu et place de Postgres et que autrement il ne lui serait d'aucun intérêt.
Vous trouverez plus loin un bestiaire des incompatibilités SQL entre les deux systèmes. Il ne s'agit pas ici de comparer mais simplement de pointer les différences.

Les transactions


L'ouverture de transaction permet de rendre atomique l'exécution d'un ensemble de requêtes. Si l'une plante, la base retombe dans son état initial (rollback) quelque soit les modifications déjà effectuées avant le plantage.






PostgresMySQL
Ouvrir la transaction
BEGIN TRANSACTIONSTART TRANSACTION
Terminer la transaction
COMMITCOMMIT
Annuler la transaction
ABORTROLLBACK

Une différence plus importante concerne le verrouillage des tables. Sous Postgres, le verrouillages des tables durant une transaction est implicite. Il n'y a pas à s'en occuper.
Sous MySQL, il faut verrouiller explicitement toutes les tables auxquelles on accède durant la transaction.

  • Si au cours de la transaction une requête modifie la table TABLE_W il faut la faire précéder par LOCK TABLE TABLE_W WRITE

  • Si au cours de la transaction une requête lit la table TABLE_R il faut la faire précéder par LOCK TABLE TABLE_R READ

  • Si au cours de la transaction une requête lit les tables TABLE_R1 et TABLE_R2 et qu'elle modifie la table TABLE_W, il faut la faire précéder par LOCK TABLE TABLE_R1, TABLE_R2 READ, TABLE_W WRITE


Les tables temporaires


Un petit bonnet d'âne à MySQL: La partie Web des bases Saada utilise un rôle (compte) d'accès à la base avec des droits réduits (le reader) de manière a éviter à des requêtes malicieuses d'altérer les données. Seulement voila, certaines requêtes compliquées utilisent des tables temporaires. Cela nous permet d'éviter des jointure compliquées dont on ne sait jamais comment l'optimiseur du moteur de requête se sortira.
Sous Postgres, il n'y a rien à faire de particulier, le reader peut créer ses tables temporaires.
Sous MySQL, la musique est bien différente. Il faut donner explicitement au reader le droit de créer des tables temporaires:GRANT CREATE TEMPORARY TABLES ON database TO reader

Le chargement de données à partir de fichiers


Lors du chargement de donnée dans MySQL à partir d'un fichiers ASCII (LOAD DATA INFILE...) les contraintes de clés primaires sont vérifiées ligne par ligne, et ça rame vraiment beaucoup. La commande SQL suivante permer de dévalider temporairement les contraintes sur la clé primaire.
ALTER TABLE tbl_name DISABLE KEYS