Sébastian Le Merdy

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

jeudi 8 mars 2012

le singleton statique

J'aime bien les singletons et je n'ai jamais vraiment pris le temps de comprendre pourquoi tant de gens déconseillent ce design pattern (d'ailleurs le singleton est-il un design pattern ?). J'ai eu un exemple très récemment des dangers que cela peut provoquer.

Le projet sur lequel je travaille a une base de code plutôt impressionnante et plutôt pas trop mal testée (de l'ordre de 13000 classes et 56% de couverture de code). Tous ces tests exécutés chaque jour n'auront finalement permi de détecter ce problème de singleton qu'aujourd'hui. Je résume la situation : une classe héritant de ArrayList<MaClasse> a été écrite il y a plusieurs années afin d'ajouter des comportements à ce type de listes bien particulières. Jusque là : pourquoi pas. Problème : cette classe déclare une variable de classe (donc static) nommée EMPTY. Elle permettait à l'origine d'économiser les instanciations pour l'utilisation de la liste vide à l'extérieur. Cela ne choque pas plus de ça - d'autant plus que des classes du JDK offrent ce genre de fonctionnalités.

Une chose primordiale à noter dans la documentation est la notion d'immuabilité. La liste EMPTY découverte dans le code n'est pas immuable et n'interdit donc pas ce type d'instructions :

MaListe.EMPTY.add(new MonObjet());

Inévitablement, ce code a fini par être écrit (en fait, l'ajout dans la liste vide était fait beaucoup plus loin que son initialisation). Et les effets de bord néfastes se sont donc produits.
Pour corriger ce problème, il est nécessaire de créer une implémentation spécifique de MaListe<MaClasse> qui soit immuable.

Moralité : de grands pouvoirs impliquent de grandes responsabilités. Il faut toujours bien savoir ce que l'ont fait quand on parle de singleton, surtout dans un environnement concurrent.

Un article qui couvre une autre dimension du singleton : le lazy singleton.

mercredi 12 octobre 2011

Un résumé super concis de la soirée "Le métier de développeur"

Pour les pressés, un résumé super concis de la soirée qui s'est tenue le mardi 11 octobre organisée par le Paris JUG.

  • Didier Girard : C'est Internet qui permet au développeur de se surpasser.
  • Vincent Massol : Faites quelque chose qui vous passionne, et faites le bien.
  • Jean Laurent de Morlhon : Apprendre à s'améliorer chaque jour grâce au Software Craftmanship.
  • Régis Médina : Permettre aux organisations d'aboutir aux meilleurs produits : ceux qui décuplent la puissance des utilisateurs.
  • Gregory Weinbach : À la fin de chaque journée, il faut avoir appris, transmis ou réalisé quelque chose dont on est fier.

dimanche 6 février 2011

Comment tester en Java un client HTTP sans se connecter ?

C'est cet article datant d'octobre 2000 (!) qui m'a montré la voie. Il explique en détail le mécanisme utilisé par la JVM pour invoquer le bon composant permettant d'interpréter correctement un protocole décrit dans une java.net.URL.

Pour montrer l'utilité de ce mécanisme dans un cas de programmation orientée par les tests, je vais prendre l'exemple de l'écriture d'une classe permettant de géocoder une adresse à travers l'API Google Maps.
Cette API est accessible à travers le protocole HTTP. Pour ne pas solliciter constamment le service web nous allons donc mocker l'appel en se basant sur la documentation du service.

Remplacer le composant HTTP standard

Lors de l'appel à java.net.URL.openStream() la JVM interprète le protocole contenu dans l'URL. Si c'est "http" la JVM instanciera par défaut une sun.net.www.protocol.http.HttpURLConnection qui gèrera le dialogue avec le serveur. En définissant la propriété java.protocol.handler.pkgs avec un nom de package, la JVM introspectera ce package suivi du nom du protocole et de la classe nommée Handler et devant étendre java.net.URLStreamHandler.

Cela se traduit par le code suivant dans notre test unitaire :

@BeforeClass
public static void setupMockHTTP() {
    System.setProperty("java.protocol.handler.pkgs", "fr.free.lemerdy.sebastian.mock");
}

Et par les deux classes suivantes dans le package fr.free.lemerdy.sebastian.mock.http :

package fr.free.lemerdy.sebastian.mock.http;

import java.io.IOException;
import java.net.URL;
import java.net.URLStreamHandler;

public class Handler extends URLStreamHandler {

    @Override
    protected URLConnection openConnection(URL url) throws IOException {
        return new URLConnection(url);
    }

}
package fr.free.lemerdy.sebastian.mock.http;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

public class URLConnection extends java.net.URLConnection {

    protected URLConnection(URL url) {
        super(url);
    }

    @Override
    public void connect() throws IOException {
        connected = true;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        if (!connected) {
            connect();
        }
        return this.getClass().getResourceAsStream(url.getPath());
    }

}

Cette seconde classe ouvre un fichier dont le chemin dans le classpath est la même que celle de l'URL.

Mise en oeuvre dans l'exemple

Maintenant qu'on a remplacé l'envoi d'une requête HTTP par l'ouverture d'un fichier du classpath on peut donc tester notre service dont voici un extrait du code :

public static double[] geocode(String address) {
    double[] coordinates = null;
    if (address != null && !address.isEmpty()) {
        try {
            address = URLEncoder.encode(address, Charset.defaultCharset().name());
            final URL url = new URL("http://maps.googleapis.com/maps/api/geocode/xml?address=" + address + "&sensor=false");
            final XPath xPath = XPathFactory.newInstance().newXPath();
            XPathExpression xPathExpression = xPath.compile("/GeocodeResponse/result/geometry/location/lat|/GeocodeResponse/result/geometry/location/lng");
            final NodeList location = (NodeList) xPathExpression.evaluate(new InputSource(url.openStream()), XPathConstants.NODESET);
            if (location != null) {
                coordinates = new double[2];
                for (int i = 0; i < 2; i++) {
                    if (location.item(i) != null) {
                        coordinates[i] = Double.parseDouble(location.item(i).getTextContent());
                    }
                }
            }
        } catch (UnsupportedEncodingException e) {
        } catch (MalformedURLException e) {
        } catch (IOException e) {
        } catch (XPathExpressionException e) {
        }
    }
    return coordinates;
}

Enfin voici le test du code ci-dessus :

@Test
public void testGeocode() {
    assertThat(GeocodeService.geocode("1600 Amphitheatre Parkway, Mountain View, CA"), is(new double[] { 37.4217550d, -122.0846330d }));
}

Puisque le code à tester se connecte à l'URL http://maps.googleapis.com/maps/api/geocode/xml il suffit donc maintenant de créer un fichier nommé xml dans le package maps.api.geocode et dont le contenu sera envoyé lors de l'éxécution du test. Ce fichier XML pourra donc ressembler à ceci :

<GeocodeResponse>
    (...)
    <result>
    (...)
        <geometry>
            <location>
                <lat>37.4217550</lat>
                <lng>-122.0846330</lng>
            </location>
            (...)
        </geometry>
        (...)
    </result>
    (...)
</GeocodeResponse>

vendredi 4 février 2011

Tester une expression régulière

J'ai découvert assez récemment une nouvelle méthode de JUnit 4 :

public static <T> void org.junit.Assert.assertThat(T actual, org.hamcrest.Matcher<T> matcher)

Elle permet d'écire des tests assez lisibles du genre :

assertThat("toto", notNullValue());
assertThat("toto", is("toto"));

Le propos ici est d'utiliser un Matcher spécifique qui valide que la valeur correspond au Pattern

Exemple :

assertThat("01 02 04 05 06", matchPattern(Pattern.compile("\\d{2}(?:\\s\\d{2}){4}"));

Voici donc le code de la classe de Matcher :

import java.util.regex.Pattern;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;

public final class MatchPattern extends BaseMatcher<String> {

    private final Pattern pattern;
    
    private MatchPattern(final Pattern pattern) {
        if (pattern == null) {
            throw new IllegalArgumentException("pattern cannot be null");
        }
        this.pattern = pattern;
    }
    
    @Override
    public boolean matches(Object arg0) {
        return pattern.matcher((CharSequence) arg0).matches();
    }
    
    @Override
    public void describeTo(Description arg0) {
        arg0.appendText("String that matches pattern \"").appendText(pattern.pattern()).appendText("\"");
    }
    
    @Factory
    public static Matcher matchPattern(final Pattern pattern) {
        return new MatchPattern(pattern);
    }
    
}

mercredi 17 novembre 2010

Déterminer les dépendances d'un projet Maven

Rappel de l'importance des dépendances dans un projet

Il fut un temps où gérer les dépendances d'un projet JEE consistait à copier tous les jars téléchargés au fil de l'eau depuis sourceforge vers le répertoire magique de son application web : le fameux WEB-INF/lib. Le temps où les ClassNotFoundException ou les NoSuchMethodError pouvaient arriver à tout moment (ou presque). Comment gérait-on les migrations des librairies tierces ? Comment déterminions-nous le degré de dépendance de notre code avec les frameworks ? Mon avis est qu'on ne s'en souciait peu – Java ayant une forte tradition de non-régression dans ses évolutions.

Tout le monde s'accorde pour dire que la puissance de la plateforme Java est justement la multitude de librairies tierces et la réactivité potentielle d'une communauté très présente. Mais ce foisement implique de fait une gestion plus rationnelle des liens de dépendances tissées entre ces différents produits. C'est entre autre ce que maven essaie de faire en mettant en place des conventions dans la construction et le packaging des librairies.

Nous allons donc voir comment maven peut nous aider à optimiser voire minimiser les dépendances afin de consommer moins d'espace mémoire dans la JVM pendant l'exécution d'un programme.

Les niveaux de dépendances définis par maven

Dans tout langage il existe plusieurs niveaux de dépendance. Nous allons voir les principaux niveaux que maven défini pour java :

  • Le premier niveau est le plus important : compile. Si une dépendance n'est pas satisfaite à ce niveau, le projet ne compile pas.
  • Le second niveau permet justement d'éviter les ClassNotFoundException et les NoSuchMethodError citées plus haut : runtime. Il indique que certaines librairies tout comme votre projet peut invoquer du code de manière dynamique (c'est la réflexion). C'est le cas de Spring-context par exemple qui essaie d'instancier des classes sur la base d'une chaîne de caractères qu'il aura parsé dans un fichier XML.
  • Les troisième et quatrième niveaux ont été introduits suite aux différentes absobtions de librairies vraiment centrales dans le cœur de la plateforme ou bien dans le classpath global d'une JVM : system et provided. Ce niveau ne nous intéresse pas vraiment. Enfin le dernier niveau permet de définir des dépendances qui ne sera jamais embarqués dans les applications de production mais sont nécessaires pour tester le logiciel : test. Le meilleur exemple de ce niveau est bien entendu la librairie JUnit.

Pour chaque librairie (appelons-les artifact), ces dépendances permettent de construire un graphe. Ce graphe permet de gérer de manière cohérente et reproductible les classpath lors de la compilation, les tests, la construction et l'exécution de l'artifact. L'ensemble des dépendances contenues dans le graphe sont déterminées en résolvant les dépendances de chacuns des artifacts récursivement en commençant par l'artifact initial et cela récursivement jusqu'à ce qu'il ne reste plus aucune dépendance transitive à résoudre.

exemple de graphe de dépendances

À la main

Commencer par supprimer toutes les dépendances (les mettre en commentaires dans le pom). Le scope le plus proche du code étant compile, la première étape est donc de pouvoir faire compiler le projet. Toute dépendance qui n'aide pas à la compilation doit avoir un scope différent de compile ou supprimée si elle ne sert à rien.

Une fois que le projet compile on doit pouvoir valider le scope runtime. Pour cela deux solutions plus ou moins élégantes :

  1. Soit votre projet dispose d'une bonne couverture de tests unitaires et l'exécution de ces tests vous permet de valider qu'un maximum d'instanciations dynamiques seront réussies.
  2. Soit votre projet ne dispose pas de tests unitaires auquel cas vous devrez exécuter le projet dans un environnement proche de la production. Vous noterez qu'en fonction de l'ordre avec lequel vous ajoutez au fur et à mesure vos dépendances il se peut que vous puissiez oublier des dépendances de niveau compile (utilisées directement dans votre code) mais résolues par une dépendance transitive. Or cela est problématique car rien ne vous garantie que la dépendance transitive ne soit pas un jour supprimée par la librairie qui la porte.
À l'aide d'outils

Maven dispose d'un outil très utile pour minimiser les dépendances inutiles mais aussi pour garantir une bonne qualité des dépendances de niveau compile : il s'agit de maven-dependency-plugin.

Placez-vous dans un projet mavenisé et exécutez le goal suivant :
mvn dependency:analyze

L'exécution doit vous indiquer :

  • les dépendances de niveau compile qui ne sont pas déclarées dans votre projet
  • Les dépendances qui sont déclarées mais qui ne servent pas à la compilation

Ces informations vous seront très utiles pour optimiser les dépendances. Exemple pour un projet fwk :

[INFO] [dependency:analyze {execution: default-cli}]
[WARNING] Used undeclared dependencies found:
[WARNING]    org.springframework:spring-core:jar:2.0.8:compile
[WARNING]    org.hibernate:hibernate-core:jar:3.3.0.SP1:compile
[WARNING]    org.springframework:spring-dao:jar:2.0.8:compile
[WARNING]    aopalliance:aopalliance:jar:1.0:compile
[WARNING]    org.springframework:spring-beans:jar:2.0.8:compile
[WARNING]    org.apache.axis:axis-jaxrpc:jar:1.4:compile
[WARNING]    commons-logging:commons-logging:jar:1.0.4:compile
[WARNING]    org.hibernate:ejb3-persistence:jar:1.0.2.GA:compile
[WARNING] Unused declared dependencies found:
[WARNING]    javax.faces:jsf-impl:jar:1.2_04-p02:compile
[WARNING]    org.slf4j:slf4j-api:jar:1.5.8:compile
[WARNING]    org.springframework:spring-jpa:jar:2.0.8:compile
[WARNING]    net.sf.ehcache:ehcache:jar:1.5.0:compile
[WARNING]    org.hibernate:hibernate-validator:jar:3.1.0.GA:compile
[WARNING]    javax.servlet:jstl:jar:1.2:compile
[WARNING]    com.sun.facelets:jsf-facelets:jar:1.1.11:compile
[WARNING]    org.slf4j:slf4j-simple:jar:1.5.8:compile
[WARNING]    commons-collections:commons-collections:jar:3.2:compile
[WARNING]    org.slf4j:slf4j-log4j12:jar:1.4.2:test
[WARNING]    org.springframework.security:spring-security-acl:jar:2.0.4:compile
[WARNING]    javax.persistence:persistence-api:jar:1.0:provided
[WARNING]    org.hibernate:hibernate-entitymanager:jar:3.4.0.GA:compile

Dans ce rapport on voit que 8 dépendances sont utilisées par le code source sans être déclarées (le code compile car il se repose sur les dépendances transitives). 13 dépendences sont déclarées alors qu'elles ne sont pas utilisées par le code source. Il faut toutefois nuancer ce deuxième résultat : en effet seule le niveau compile est intéressant pour cette mesure. Le niveau provided ou test reste dans bien des cas utile à déclarer.

mardi 10 août 2010

Restaurer son iPhone 3G en OS 3.1.3

Je viens de suivre cette procédure. Ça a fonctionné parfaitement pour moi.

capture d'écran de iTunes montrant le numéro de version de l'iPhone

J'apporte quelques précisions par rapport à l'article :

  1. J'ai téléchargé l'os 3.1.3 grâce à cette page. Le md5 du fichier est 82e93e4e51b0e4503a8165507b8a3df2.
  2. Une fois dézippé, RecBoot propose deux binaires. Il faut évidemment exécuter RecBoot exit only.
  3. Ajouter les droits d'écriture de mon compte sur /etc/hosts n'a pas suffit à pouvoir l'enregistrer après modifications avec TextEdit. J'ai dû faire un $ sudo vi /etc/hosts...
  4. Une fois l'iPhone débloqué j'ai supprimé cette entrée dans mon fichier hosts sans que cela ne pose problème.


J'espère que ces indications supplémentaires vous permettrons enfin de récupérer votre iPhone 3G dans le meilleur de sa forme. J'attends maintenant une mise à jour de iOS 4 en tenant compte du processeur et de la RAM vieillissants de mon iPhone 3G.

Cette expérience de mise à jour m'aura au moins appris à ne pas faire une confiance aveugle dans les logiciels d'Apple - confiance qu'ils avaient pourtant su gagner assez facilement lors de mon passage sur Mac OS X.

mardi 20 juillet 2010

Utiliser java.util.logging dans Tomcat avec Eclipse.

Si vous êtes devenus fans de java.util.logging (parce que c'est embarqué dans la jre, parce qu'il n'y a pas besoin de librairies supplémentaires, parce que les messages sont formatés à base de java.text.MessageFormat, etc.) alors vous serez peut-être surpris de ne pas voir une seule ligne de log s'afficher lors de l'exécution de votre application dans un Tomcat lancé par Eclipse. D'autant plus surpris que la documentation (la version 6.0 en l'occurrence) précise bien que :
  1. tomcat utilise en interne java.util.logging comme implémentation de sa couche d'abstraction du système de log (commons-logging)
  2. il est tout-à-fait possible grâce à JULI de configurer ses logs par classpath d'application et non par JVM (énorme défaut de java.util.logging reconnaissons-le)
Voici la solution à mettre en œuvre dans Eclipse afin de pouvoir configurer java.util.logging uniquement pour une application - l'autre méthode aurait évidemment été de modifier le fichier %CATALINA_BASE%\conf\logging.properties mais avouons que c'est moins séduisant.

Pré-requis :
  1. Eclipse (je n'ai testé qu'avec Helios)
  2. Apache Tomcat (je n'ai testé qu'avec Apache Tomcat v6.0)
  3. Une configuration de ce serveur dans Eclipse.
Marche à suivre

Dans Eclipse cliquer sur le menu Run > Run Configurations…. Sélectionner l'onglet Apache Tomcat > nom de la configuration du serveur. Cliquer sur l'onglet Arguments et ajouter cette chaîne à la fin de la zone de texte VM arguments :

-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager

Cliquer sur les boutons Apply puis Run ou Close.

Conclusion

Vous pouvez maintenant joyeusement configurer les logs de votre application dans un fichier logging.properties bien formé et placé à la racine d'un dossier source java. L'essentiel étant qu'il se retrouve par la suite dans le fameux répertoire /WEB-INF/classes de votre war. Plus d'infos sur la documentation officielle.

Source : Ligne 149 de apache-tomcat-6.0.26/bin/catalina.bat
set LOGGING_MANAGER=-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager

mardi 25 mai 2010

Authentification SSH sans mot de passe

Ca fait plusieurs semaines que j'essayais de faire interagir deux machines sous linux avec SSH sans succès. Je vous conseille tout particulièrement cet article qui vient de me sauver la vie. Il mérite amplement son page rank dans Google !

vendredi 5 février 2010

Erreur dans l'implémentation d'IBM de la spécification portlet

Le contexte est le suivant : le client a besoin de lancer des applications Citrix depuis un portail d'entreprise.

Pour lancer une application Citrix depuis un navigateur web, on doit répondre à une requête HTTP en renvoyant le contenu d'un fichier ICA (c'est ce fichier qui décrit où et comment accéder à l'application Citrix) avec l'entête MIME application/x-ica. Afin de ne pas perdre la page web qui aura permis à l'utilisateur de cliquer sur le lien, on introduit une iframe cachée est c'est à l'intérieur de cette iframe que le fichier ICA est renvoyé (attribut target du lien).

Lorsqu'on est dans un portail, l'unité de base est la portlet. Par définition, la première mouture de la spécification portlet ne pouvait renvoyer qu'un fragment de page. Or depuis la seconde mouture de la spécification il est possible de fournir des réponses entières : c'est la notion de requête/réponse de type ressource (auparavant nous ne disposions que des requêtes/réponses de type action et de type vue). Cette typologie de requêtes/réponses est tout particulièrement adaptée à des requêtes asynchrones. On peut partir du postulat que l'iframe cachée est un moyen d'effectuer une requête asynchrone car la page ayant chargée le lien et l'iframe reste affichée dans le navigateur.

Cette longue introduction terminée, je vais pouvoir aborder le point qui pose problème aujourd'hui sur WebSphere Portal 6.1.0.1. La spécification JSR 286 définit ce qui suit :

PLT.19.1 Obtaining a PortletRequestDispatcher
(...) If no resource can be resolved based on the given path or name the methods must return null.
PLT.19.3.7 Error Handling
If the servlet or JSP that is the target of a request dispatcher throws a runtime exception or a checked exception of type IOException, it must be propagated to the calling portlet. All other exceptions, including a ServletException, must be wrapped with a PortletException. The root cause of the exception must be set to the orginial exception before being propagated.

Ces deux points ne sont pas du tout respectés dans le cadre de la méthode serveResource évoquée plus haut. Le code suivant ne retourne pas null :

getPortletContext().getRequestDispatcher("/fichierJspInexistant.jsp");

D'autre part si l'exécution de index.jsp provoque une Exception, l'appel au code suivant ne retourne pas de PortletException :

getPortletContext().getRequestDispatcher("/index.jsp").include(request, response);

C'est plutôt gênant dans mon cas car j'aimerais pouvoir m'assurer que la ressource avec laquelle je veux déléguer la génération de la vue existe et que le traitement s'effectue correctement.

samedi 7 novembre 2009

Générateur de mots de passe avec Calc d'OpenOffice

Comment générer un mot de passe aléatoirement à l'aide de Calc d'OpenOffice ? C'est ce que l'article explique.

Lire la suite

mercredi 3 juin 2009

Chronograph

Directement inspiré d'un article écrit par Éric, voici un petit logiciel qui vous permettra de mesurer votre activité.

Lire la suite

lundi 1 juin 2009

java.beans.XMLEncoder et java.beans.XMLDecoder ne sont pas parfaits

On pourrait penser que ces deux classes permettent de mettre en place une fonction bijective (en informatique, ça se traduit par le fait qu'il n'y ait pas de perte de données dans l'encodage ou le décodage). Or il semblerait que ça ne soit pas le cas avec une machine virtuelle java 1.5.0_16.

Lire la suite

mardi 22 juillet 2008

Retour d'expérience sur l'iPhone 3G

Peut être que mon expérience sur l'achat et le mise en route de l'iPhone servira à de futurs heureux possesseurs du smart phone ? C'est pourquoi je déroge à la règle que tout bloggeur devrait suivre : ne pas raconter sa vie sur internet...

Lire la suite

lundi 2 juin 2008

Intégration d'Orbeon Forms dans Liferay

Orbeon Forms est une application web permettant d'exécuter et de mettre en forme des formulaires respectant la norme XForms du W3C dans un navigateur supportant simplement le HTML. Le produit est livré avec une compatibilité avec Liferay. Cependant, avec la toute dernière version, le déploiement de la portlet pose des petits problèmes. Nous allons expliquer la procédure pour faire fonctionner Orbeon Forms dans Liferay 4.4.2.

Lire la suite

mercredi 28 mai 2008

onclick et commandLink en JSF 1.1

En JSF 1_1_01, l'attribut onclick n'est pas disponible pour ce tag. Cet article démontre comment déclencher un traitement lors du click.

Lire la suite