Twister's blog

Aller au contenu | Aller au menu | Aller à la recherche

mercredi, juillet 24 2013

Lire le contenu de fichiers dans un fichier zip en JAVA

Je viens de tomber sur un problème bête en Java, j'ai une application client/serveur avec un client qui envoie régulièrement des fichiers de logs au serveur sous forme de fichier ZIP. Le serveur doit décompresser les fichiers afin de la analyser pour en tirer des statistiques. Ce qui m'embêtait dans ce développement c'est qu'il faut dézipper les fichiers avant de pouvoir les analyser et que ce "dézippage" pourrait éventuellement laisser trainer des fichiers temporaires quelque part sur le serveur. Il me fallait donc une autre solution. J'en voyais 2 :

  • soit concatener les fichiers de logs en un seul fichier qu'il aurait suffit de gzipper pour pouvoir le lire avec un GZIPInputStream côté serveur.
  • soit permettre au serveur de lire de manière transparente les données de tous les fichiers contenus dans le zip.
La première solution aurait impliqué de faire des modifications sur le client qui utilise un système déjà existant (et standard pour notre application) pour envoyer ses fichiers ZIP, donc pas top. La seconde pose problème parce que le JDK ne fournit qu'un ZipInputStream qui permet de lire le Zip fichier par fichier, il faut en effet faire un appel à ZipInputStream.getNextEntry() entre chaque fichier pour passer à la suite. Comme l'ordre de lecture des fichiers n'a pas d'importance je décidais de faire un petit wrapper au stream afin qu'il soit capable de passer tout seul d'un fichier à l'autre. Et voilà le travail :

public static Reader acquireReader(File file) throws IOException {
       if (file.getName().endsWith(".zip")) {
           final ZipInputStream zis = new ZipInputStream(new FileInputStream(file));
           return new InputStreamReader(zis) {
               @Override
               public int read(char cbuf, int offset, int length) throws IOException {
                   int read = super.read(cbuf, offset, length);
                   if (read < length) {
                       if (read == -1) {
                           read = 0;
                       }
                       zis.getNextEntry();
                       read += super.read(cbuf, offset + read, length - read);
                   }
                   return read;
               }

               @Override
               public int read() throws IOException {
                   int c = super.read();
                   if (c == -1) {
                       zis.getNextEntry();
                       c = super.read();
                   }
                   return c;
               }
           };
       }
       return new FileReader(file);
   }

Comme vous pouvez le voire c'est relativement bateau :).

vendredi, juin 1 2012

Java code quality (Sonar, PMD, findBugs, ...)

Je viens de tomber sur quelques bugs sympa causés par un désir fou de faire baisser les niveaux d'alertes dans Sonar.

Et oui PMD (intégré à Sonar) râle lorsque l'on fait un new Integer(1). Cela dit la solution n'est pas de remplacer new Integer(1) par 1. Et oui faire ce changement la peu provoquer des problèmes. Si le new Integer(1) était un paramètre d'appel de méthode cela peut changer la méthode appelée. Dans mon cas il est arrivé sur une modification de ce genre que l'on passe d'un appel à une méthode add(Obect o, Object constraint) à add(Object o, int i) qui n'avait pas du tout les mêmes fonctions ! Et oui elles appellent toutes les deux la méthode add(Object o, Object constraint, int index)... Vous voyez où je veux en venir ??? :-)

Bref n'écoutez pas bêtement les outils de qualité de code ils peuvent vous faire faire des bétises, ou alors écoutez les bien. En l’occurrence sur une instanciation d'Integer PMD donne le message suivant :

Avoid instantiating Integer objects. Call Integer.valueOf() instead.

C'est d'ailleurs la solution sûr pour régler le warning puisque Integer.valueOf() va renvoyer un objet Integer et non un int. Son intérêt par rapport à l'instanciation brute d'un Integer et qu'il utilise un système de cache des Integers.

J'ai pu constater plusieurs commits sur d'autres projets qui, pour faire baisser des alertes sonar, ont sûrement provoqué des bugs. Par exemple un jour j'ai vu un petit rigolo s'amuser à remplacer plusieurs :

int a = 10;
StringBuffer sb = new StringBuffer();
//... bla bla ...
sb.append(" " + a);
return sb.toString();

par

int a = 10;
StringBuffer sb = new StringBuffer();
//... bla bla ...
sb.append(' ' + a);
return sb.toString();

(Là, maintenant comme ça, à chaud, vous voyez ce que ça change ? ;-))

Alors maintenant que faire, certaines modifications ont pû provoquer des bugs qui ont été fixés en amont, d'autres peuvent être toujours là. La question est : est ce qu'il faut les fixer. Essayer de les fixer pourrait provoquer d'autres problèmes (si par exemple d'autres bouts de code on été modifiés pour répondre aux modifs initiales...).

Bref ma conclusion est que ce genre d'outils c'est bien, mais qu'il faut faire attention. Rien ne remplace une charte de codage fourni au début d'un projet et à laquelle les développeurs doivent se tenir. Eventuellement il est bien d'avoir un outil de qualité qui remonte les effractions au fur et à mesure du développement. Mais déployer un tel outil quand le projet a déjà de l'âge et écouter les alertes sur le code existant peut être dangereux...

mardi, mars 27 2012

Java - Attention à vos garbages collectors !

Si vous êtes un développeur JAVA vous avez sûrement déjà eu besoin de paramétrer le garbage collector pour qu'il colle au mieux à votre application.

Pour ma part je travaille sur une application de trading ayant une architecture distribuée qui utilise une bonne pelletée de serveur (plus de 100) et qui se doit d'avoir des temps de réponses rapides. Nous sommes donc censés avoir des JVMs et des GC configurés de telle sorte que les processes soient le moins possible interrompus par la garbage collection.

Ainsi quand on livre un process en prod on le livre avec ses paramètres de GC. Jusque là tout va bien. Malheureusement certains de nos process ont une dizaine d'années. Ils ont donc été migrés de serveurs en serveurs au fil des ans (plus de ram, plus de coeurs, plus de Hz, ...) sans que personne ne se préoccupe de vérifier un tant soit peu les paramètres de GC. Dans le meilleur des mondes tout aurait pu bien se passer, mais non...

J'ai découvert avec stupeur qu'un process censé être "temps réel" avait 14,4Mo de Young pour 1.970Mo de Old !!!! Echec. Le plus fun est que ce process tourne en fault-tolerance[1] et que le process de backup a lui 115Mo de Young. Et oui, les deux serveurs où tournent les instances sont différents. Ils ont tous les deux 10Go de ram, mais l'un est bi-proc quad-core et l'autre est bi-proc bi-core et bien sûr les procs ont des fréquences différentes.

Que s'est il passé ? Et bien c'est simple au bout de 2h d'uptime le process de backup est arrivé à bout de sa heap et a décidé de rajouter 100Mo de heap (il était justement 100Mo de son Xmx) et il a tout mis dans sa Young. Ca pourrait être le hasard, mais ce schéma se reproduit presque tous les jours (en fonction de la charge de la journée). Et cela se produit sur tous les couples que nous avons de ce process qui tournent sur des serveurs différents (6 couples sur 12 serveurs). Les serveurs les plus rapides utilisent plus de young. Cela est dû au parrallel GC que nous utilisons et au fait que la JVM change ses paramètres en fonction des performances du process (et donc de la machine)[2]. En effet ajouter de la mémoire impacte les temps de GC, et donc le throughput de l'application, sur les serveurs lents la VM mets plus de mémoire dans la Old, et sur les rapides elle en met plus dans la Young, je trouve ça idiot puisque du coup les collections majeurs sont plus fréquentes sur les vieux serveur et l'application est donc plus lente... échec.

En conclusion quand vous fixez vos paramètres de GC pour un serveur, faites attention ils ne seront peut être pas valide pour un autre serveur même s'il est plus puissant... Ensuite si vous avez définit un Xmx n'hésitez pas à mettre un Xms égal au Xmx et à définir un ratio young/old cohérent avec votre application. (attention le ratio est appliqué au démarrage et dépend de la heap à ce moment, donc Xmx=Xms est important !!!)

Notes

[1] il tourne en 2 exemplaires, un actif, un inactif. Quand l'actif crash, l'inactif devient actif et prend le relais

[2] http://docs.oracle.com/javase/1.5.0/docs/guide/vm/gc-ergonomics.html

mercredi, juin 3 2009

Firefox + Applet Java + CSS Resize

Salut à tous, aujourd'hui un peu de technique. J'ai récemment eu à intégrer une applet Java dans une application Web. Le client voulait pouvoir passer l'applet en full screen avec un bouton.

J'ai été très surpris par le comportement de Firefox vis à vis de cela. En effet sous Internet Explorer pas de problèmes, on met l'applet dans un div avec des paramètres de tailles à 100% et lorsque l'on retaille le DIV l'applet suit les changements sans problèmes. Avec Firefox ca a été une autre paire de manches.

Ok l'applet était bien retaillée sous Firefox mais malheureusement celle ci était rechargée. En effet Firefox détruit et recréé une nouvelle instance de l'applet pour l'afficher à la nouvelle taille. Dans beacoup de cas cela ne sera pas un problème mais dans mon cas c'en était un gros. L'applet en question est JxCell qui est une applet (très bien faite d'ailleurs) permettant l'édition de fichier Excel en ligne. Résultat le retaillage du div provoquait le rechargement du fichier Excel et donc la perte des modifications effectuées avant le passage en full screen... ... ...

Bref il a fallut ruser un maximum. Ce qu'il faut savoir c'est qu'il ne faut surtout pas toucher au conteneur de l'applet alors que vous pouvez toucher à l'applet elle même.

Voici comment j'ai procédé : 1. Mettre l'applet au plus au niveau du HTML (directement dans le BODY) 2. Mettre un DIV à l'endroit où l'applet doit apparaître sous sa petite forme. Ce div doit avoir la taille que devra avoir l'applet. 3. Au chargement de la page déplacer l'applet par dessus le DIV afin de le recouvrir. 4. Pour passer en fullScreen il suffit de changer la taille de l'applet 5. Pour repasser en mode normal il suffit de rechanger la taille de l'applet pour recouvrir à nouveau le DIV.

Voici les bouts de codes nécessaires à la manip :

Dans le code le DIV de placement s'appelle appletDivPlace

JavaScript :

function getSize(objName) {
    var obj = document.getElementById(objName);
    return \[obj.offsetWidth, obj.offsetHeight\];
}

function getOffset(objName) {
    var obj = document.getElementById(objName);
    var curleft = 0;
    var curtop = 0;
    do {
	curleft += obj.offsetLeft;
	curtop += obj.offsetTop;
    } while (obj = obj.offsetParent);
    return \[curleft, curtop\];
}

function getWindowHeight()
{
    var myWidth = 0, myHeight = 0;
    if( typeof( window.innerWidth ) == 'number' )
	{ //Non-IE
	    myHeight = window.innerHeight;
	}
    else
	{
	    if ( document.documentElement && ( document.documentElement.clientWidth || document.documentElement.clientHeight ) )
		{ //IE 6+ in 'standards compliant mode'
		    myHeight = document.documentElement.clientHeight;
		}
	    else
		{
		    if ( document.body && ( document.body.clientWidth || document.body.clientHeight ) )
			{ //IE 4 compatible
			    myHeight = document.body.clientHeight;
			}
		}
	}
    return myHeight;
}

function fullScreen(editable) {
    var applet = document.Applet;
    var appletDivPlace = window.document.getElementById("appletDivPlace");

    var height = getWindowHeight() - 50 + "px";
    var width = document.body.parentNode.scrollWidth - 50 + "px"; 

    applet.width = width;
    applet.height = height;
    applet.style.top = "20px";
    applet.style.left = "0px";
}

function initApplet() {
    // move the applet over the div representing the position of the applet
    var applet = document.Applet
    var offset = getOffset("appletDivPlace");
    var size = getSize("appletDivPlace");

    applet.style.left = offset[0];
    applet.style.top = offset[1] + 20;
    applet.width = size[0];
    applet.height = size[1];
}

HTML:

<HTML>
<HEAD><TITLE>Applet</TITLE></HEAD>
<BODY onLoad="javascript:initApplet">
<APPLET id="applet" name="applet" .... />
...
...
           <DIV id="appletDivPlace" style="width:200px; height:200px"></DIV>
           <A HREF="#" onClick="javascript:fullScreen();">Fullscreen</a>
...
...
</BODY>
</HTML>