Si on considère la classe Color qui comporte 2 méthode : une qui change les 2 champs preferedColor et colorOfMyDress avec la même couleur donnée en paramètre et une qui affiche les 2 champs.
public class Color { public String preferedColor; public String colorOfMyDress; public void changeColor(String color) { preferedColor = color; colorOfMyDress = color; } public void printColor() { System.out.println("Ma couleur préferée est le " + preferedColor + " et la couleur de ma robe est " + colorOfMyDress); } }
Imaginons une classe AuroreColor qui ressemble à celle la et que cette classe soit utilisée dans une appli web, et l’on souhaite que tous les utilisateurs puissent modifier la couleur qui est partagée entre tous :
public class AuroreColor { public Color color; ... }
Chaque utilisateur peut changer la couleur via une interface et en réponse, s’affiche le texte affiché par la méthode printColor(). Si Paul choisit et valide la couleur rouge, alors il voit à l’écran : « Ma couleur préferée est le rouge et la couleur de ma robe est rouge. »
Mais si 2 utilisateurs utilisent en même temps cette fonctionnalité, on peut obtenir des résultats « non attendus » comme « Ma couleur préferée est le vert et la couleur de ma robe est jaune », alors qu’on s’attends à ce que les 2 champs soient identiques.
En effet, il est (entre autre) possible que au même moment, les threads associés à chacun des utilisateurs soient au même moment au sein de la méthode changeColor, impliquant que pour un laps de temps très court, les 2 paramètres sont modifiés et ont des valeurs différentes. C’est particulièrement visualisable si on reprends l’exemple d’un counter :
public class Counter {
double counter; public void increment() { counter++; } public double getCounter() { return counter; } }
On essaie de compter le nombre de mots d’un livre mais on se rends compte que le résultat n’est pas toujours correct. La cause est assez simple : counter++ marche de la façon suivante :
– Je récupère la valeur de counter
– J’ajoute 1
– J’affecte le résultat à counter
Si 2 threads accèdent l’un après l’autre à ce code, pas de problème. Par contre, si 2 threads appelent la méthode incrémente dans un laps de temps très court, alors il est possible qu’ils utilisent la même valeur de counter lors de l’incrémentation, alors, chacun va faire un +1, sur la même valeur et réassignée cette valeur à counter. Ainsi, on aura au final, counter = 1 alors que l’un après l’autre, on voit bien counter = 2.
En séquence on aura :
Thread 1 :
Valeur de counter initial : 0
Valeur après incrémentation : 1
Thread 2 :
Valeur de counter initial : 1
Valeur après incrémentation : 2
Soit un résultat final de 2
En parrallèle, on peut avoir :
Thread 1 :
Valeur de counter initial : 0
Valeur après incrémentation : 1
Thread 2 :
Valeur de counter initial : 0
Valeur après incrémentation : 1
Soit un résultat final de 1
Il y a plusieurs manières de résoudre ce problème. L’un d’elle serait de garantir que 2 threads ne puissent pas accéder en même temps à la méthode changeColor (ou increment) en utilisant le mot clé synchronized sur cette méthode . Si on reprends les explications pour une méthode synchronized :
- First, it is not possible for two invocations of synchronized methods on the same object to interleave. When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object.
- Second, when a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent invocation of a synchronized method for the same object. This guarantees that changes to the state of the object are visible to all threads.
Pour faire plus simple, le mot clé synchronized sur une méthode garantit que cette dernière ne sera exécutée qu’un par au plus un thread, les autres attendant que le premier ait finit.
public class Color { public String preferedColor; public String colorOfMyDress; public synchronized void changeColor(String color) { preferedColor = color; colorOfMyDress = color; } public void printColor() { System.out.println("Ma couleur préferée est le " + preferedColor + " et la couleur de ma robe est " + colorOfMyDress); } }
En modifiant de la sorte, 2 threads ne pourront jamais modifier en même temps les couleurs, mais seulement l’un après l’autre. Mais il y a un mais, car cela ne marche pas et cela fait partie des erreurs que l’on retrouve souvent
]]>
Déjà eu envie de faire de l’open-source mais ne sachant pas comment débuter ? Vous avez un projet open-source et cherchez un coup de main ? Quelque soit votre situation, le Hackergarten qui aura lieu mercredi après midi lors de Devoxx France est LE lieu où il faut être, vous y trouverez des commiteurs qui vous accompagnerons pour faire votre premier commit open-source sur un des projets représentés ! Organisé par Brice Dutheil et Mathilde Lemée, c’est un bon moyen de progresser et de se rendre utile.
Hackergarten c’est le rendez-vous des gens qui veulent participer aux projets opensource. L’idée c’est, dans un format de 3h, de contribuer un logiciel, un fix, un feature, une documentation dont d’autres pourraient avoir l’usage. Il s’articule autour de commiters actifs pour mentorer les hackers qui participent à l’évènement.
Bref que du bon.
Voilà la liste des projets qui seront représentés :
La liste n’est pas fixée, commiters, vous pouvez vous ajouter ici ! Il vous faudra venir avec votre portable et n’hésitez pas à regarder les pré-requis pour pouvoir démarrer plus rapidement !
]]>
Premièrement, ajouter un champ input acceptant l’ajout de plusieurs fichiers :
<input id="files" multiple="multiple" name="file[]" type="file">
Ensuite, uploadons les fichiers en utilisant les « FormData » d’HTML5 :
var upload = function (file) { var data = new FormData(); data.append('name', file.name); data.append('file', file); $.ajax({ url:'/photo', data:data, cache:false, contentType:false, processData:false, type:'POST' }).error(function () { alert("unable to upload " + file.name); }) .done(function (data, status) { doSomethingUseful(data); }); }; function multiUpload(files) { for (var i = 0; i < files.length; i++) { // Only upload images if (/image/.test(files[i].type)) { upload(files[i]); } } } $(document).ready(function () { $("#files").change(function (e) { multiUpload(e.target.files); }) });
Coté serveur, avec Jersey, il faut inclure le module « multipart » :
<dependency> <groupId>com.sun.jersey.contribs</groupId> <artifactId>jersey-multipart</artifactId> <version>1.13</version> </dependency>
Ensuite le code est plutôt simple :
import javax.inject.Inject; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import com.sun.jersey.core.header.FormDataContentDisposition; import com.sun.jersey.multipart.FormDataParam; import java.io.IOException; import org.apache.log4j.Logger; import org.springframework.stereotype.Controller; @Controller @Path("/photo") public class PhotoResource extends AbstractResource { private static final Logger LOG = Logger.getLogger(PhotoResource.class); @Inject private FileRepository fileRepository; @GET @Produces("image/png") @Path("/{photoId}") public byte[] photo(@PathParam("photoId") String photoId) { try { return fileRepository.get(photoId); } catch (IOException e) { LOG.warn("When get photo id : " + photoId, e); throw ResourceException.notFound(); } } @POST @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_PLAIN) public String addPhoto(@FormDataParam("file") byte[] photo, @FormDataParam("file") FormDataContentDisposition fileDetail) { String photoId = null; try { photoId = fileRepository.save(photo); } catch (IOException e) { LOG.error("unable to add photo", e); throw ResourceException.error(e); } return photoId; } }
Et pour s’amuser, stockons les fichiers dans Mongodb grace à GridFS :
import javax.annotation.PostConstruct; import javax.inject.Inject; import com.mongodb.gridfs.GridFS; import com.mongodb.gridfs.GridFSInputFile; import java.io.IOException; import org.apache.commons.io.IOUtils; import org.bson.types.ObjectId; import org.jongo.Jongo; import org.springframework.stereotype.Repository; @Repository public class FileRepository { private Jongo jongo; private GridFS files; @Inject public FileRepository(Jongo jongo) { this.jongo = jongo; } @PostConstruct public void afterPropertiesSet() throws Exception { files = new GridFS(jongo.getDatabase()); } /** * Save a file and return the corresponding id */ public String save(byte[] file) { GridFSInputFile savedFile = this.files.createFile(file); savedFile.save(); return savedFile.getId().toString(); } /** Return the file */ public byte[] get(String fileId) throws IOException { return IOUtils.toByteArray(files.findOne(new ObjectId(fileId)).getInputStream()); } } }
Et si vous voulez faire du drag and drop, il suffit d’inclure ce plugin jQuery : drop.js et de faire comme ceci :
$(document).ready(function () { $('body').dropArea(); $('body').bind('drop', function (e) { e.preventDefault(); e = e.originalEvent; multiUpload(e.dataTransfer.files); }); });
Sources :
]]>Wikipédia :
Un BarCamp est une rencontre d’un week-end, une non-conférence ouverte qui prend la forme d’ateliers-événements participatifs où le contenu est fourni par les participants qui doivent tous, à un titre ou à un autre, apporter quelque chose au Barcamp.
C’est le principe pas de spectateur, tous participants. L’événement met l’accent sur les toutes dernières innovations en matière d’applications Internet, de logiciels libres et de réseaux sociaux.
Si vous souhaitez venir, il suffit de s’inscrire ici : http://barcamp.org/w/page/54826845/JavaCampParis
Il y a également un plan précis pour s’y retrouver !
Du coup la documentation de Redis est simple. Un autre truc que j’aime c’est qu’elle spécifie la complexité de chaque opération, ce qui permet au développeur de vérifier à chaque fois que la commande qu’il s’apprête à utiliser n’est pas trop gourmande. De plus, il n’a pas le risque d’oublier de mettre un index sur un champ (combien de fois cela arrive en SQL ou avec MongoDb ! ) car toutes les clés sont par définition « indexées » dans une (Hash) Map.
Simple et (donc ?) performant. Redis est sans doute ce qu’il y a de plus performant en terme de base de données. Nous avons donc intérêt à nous y intéresser avant de chercher des solutions plus complètes et donc plus complexes et moins performantes.
Sauf que, Redis ne stocke que des chaînes de caractères. Comment faire pour stocker nos objets métiers complexes ?
On pourrait simplement utiliser une base de données SQL pour stocker notre modèle et utiliser un ORM pour que ce soit « simple ». Ensuite dans Redis on ne stockerai que la dé-normalisation de certaines requêtes : 3 meilleurs clients => { 1 => id:130 ; 2 => id:345 ; 3 => id:456 } Ainsi la requête pour récupérer les identifiants des trois meilleurs clients se fera en temps constant O(1) puis la requête pour récupérer les données des trois clients dans la base SQL se fera également en temps constant, car les clés primaires sont indexées dans les bases SQL.
Mais il faut avouer que c’est embêtant de devoir gérer deux bases de données, surtout quand on pourrait simplement sérialiser les objets directement dans Redis.
Dans notre exemple, le besoin est de pouvoir insérer des liens dans des listes et de pouvoir récupérer en temps constant tous les liens d’une liste donnée.
Jackson est un sérialiseur Objet -> JSON. Ce qui est parfait pour pouvoir lire facilement les objets sérialisé où pour pouvoir les utiliser directement en JavaScript, sans passer par une dé-sérialisation.
On va utiliser JAX-B pour annoter nos objets Java, par exemple :
@XmlRootElement(name = "links") @XmlAccessorType(FIELD) public class BannerLink { private String label; private String url; public BannerLink(String label, String url) { this.label = label; this.url = url; } public String getLabel() { return label; } public String getUrl() { return url; } protected BannerLink(){} }
Notez que Jackson sait aussi sérialiser des POJOs (sans JAX-B mais avec Setter)
Ensuite il suffit de sérialiser l’objet avec Jackson pour l’insérer dans une liste Redis.
AnnotationIntrospector introspector = new JaxbAnnotationIntrospector(); mapper.getDeserializationConfig().setAnnotationIntrospector(introspector); mapper.getSerializationConfig().setAnnotationIntrospector(introspector);
jedis.rpush("listKey", mapper.writeValueAsString(link));
Pour la lecture, il suffit de dé-sérialiser les objets de la liste.
mapStringsToLinks(jedis.lrange(key, 0, jedis.llen(key))); private List mapStringsToLinks(List jedisResult) { return Lists.transform(jedisResult, toBannerLink()); } private Function<String, BannerLink> toBannerLink() { return new Function<String, BannerLink>() { @Override public BannerLink apply(@Nullable String link) { return mapper.readValue(link, BannerLink.class); } }; }
Je ne sais pas vous, mais moi je trouve ça vraiment plus simple de persister directement et simplement les instances d’objets, telle quelle, sans se prendre la tête avec du mapping, jointure ou autre joyeuseté.
Je n’ai pas fait le test avec une base SQL et un ORM de type Hibernate mais n’hésitez pas à forker le code et à le faire, ça m’intéresse.
Aucun tuning n’a été fait sur les bases de données. Mongo est bien meilleur en écriture et peut être encore meilleur, car il ne garantie pas que les données sont effectivement écrites sur le disque, j’avoue ne pas avoir cherché à optimiser Redis pour l’écriture, j’ai gardé la configuration par défaut.
En revanche, Redis est bien meilleur en lecture, malgré le surplus de traitements dû à la désérialisation Jackson. Et c’est ce qu’on cherche dans notre cas d’usage, nos listes de liens vont être lus beaucoup plus souvent que modifiées. Et pour ceux qui se poseraient la question, oui j’ai bien créé les indexes dans Mongo.
Ce qu’il faut retenir, c’est que dans bien des cas, une base de données document ou SQL n’est pas forcément obligatoire et qu’il vaut mieux démarrer simple et (très) efficace, quitte à changer par la suite…
Un peu de lecture si vous souhaitez en savoir plus sur Redis : Redis: the Definitive Guide: Data Modeling, Caching, and Messaging
]]>@Configuration public class RequestConfiguration { @Value(value = "${repository?InMemoryRepository}") private String repository; }
Pour ce faire, il faut configurer le property placeholder de Spring pour :
<bean id="placeholderConfig"> <property name="locations"> <list> <value>classpath:conf.properties</value> </list> </property> <property name="ignoreResourceNotFound" value="true"/> <property name="ignoreUnresolvablePlaceholders" value="true" /> <property name="valueSeparator" value="?" /> </bean>
Et voilà !
Maintenant il faut faire attention car Spring va ignorer toutes les properties null ce qui pourra provoquer des NullPointerException à l’exécution plutôt que des erreurs de configuration au démarrage de l’application…
]]>Avec Jersey, l’implémentation de référence de JAX-RS (JSR311) pas de prise de tête, tout est générés automatiquement.
Il suffit de faire deux choses :
public class SchemaGenConfig extends WadlGeneratorConfig { @Override public List configure() { return generator( WadlGeneratorJAXBGrammarGenerator.class).descriptions(); } }
<servlet> <servlet-name> banner </servlet-name> <servlet-class> com.lateralthoughts.commons.web.LateralCommonsServlet </servlet-class> <init-param> <param-name> com.sun.jersey.config.property.WadlGeneratorConfig </param-name> <param-value> com.lateralthoughts.commons.web.wadl.SchemaGenConfig </param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>
Le WADL est maintenant généré et accessible à cette adresse : http://localhost:port/maservlet/application.wadl
Pour aller plus loin, le wiki d’Oracle : https://wikis.oracle.com/display/Jersey/WADL
Merci à Aurélien Thieriot pour l’astuce
]]>Pour me préparer aux sélections de code story et pouvoir coder pendant 2 jours une application devant des centaines de développeurs à Devoxx, je me suis entrainé à refactorer une méthode en m’enregistrant.
Le bénéfice que j’attendais de l’exercice était de :
Pour la petite histoire, je suis partie d’une classe que je venais de refactorer chez un client, il s’agit donc d’un exemple réel. J’ai fait environ 15 essais avant les sélections pour finalement me faire éliminer ! Je suis bon perdant, et je me suis dit que les quelques techniques simples que j’explique pouvaient être intéressantes et que c’était dommage de garder le screencast pour moi. Du coup j’ai refait 5 essais et voici le résultat :
Ce n’est pas parfait ! J’ai même fait une grossière erreur en cassant le comportement de la méthode. Le premier qui trouve où gagne une bière (la date du commentaire faisant foi) ! Si vous trouvez d’autres boulettes ça marche aussi, à l’exclusion de l’utilisation de framework ou de l’API java, Boolean.compareTo par exemple, car ce n’est pas le propos de l’exercice. D’ailleurs s’il y a une chose que je retiens c’est qu’on peut toujours faire mieux !
Pour ceux qui voudrait faire pareil :
Bon code à tous !
]]>En effet, un bloc then permet de déclarer des conditions, des exceptions, des interactions et des définitions de variables là où un bloc expect ne peut contenir que des conditions et des déclarations de variables. L’écriture given/when/then est également plus intuitive dans le cas où vous souhaitez tester des stories. C’est également une des clés du Behavior Driven Development et une méthode saine pour structurer ses tests, qui oblige à réfléchir vraiment à ce que l’on teste. Ce que j’aime chez spock, c’est que c’est obligatoirement intégré via ces blocs, on ne peut pas faire autrement
Spock permet le data-driven testing mais c’est également un framework facilitant la création de bouchons/simulacres [Plus d’infos sur les différences bouchon/simulacre et test d’état/de comportement]. On s’intéresse ici au le test par comportement, c’est à dire qu’on va s’occuper des chainages des appels des méthodes entre elles et moins du résultat. On cherche alors à vérifier que l’appel à spockResource.findCalendarByDay(‘1’) entraîne bien un unique appel à calendarDao.getInfosByDay(‘1’).
def "test par comportement"() { given: def calendarDao = Mock(CalendarDao) def spockResource = new SpockResource(calendarDao) when : spockResource.findCalendarByDay("1") then : 1 * calendarDao.getInfosByDay("1") }
Le bloc given permet de définir les variables nécessaires à l’exécution du test. Ici, on bouchonne le dao que l’on affecte ensuite au service que l’on souhaite tester. Le bloc when correspond à l’appel de la méthode à tester.
Le bloc then comporte ici uniquement la condition à tester. La syntaxe veut dire on vérifie que la méthode calendarDao
.getInfosByDay est appelée uniquement une fois (1 *) avec le paramètre ‘1’. Les paramètres sont évalués via l’appel à la méthode equals.
A la différence d’EasyMock, qui fonctionne par défaut avec des simulacres, Spock comme Mockito renvoie de base pour toutes les méthodes mockées sans spécification null ou 0 ou false. Ici par exemple, l’appel à
calendarDao .getInfosByDay("1")
renverra null. Pour spécifier une valeur de retour différente, il suffit d’utiliser la syntaxe suivante :
calendarDao .getInfosByDay(_) >> new SpockInfo("1");
Le même code avec EasyMock avec une valeur de retour :
@Test public void testEasyMock() { //given CalendarDao calendarDao = createNiceMock(CalendarDao.class); SpockResourcespockResource = new SpockResource( calendarDao); expect(calendarDao.getInfosByDay("1")).andReturn( new SpockInfo("1")); replay(calendarDao); //when SpockInfo spockInfo= spockResource.findCalendarByDay("1"); //then verify(calendarDao); }
Avec EasyMock, on annote la méthode par @Test [annotation JUnit] puis on crée le mock à l’aide de EasyMock.createNiceMock, pour avoir un mock lénient. On précise ensuite que l’on s’attend à ce que la méthode calendarDao.getInfosByDay(‘1’) retourne l’objet new SpockInfo(‘1’) avec expect(calendarDao.getInfosByDay(‘1’)).andReturn(new SpockInfo(‘1’)); . On ‘charge’ ensuite les mocks via le replay et à la ligne suivante on lance l’appel à la méthode testée. Le verify à la dernière ligne permet de vérifier qu’un unique appel à la méthode a bien été effectué.
L’inconvénient de l’utilisation des simulacres, c’est que les tests et le code testé sont très (trop!) liés. Ainsi une modification du code peut entraîner beaucoup de refactoring au niveau des tests sans valeur ajouté. L’exemple le plus marquant est la séparation d’une méthode en deux méthodes distinctes : il faut reprendre tous les simulacres alors que ce n’est qu’une modification de ‘clarté’. Il est donc souvent préférable de ne pas tester le comportement mais uniquement le résultat à chaque fois que cela est possible et judicieux, c’est à dire du faire du test sur l’état des objets.
def "test stub avec retour"() { given: def calendarDao = Mock(CalendarDao) def spockResource = new SpockResource(calendarDao) when : def spockInfo = spockResource.findCalendarByDay("1") then : calendarDao .getInfosByDay("1") >> new SpockInfo("1"); spockInfo.day == 1 }
Ici, on ne fait plus de contrôle sur le nombre d’appel à la méthode getInfosByDay(‘1’). On indique juste que lorsque cette méthode est appelée, on renvoie (>>) une nouvelle instance de SpockInfo. Ici pas de assertEquals ou autre méthode du genre, le spockInfo.day==1 est en fait un raccourci pour écrire assert spockInfo.day == 1.On vérifie que la variable day de l’objet spockInfo est bien égale à 1.
Voilà le code équivalent avec Mockito :
@Test public void testMockito() { //Given CalendarDao calendarDao = mock(CalendarDao.class); SpockResource spockResource = new SpockResource( calendarDao); when(calendarDao.getInfosByDay("1")).thenReturn( new SpockData("1")); //when SpockData spockData= spockResource.findCalendarByDay("1"); //then assertEquals("1", spockData.getDay()); }
A la première ligne, on construit le bouchon calendarDao que l’on affecte à la ligne suivante à l’objet spockResource. On indique au bouchon calendarDao que quand la méthode getInfosByDay est appelée avec le paramètre ‘1’, alors elle retourne new SpockData(‘1’). On effectue ensuite l’appel à la méthode testée spockResource.findCalendarByDay(‘1’) et on vérifie que la variable day du résultat spockData est bien égale à 1.
Et si on veut chainer les retours ?
Il est parfois nécessaire de renvoyer des valeurs différentes pour une même méthode bouchonnée. Pour les 3 cas suivants, le premier appel à la méthode calendarDao.getInfosByDay avec le paramètre ‘1’ renverra SpockInfo(‘1’), le deuxième new SpockInfo(‘2’) :
Avec Easymock :
expect(calendarDao.getInfosByDay("1")).andReturn(new SpockInfo("1")).andReturn(new SpockInfo("2"))
Avec Mockito :
when(calendarDao.getInfosByDay("1")).thenReturn(new SpockData("1")).thenReturn(new SpockInfo("2"))
Avec Spock :
calendarDao.getInfosByDay("1") >>> [new SpockInfo("1"),new SpockInfo("2")];
Le prochain article abordera les fonctionnalités plus avancées de la gestion des arguments des fonctions mockées (ArgumentMatcher) également dans les trois frameworks.
Crédit Photo : Mistinguette18
]]>Ce qui suit est tiré de la conférence de David Gageot [ @dgageot / http://javabien.net ] à SoftShake 2010.
Sur une vingtaine de personnes présentes dans la salle, les durées de build vont de quelques minutes à plusieurs heures. David présente brièvement quelques frameworks comme Infinitest et JUnitMax. Ce sont des plugins pour IDE Java qui permettent de lancer les tests unitaires en continu et de manière intelligente, c’est à dire uniquement ceux impactés par le code modifié.
La première idée lorsque l’on cherche à optimiser cette durée d’exécution, c’est de vouloir déléguer le problème. C’est faire tourner les tests sur des serveurs distribués qui permettront d’exécuter les tests en tâches de fond. C’est une mauvaise idée, les serveurs coûtent chers et on peut se retrouver submerger. Il existe des méthodes plus simples pour réduire cette durée.
Le KISS ( Keep It Simple, Stupid ) est également applicable lorsque l’on crée des tests. Chercher à optimises ses tests peut améliorer votre produit : ce qui est simple à tester sera simple à coder et à utiliser. Ce qui est compliqué n’en vaut surement pas la peine.
La manière la plus simple pour accélérer les tests c’est d’acheter une machine plus rapide. Exécuter les tests sur une machine plus rapide peut être un vrai gain de temps, David nous donne l’exemple d’une exécution 15% plus rapide sur la machine la plus rapide par rapport à la plus lente. Il est également possible d’utiliser la nouvelle fonctionnalité de maven de build en parallèle (mvn -T2 clean install / mvn -t4 clean install). Nous avons essayé sur un de nos projets, l’exécution du build est passé de 1m30 à 30 secondes !
Il est également possible de faire en sorte que les tâches maven surefire pour JUnit et TestNG soient exécutés en parallèle. Comme les tests se doivent d’être indépendant et isolés, ce sont de bons candidats à une exécution en parallèle. Faire quand même attention que vous pouvez vous retrouver avec des problèmes de concurrence dans certains cas.
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.5</version> <configuration> <parallel>methods</parallel> <threadCount>4</threadCount> </configuration> </plugin>
Il existe deux façons de paralléliser : méthodes ou classes. Utiliser la méthode de parallélisation par méthode peut se révéler risqué car il est fort probable que tous les tests n’auront pas été designés dans l’optique d’être exécutés en parallèle, la méthode ‘classes’ est un choix plus prudent. Plus d’infos sur le blog de Wakaleo Consulting.
Il y a souvent des tests redondants dans un projets : débusquez les, cela permettra de gagner en exécution et en lisibilité. C’est tellement simple qu’on ne le fait pas ! Ne pas hésiter à supprimer des fonctionnalités et les tests associés si elles ne servent plus rien, le projet y gagnera en simplicité.
Les accès réseaux et disques sont trop lents. Il faut essayer au maximum de s’en passer et privilégier les bases de données en mémoire comme h2 (qui ressemblera plus à mysql que hsqldb). De même pour les accès mails, il est possible d’utiliser dans les tests des serveurs SMTP en mémoire comme ethereal. Si beaucoup de tests accèdent à des fichiers, Spring Resource ou Apache VFS (Virtual File System) sont de bonnes alternatives.
Il est préférable de tester les règles métiers dans les tests unitaires plutôt que dans les test d’intégrations. Il ne faut pas confondre tests d’intégrations et tests unitaires : les premiers sont , bien qu’essentiel, plus longs à tester, ils doivent être utiliser avec parcimonie. Par exemple, pour plusieurs tests qui accèderaient à une base de données peuvent être remplacés par un test qui permet de garantir que l’on peut bien accéder à la base de données et par plusieurs autres tests où l’accès à la base de données aura été bouchonné.
Une méthode lorsque l’on cherche à diminuer la durée d’exécution de tests est de prendre le test d’intégration le plus long et de l’analyser jusqu’à réussir à le découper en un test d’intégration plus petit et plusieurs tests unitaires. Si c’est l’accès à certaines couches qui sont lentes lors de tests d’intégrations, il est recommandé de les bouchonner, les frameworks de mocks ne servent pas que dans le cas de tests unitaires
De même, méfiez vous des tests d’interface, ils prennent beaucoup de temps et souvent, ce qu’ils testent peut être tester unitairement. Selenium est à utiliser avec modération. Méfiez vous vraiment quand vous commencez à tester vos fonctionnalités via Selenium. Et ne dites pas ‘mon utilisateur veut de l’AJAX‘ ‘J’ai besoin de tester la compatibilité des différents navigateurs’.
Chaque complexité a un coût. Et cela se paye à chaque fois que les tests sont exécutés. Si c’est compliqué à tester : danger, la fonctionnalité peut sûrement être faite plus simplement. Il est possible de faire des tests unitaires en Javascript plutôt que tester dans les browsers (ex : QUnit).
David préfère limité AJAX aux réels besoins et d’effectuer au maximum le code server-side.
Et pour finir : simplifier et optimiser votre code. Ce sont des choses qui se font. Le build va être plus rapide et l’application aussi A vous l’effet Kiss Cool
]]>JRebel est un outil payant permettant d’effectuer des rechargements à chaud d’applicatif Java. C’est clairement l’outil qui fera de vous un développeur plus productif. Voilà une bonne façon de se faire bien voir, de rentrer plus tôt à la maison ou d’augmenter sa facturation
Vous pouvez, en tant que personne, acheter une « Personnal license » et l’utiliser dans le cadre de votre travail. Cette licence ne peut pas être remboursée par votre entreprise. L’achat d’une licence « standard » n’est pas rentable, même payé par votre EURL. Votre client peut également acheter des licences standard mais personnellement, je pense qu’il vaut mieux lui permettre de faire cette économie. Un client n’est pas éternel et un geste commercial est toujours bien vu non ?
JRebel est ce que l’on appelle un « java agent » (see : http://blog.xebia.fr/2008/05/02/java-agent-instrumentez-vos-classes/ ). C’est à dire qu’il va instrumenter le bytecode.
Pour que cela fonctionne il faudra donc spécifier à la JVM l’agent JRebel (jrebel.jar).
Cette agent fonctionne de manière très simple, il scrute le classpath à la recherche des fichiers rebel.xml. Ces fichiers permettent de faire un mapping entre l’application (bytecode et ressources) et votre code source. JRebel scan les sources et les mets à jour dans l’application en cours d’exécution. C’est aussi simple que cela.
Vous l’avez compris, pour recharger à chaud votre projet et toutes vos dépendances vous devez avoir un fichier rebel.xml dans chacun des projets (.jar .war) que vous souhaitez recharger à chaud.
L’installation de JRebel va simplement copier l’agent jrebel.jar dans le répertoire de votre choix. Il existe également un plugin Eclipse (optionnel).
Comme nous l’avons vu, nous devons fournir un fichier rebel.xml dans chacun de nos projets. Avec Maven, il suffit simplement d’ajouter le plugin maven-rebel, dans le fichier pom.xml :
<plugin> <groupId>org.zeroturnaround</groupId> <artifactId>javarebel-maven-plugin</artifactId> <version>1.0.5</version> <executions> <execution> <id>generate-rebel-xml</id> <phase>process-resources</phase> <goals> <goal>generate</goal> </goals> </execution> </executions> </plugin>
On pourra lancer la génération manuellement via « mvn javarebel:generate »
Ensuite, il suffit de lancer l’application en spécifiant l’agent à la JVM :
-noverify
-javaagent: »C:Program FilesZeroTurnaroundJRebeljrebel.jar »
Si vous lancer votre application via maven (mvn jetty:run par ex), vous pouvez utiliser la variable système MAVEN_OPTS, ou utiliser « Run configuration » dans Eclipse. Il doit sans doute exister un moyen de le faire dans le pom (via un profil « rebel » par exemple).
Voilà ça fonctionne !
Have fun !
]]>Au début de ma carrière, je me souviens qu’un de mes collègues m’a dit « Tu sais, au final, on se rend compte qu’il n’y a que très peu de différence de productivité entre un bon développeur et un développeur lambda ». Je l’ai cru, et il m’a fallu quelques années pour me rendre compte qu’il avait complétement tort. Un développeur productif peu produire jusqu’à 10 fois plus (sans diminuer la qualité du code). Sachant que les tarifs vont du simple au double, engager un bon développeur, 2 fois plus cher, peut s’avérer en fait 5 fois plus rentable pour le client. Pensez y au prochain entretien
A votre avis, un client préférera prendre un développeur productif mais cher ou un développeur lambda pas cher ?
Le seul moyen de s’améliorer c’est d’apprendre, apprendre, apprendre. N’hésitez pas à poser une journée ou deux par mois, et de préférence en semaine pour vous former au calme, chez vous. Les transports sont aussi une bonne occasion, pour ceux qui ont la chance de les utiliser.
Ce n’est que quand j’ai commencé à m’ennuyer en mission que j’ai décidé de lire. Et c’est en lisant que je me suis rendu compte à quel point j’étais mauvais en Java. Le livre qui m’a fait prendre conscience est « Effective Java » :
Bien, une fois que j’ai pris conscience qu’en fait j’avais rien compris à Java, suis je meilleur développeur ? Non, mais c’est un début. Maintenant il faut apprendre se comporter comme un bon développeur. Que ce soit avec votre code : « Don’t repeat yourself », avec les autres : « Provide Options, Don’t Make Lame Excuses » ou avec vous même : « Invest Regularly in Your Knowledge Portfolio »
« The pragmatic programmer » est LE livre à lire et se présente sous la forme d’une liste d’astuces :
Dans le même style, les astuces sont plutôt orientées Business mais certaines peuvent s’appliquer à nos développements. Et puis, quel freelance ne rêve pas de monter sa petite entreprise ? Rework est le genre de livre qui peut révolutionner une vie :
Voici une liste d’astuces, issue de ma propre expérience.
– Maîtriser vos outils : Apprenez les raccourcis clavier de votre IDE et utilisez les. Regarder comment les autres développeur utilisent vos outils, c’est très instructif !
– Choisissez vos missions : Débrouiller vous pour toujours travailler avec des gens meilleurs que vous. Faites en sortes de toujours apprendre quelque chose de nouveau (technos, architecture, langage, métier, méthodologie).
– Reposez vous : Inutile de se fatiguer au travail, cela ne fera que baisser votre productivité. Fatigue = erreurs. Un besoin incompris, c’est une productivité nulle.
– Changez vous les idées, consacrez du temps à vos loisirs et à votre famille. Cela boostera votre créativité et vous aidera à résoudre les problèmes complexes.
– Limitez les sources de perturbations. Coupez Twitter, votre e-mail (même le pro) ainsi que Facebook. Profitez de votre téléphone 3G dernier cris pour traiter tous ça dans les transports le matin et le soir.
– Travaillez vos forces et laissez tomber vos faiblesses : Est ce bien utile de s’obstiner à maîtriser CSS et photoshop alors que l’on n’est pas artiste pour un sou ? Cela va vous prendre énormément de temps. Temps qui serait mieux utilisé à augmenter votre expertise dans votre domaine de compétence. Ne soyez pas dupes et ne soyez pas jaloux des « rockstar », vous êtes sans doute meilleur qu’eux dans un autre domaine !
Pour ceux qui vont vite et aussi parceque cet article est l’occasion de tester les liens sponsorisés Amazon ;), voici 3 livres qui m’ont également aidé à progresser et que je vous conseil pour compléter votre read list:
YapluKa !
]]>J’ai participé à TDD/ATTD et à la session sur les commentaires dans le code.
Le Test Driven Development est une méthode de programmation basée sur les règles suivantes :
L’Acceptance Test Driven Development (ATDD) est basée sur l’idée de faire des tests d’acceptance (automatisés) avant même de commencer les développements. La bonne mise en pratique se fait en deux temps : le client, les utilisateurs, la MOA écrit les test pendant que les développeurs se chargent de les automatiser. Plusieurs outils existent pour cela comme par exemple Greenpepper, Fit/Fitnesse, Robot Framework … La plupart se base sur des wiki pour permettre aux personnes fonctionnelles d’écrire facilement leurs tests.
Dans une approche basée sur l’ATDD, les spécifications et les implémentations sont dirigées par des exemples concrets. Décrire précisément des cas de tests oblige celui qui spécifie à aller plus loin que les spécifications habituelles. Cette méthode a également l’avantage d’être plus claire pour le développeur.
Peu de personnes font du TDD pur et dur, la plupart de la dizaine de personnes présentes écrivent un bout de code avant de tester. Néanmoins, la personne ayant le plus d’expérience est un adepte du TDD pur et dur depuis de nombreuses années. Les frameworks de mocks permettent de simplifier sensiblement la tâche du développeur. Néanmoins, le TDD est loin d’être une évidence pour tout le monde. Le risque de perdre de vue l’architecture générale du projet, la difficulté de la sensibilisation d’une équipe aux tests, les problèmes de maintenabilité et de refactoring sont quelques uns des exemples soulevés pour montrer les difficultés engendrés par l’application de cette méthode.
Au niveau des tests d’acceptance, seul une seule personne les utilise dans un contexte d’entreprise.
Le gros problème soulevé par les tests d’acceptance c’est la difficulté, lors d’utilisation d’outils type fit/fitness greenpepper est la refactorisation des tests : les tests écrits sont difficilement maintenable dans le temps. L’ATDD permet au développeur d’avoir des spécifications claires et précises, permettant entre autre de réduire les difficultés de communication entre la maitrîse d’oeuvre et d’ouvrage. Un autre point bloquant est que l’ATDD demande à l’équipe fonctionnelle de s’impliquer et de se former à un nouvel outil, ce qui n’est pas simple dans certains contextes.
Un autre aspect négatif est le temps pris par les tests, qu’ils soient d’intégration, d’acceptance … Ils ont tendance à grossir encore et encore, à devenir de moins en moins maintenables jusqu’à devenir contre productif. Plutôt qu’un enchaînement d’action, difficile maintenable dans le temps, il faut se focaliser sur les work flows et sur les règles business. Il faut également essayer de limiter quand cela est possible l’utilisation de l’IHM dans les tests d’acceptance.
Olivier Croisier a pour projet de synthétiser les bonnes pratiques de commentaires. Plusieurs points ont été abordés dans cet atelier.
Au niveau des APIs publics, la javadoc est coûteuse à maintenir mais obligatoire, et en anglais. Lorsqu’il y a du code, il est souvent obsolète donc à utiliser avec précaution.
Au niveau du code non public, les personnes présentes utilisent beaucoup moins la javadoc. On remarque souvent qu’il n’y en a pas, qu’elle est obsolète ou inintéressante. Un point pour commenter facilement le code est l’encapsulation du code dans des méthodes private qui, via leurs noms, sont auto-documentées. On peut donc alors comprendre facilement une méthode principale en lisant la succession de méthodes appelées. A noter qu’il ne suffit pas de découper son code n’importe comment, il est important de ne pas avoir une profondeur d’appels de méthodes de ce type trop importantes (A qui appelle B qui appelle C qui appelle D qui appelle E). Un moyen de découper convenablement le code est d’éviter d’appeler dans la méthode principale des méthodes fonctionnelles et des méthodes techniques. Le code est plus lisible si on découpe du fonctionnel vers le technique.
J’ai utilisé cette méthode pour découper des classes de plusieurs centaines de lignes avec un fonctionnel un peu compliqué et je trouve que le code est beaucoup plus clair, en cas d’évolution ou de recherche d’un bug, on sait directement où aller. Le problème de cette méthode est que parfois, on ne voit pas certains refactoring qui pourrait faire gagner en lisibilité (là encore, il est possible de perdre de vue la ‘big picture’). C’est pour cette raison que certains estiment qu’une méthode n’a d’utilité que lorsqu’elle est réutilisée et que les méthodes de plusieurs dizaines de lignes sont en fait plus claires. Sur ce sujet, lire Clean Code et Effective Java est essentiel.
Un autre point est l’utilisation des commentaires du style /**** GETTERS AND SETTERS *****/ /***** CONSTRUCTORS *****/ . La encore, cette méthode ne fait pas l’unanimité (et je suis loin d’en être la première fan).
Au niveau de l’utilisation français/anglais, c’est assez mitigé. La plupart écrivent leurs noms de variables et de commentaires en anglais, d’autres se permettent quelques écarts en français et pour quelques un le français est la norme.
Au niveau des tâches (//FIXME //XXX //TODO), la plupart des personnes présentes sont d’accord avec le fait que cela n’est utile que si la tâche nous concerne.
En ce qui concerne les warnings sur les classes, l’idée est d’en avoir aucun : l’utilisation des outils comme PMD, FindBugs, CheckStyle est à bien configurer avant utilisation, car la multiplication des warnings noie les vrais warnings dans de la pollution.
Je retiens également que Implementation Patterns de Kent Beck sera une de mes lectures estivales et que l’outil mvn eclipse:eclipse permet de faire bien plus de choses que prévues !
J’ai passé un agréable moment, j’y étais venue surtout par curiosité et je compte bien retourner à la prochaine ! Ça a duré jusqu’à un peu plus de 23 heures pour moi, ce qui fait quand même une bonne soirée. Énormément de digressions, ça part dans tous les sens mais c’est souvent là que c’est le plus intéressant !
Sur twitter : http://twitter.com/KawaCampParis
]]>
Au niveau du déploiement, la première bonne pratique est de minimiser le nombre de composants à déployer. Ce qui ne veut pas dire qu’il faut déployer des applications monolithiques mais bien qu’il est important de chercher et d’essayer de regrouper les composants de manière cohérente.
Il est courant pour le déploiement d’un unique EAR ou WAR de devoir dupliquer certains fichiers ou de devoir modifier certaines configuration sur d’autres serveurs. Chaque ‘micro-tâche’ de déploiement peut être source d’erreurs, moins il y en a plus il y a de chance que cela se passe bien ou que le retour en arrière soit aisé.
Ainsi, on peut retrouver la situation suivante : le contenu statique est contenu dans l’archive EAR avec l’ensemble du code nécessaire à l’application. Ce contenu statique sera dupliqué sur un serveur http simple dans une optique d’optimisation d’accès aux ressources statiques. On y accède via un filtrage des URLs en utilisant un mécanisme de routage en amont.
Cela fait donc 3 étapes pour une livraison :
De plus, il est fréquent que le point de routage soit en plus sur un serveur mutualisé, ce qui augmente le risque d’impact de la mise à jour pour la web application déployée sur les autres applications dont les routes transitent par ce point de routage. La principale raison évoquée est le fait de gagner en performance.
La proposition de Cyrille est d’utiliser le mécanisme de proxy-cache http, pouvant être facilement mis en place par l’équipe infrastructure avec des outils tels que que Varnish Cache, squid, ibm Fast Response Cache Accelerator (FRCA) voir un simple module apache http comme mod_cache.
Il n’y a plus de déploiement des fichiers sur un serveur statique, plus de besoin de modifier les règles de routage.
Un autre exemple sur la nécessité du regroupement des différentes briques par cohérence est le double filtrage lié à la sécurité. Il est fréquent d’avoir en amont un premier filtrage par adresse IP puis dans un deuxième temps un filtrage sur un couple login/mot de passe. L’idée est dans ce cas de regrouper ses deux actions ensemble, que ce cela soit sur le firewall, en java ou autre mais à un même point . De plus, cela permet également de pouvoir faire un filtrage par login et par adresse IP en même temps c’est à dire de contrôler par exemple que le login ‘admin’ ne puisse être utilisée que par l’adresse IP X et l’adresse IP Y.
Le deuxième aspect du déploiement abordé est sur le fait de cohabiter en bonne intelligence. Au niveau de la cohabitation de plusieurs applications sur un même serveur, les recommandations sont simples :
Minimiser les répertoires absolus et faire en sorte que chaque fichier soit dans un répertoire préfixé par un identifiant d’application (possible d’utiliser le group id et l’artifact id du composant par exemple ou plus simple /etc/my-application.
Organiser l’utilisation des ports réseaux en séparant un préfixe definissant l’application et un suffixe définissant le service par exemple, si 100 correspond à l’application Facture, on aura le port du serveur http sur 10080, le port du SSL sur 10043. Cette séparation permet de faire cohabiter de manière organisée 100 applications.
En ce qui concerne la bonne cohabitation sur un même serveur d’application, il faut de la même manière minimiser les répertoires absolus (c’est à dire privilégier les chemins relatifs comme (${java.io.tmpdir}/my-app1/) et les rendre lisible en les préfixant par l’artifact id (et le group id si besoin) pour empêcher les collisions entre répertoire.
Cyrille recommande également de limiter au maximum les variables java statiques qui impactent l’ensemble du serveur, il vaut mieux les définir par application.
Au niveau des paramètres de configuration, la question est posée de savoir si il vaut mieux mettre les fichiers de configuration directement dans l’archive ou au contraire de les externaliser. La décision dépend de la situation, par exemple, si l’équipe d’exploitation ne désire pas que l’équipe de développement possède les accès base de données, la configuration se fera à l’extérieure de l’application.
Une autre recommandation au sujet des paramètres de configuration est de limiter le nombre de paramètres dont la valeur change suivant les environnements : en isolant ses réseaux virtuels, il est possible de conserver des noms de bases de données identiques, de conserver les noms de hosts.
Dans le cas où l’équipe d’exploitation propose d’utiliser des noms incluant par exemple les différents environnements ex : monappli-dev , monappli-rec, ma-bdd-dev, ma-bbd-rec il est possible de proposer un compromis avec la création d’alias. La multiplication des paramètres de configuration est souvent source de problème lors des déploiements. De même, il n’y a pas lieu de faire changer les ports d’écoute.
Cette recommandation impose qu’il n’y ait pas de perméabilité entre les zones de développement, de recette et de production. Dans une précédente mission, il suffisait d’utiliser foxy-proxy, plugin firefox de configuration de proxy pour switcher automatiquement de mon application web de prod vers mon application web d’homologation. Dans ce cas, l’homologation était fermée, impossible d’avoir accès à Internet par exemple. Il est également conseillé, par sécurité, de modifier par exemple le logo sur l’homologation ou de jouer avec des codes couleurs pour indiquer de manière visuelle l’environnement sur lequel on se trouve mais aussi d’ajouter un filtrage sur les ip appelantes (bloquer les ips ou changer les mots de passes). A l’aide des .profile, il est possible d’indiquer directement sur la console ‘prod’ ou ‘valid’, avec putty de changer les couleurs etc.
Un autre point important du déploiement est de déployer des composants qui soient réellement traçables. On ne déploie que des composants taggés sur lesquels aucun commit n’aura été effectué (règle d’entreprise respectée/ utilisation de la fonctionnalité d’hook pour SVN) et/ou on empêche le redéploiement ( utilisation de la fonctionnalité Disable Redeployement de Nexus)) pour pouvoir connaître avec précision les classes déployées. Des outils comme Nexus ou Archiva permettent facilement de déployer des archives sur différents environnements en garantissant leur conformité.
Voilà les différents axes abordés sur la partie ‘déploiement’, la présentation, qui a durée 2 heures environ et a abordé 5 axes différents (seul le premier a été présenté ici), a été dense ! Pour ceux que cela intéresse plus en détails, Cyrille devrait publier ses slides dans quelques semaines et organisera à la mi mai une formation de 2 jours sur le thème. Pour le suivre sous twitter : @Cyrilleleclerc .
]]>
Deux BigDecimaux qui ont la même valeur avec une précision différente ne sont pas considérés comme égaux à l’aide la méthode equals. En revanche, ils sont considérés comme égaux si on les compare avec la méthode compareTo.
Exemple :
BigDecimal bigDecimal1 = new BigDecimal(0);//0 BigDecimal bigDecimal2 = BigDecimal.ZERO;//0 BigDecimal bigDecimal3 = BigDecimal.valueOf(0.0);//0.0 System.out.println(bigDecimal1.equals(bigDecimal2)); //true System.out.println(bigDecimal1.equals(bigDecimal3)); //false System.out.println(bigDecimal2.equals(bigDecimal3)); //false System.out.println(bigDecimal1.compareTo(bigDecimal2)==0); //true System.out.println(bigDecimal1.compareTo(bigDecimal3)==0); //true System.out.println(bigDecimal2.compareTo(bigDecimal3)==0); //true
public int compareTo(BigDecimal val)
Compares this BigDecimal with the specified BigDecimal. Two BigDecimal objects that are equal in value but have a different scale (like 2.0 and 2.00) are considered equal by this method. This method is provided in preference to individual methods for each of the six boolean comparison operators (<, ==, >, >=, !=, <=). The suggested idiom for performing these comparisons is: (x.compareTo(y) <op> 0), where <op> is one of the six comparison operators.
Dans un autre registre, quelque chose qui revient souvent dans le code :
Les termes public ou abstract dans la déclaration des méthodes d’une interface. L’ensemble des méthodes d’une interface sont obligatoirement public abstract, inutile donc de l’ajouter.
Il y a toujours des variables mal nommées. Plusieurs raisons à cela :
Le livre Coder Proprement de Robert C Martin consacre plusieurs pages intéressantes sur ce sujet.
Exemple 1 : Avec utilisation de l’opérateur +
long start = System.currentTimeMillis(); String a = " Concaténation"; for (int i = 0; i < 10000; i++) { a += i; } System.out.println(a); System.out .println(" in " + (System.currentTimeMillis() - start) + "ms");
Exemple 2 : Avec un StringBuilder
start = System.currentTimeMillis(); StringBuilder stringBuffer = new StringBuilder (" Concaténation"); for (int i = 0; i < 10000; i++) { stringBuffer.append(i); } System.out.println(stringBuffer.toString()); System.out .println(" in " + (System.currentTimeMillis() - start) + "ms");
Durée des 10000 concaténations :
Exemple 1 : 360ms
Exemple 2 : 15ms
Il a néanmoins des cas où il est préférable d’utiliser la concaténation de string (+ ou String.concat) :
pour aérer une ligne ex :
String myString = " line 1 " + "line 2";
pour strictement moins de 4 concaténation :
logger.debug("x :"+x+"y :"+y);
Le compilateur transforme automatiquement cettte ligne en utilisant un StringBuilder System.out.println((new StringBuilder()).append(« x: »).append(x).append( » y: »).append(y).toString());
Les tests avec les objets StringBuilder et StringBuffer renvoient des résultats quasiment similaires. StringBuilder étant un tout petit plus rapide, il est à utiliser le plus souvent possible. Le StringBuffer est quand à lui thread-safe donc à utiliser dans les environnements concurrents.
Il n’est pas forcément judicieux de remplacer toutes les concaténations de string automatiquement par un StringBuilder / StringBuffer. Si elles sont dans une boucle, le gain peut être remarquable dans les autres cas, cela se discute. Les StringBuilder / StringBuffer alourdissent considérablement la syntaxe et le gain est parfois très faible. Comme dans la plupart des cas, l’optimisation prématurée est à éviter ! Pour appronfondir le sujet, voir l’interview du Dr Heintz Kabutz : http://java.sun.com/developer/technicalArticles/Interviews/community/kabutz_qa.html
]]>Le domain driven design « Vite fait » : 80 pages pour changer votre vie. Ou plutôt changer votre façon d’écrire vos modèles métiers.
Cours sur la concurrence : Une piqure de rappel ne fait pas (de) mal.
Développer avec Comet et Java : Vous rêver d’envoyer des informations au navigateur en mode « push » ? Voici la solution.
Algodeal: Pour tous ceux qui aime inventer des algos, voici une occasion de faire fortune en s’amusant (et en java) !
Les castcodeurs : Episode spécial « Freelance » avec Mathilde ! A écouter absolument si vous souhaitez vous mettre indépendant !
]]>Dans la plupart des cas, pas de problème, la date est bien parsée et l’identifiant est crée conformément à ce que l’on attends. Néanmoins, dans de rares cas, il est possible que cela se passe beaucoup moins bien. En effet, la classe SimpleDateFormat n’est pas synchronisée. Si 2 threads essaient en même temps d’utiliser cette instance pour un parsage ou un formatage, le résultat est aléatoire.
La classe DateUtil a été simplifiée à l’extrême.
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil { public static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat( "ddMMyyyy"); public static final Date parse(String date) throws ParseException{ return simpleDateFormat.parse(date); } public static final String format(Date date) throws ParseException{ return simpleDateFormat.format(date); } }
La classe de test ci dessous fait en sorte que deux threads concurrents effectuent des parsages/formatages via les deux méthodes définies dans la classe DateUtil sur les trois dates de référence définies dans le tableau de String. Elle effectue ensuite une comparaison entre la date de référence et celle qui a été parsée+formatée.
import java.text.ParseException; public class TestSimpleDateFormat { public static void main(String[] args) { final String[] tabDateString = new String[] { "01012001", "08082008", "05052005" }; Runnable runnable = new Runnable() { public void run() { try { for (int j = 0; j < 1000; j++) { for (int i = 0; i < 2; i++) { String date = DateUtil .format(DateUtil .parse(tabDateString[i])); if (!(tabDateString[i].equals(date))) { throw new ParseException( tabDateString[i] + " =>" + date, 0); } } } } catch (ParseException e) { e.printStackTrace(); } } }; new Thread(runnable).start(); Runnable runnable2 = new Runnable() { public void run() { try { for (int j = 0; j < 1000; j++) { for (int i = 0; i < 2; i++) { String date = DateUtil .format(DateUtil .parse(tabDateString[i])); if (!(tabDateString[i].equals(date))) { throw new ParseException( tabDateString[i] + " =>" + date, 0); } } } } catch (ParseException e) { e.printStackTrace(); } } }; new Thread(runnable2).start(); } }
Les résultats sont plus que divers, voilà ci dessous la liste des exceptions les plus fréquentes.
java.text.ParseException: 08082008 =>08012008 at TestSimpleDateFormat$1.run(TestSimpleDateFormat.java:19) at java.lang.Thread.run(Thread.java:619) java.text.ParseException: 01012001 =>08012001 at TestSimpleDateFormat$2.run(TestSimpleDateFormat.java:42) at java.lang.Thread.run(Thread.java:619) Exception in thread "Thread-1" java.lang.NumberFormatException: For input string: ".808E.8082E2" at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1224) at java.lang.Double.parseDouble(Double.java:510) at java.text.DigitList.getDouble(DigitList.java:151) at java.text.DecimalFormat.parse(DecimalFormat.java:1303) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1934) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312) at java.text.DateFormat.parse(DateFormat.java:335) at TestSimpleDateFormat$2.run(TestSimpleDateFormat.java:40) at java.lang.Thread.run(Thread.java:619) Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1084) at java.lang.Double.parseDouble(Double.java:510) at java.text.DigitList.getDouble(DigitList.java:151) at java.text.DecimalFormat.parse(DecimalFormat.java:1303) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1934) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312) at java.text.DateFormat.parse(DateFormat.java:335) at TestSimpleDateFormat$2.run(TestSimpleDateFormat.java:40) at java.lang.Thread.run(Thread.java:619) Exception in thread "Thread-1" java.lang.NumberFormatException: For input string: "" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48) at java.lang.Long.parseLong(Long.java:431) at java.lang.Long.parseLong(Long.java:468) at java.text.DigitList.getLong(DigitList.java:177) at java.text.DecimalFormat.parse(DecimalFormat.java:1298) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1934) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312) at java.text.DateFormat.parse(DateFormat.java:335) at TestSimpleDateFormat$2.run(TestSimpleDateFormat.java:40) at java.lang.Thread.run(Thread.java:619)
Bien sur, ces erreurs n’arrivent en réalité qu’exceptionnellement. Parfois, des exceptions sont remontées, parfois cela a l’air de bien se passer mais l’objet crée est en fait une combinaison d’autres dates et ne correspond donc pas à la date attendue. Il y a plusieurs manière de rendre la classe DateUtil thread safe.
Le plus simple, ne pas rendre l’objet simpleDateFormat comme étant un attribut de la classe mais de le recréer à chaque appel.
Ex :
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil { public static final Date parse(String date) throws ParseException{ SimpleDateFormat simpleDateFormat = new SimpleDateFormat( "ddMMyyyy"); return simpleDateFormat.parse(date); } public static final String format(Date date) throws ParseException{ SimpleDateFormat simpleDateFormat = new SimpleDateFormat( "ddMMyyyy"); return simpleDateFormat.format(date); } }
Dans le cas de méthodes communes, il est possible que cela deviennent couteux.
Une deuxième méthode est la définition des deux méthodes parse & format comme étant synchronized :
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil { public static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat( "ddMMyyyy"); public synchronized static final Date parse(String date) throws ParseException{ return simpleDateFormat.parse(date); } public synchronized static final String format(Date date) throws ParseException{ return simpleDateFormat.format(date); } }
Cette méthode marche, néanmoins, il est possible qu’elle crée un goulet d’étranglement du au fonctionnement de synchronized, les méthodes format & parse pouvant être extrêmement communes dans une application. Néanmoins, elle offre l’avantage d’être simple et claire.
Une approche un peu plus fine est l’utilisation d’un objet ThreadLocal. Cet objet permet d’utiliser des variables comme étant locale à un thread. Tous les threads ont une copie de la variable.
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil { private static ThreadLocal format = new ThreadLocal() { protected synchronized SimpleDateFormat initialValue() { return new SimpleDateFormat( "ddMMyyyy"); } }; private static SimpleDateFormat getSimpleDateFormat(){ return format.get(); } public static final Date parse(String date) throws ParseException{ return getSimpleDateFormat().parse(date); } public static final String format(Date date) throws ParseException{ return getSimpleDateFormat().format(date); } }
Comme je suis dans le cas où ma classe DateUtil n’a besoin que d’une méthode format, j’ai choisi une 4ème méthode, l’utilisation non plus de l’objet java.text.SimpleDateFormat mais org.apache.commons.lang.time.FastDateFormat . Le formatage est supportée, avec le même pattern que pour la classe SimpleDateFormat à l’exception du z (se référer à la doc).
]]>
Les actions menées en France seront de plusieurs types :
– La création d’un réseau virtuel, via twitter, linkedin, une mailing list …
– Inciter les femmes à participer à des évènements locaux, comme les soirées JUG ou les autres conférences.
– Se rencontrer pour échanger, avant ou après les soirées JUGs, autour d’un apéro ou d’un repas.
– Lister les différents évènements en France pour ne pas les rater et éventuellement trouver quelqu’un avec qui y aller.
Pour se tenir informer des évènements de ce groupe, vous pouvez nous rejoindre sur twitter @duchessfr , sur LinkedIn ou via notre mailing list.
Le premier évènement aura lieu le 9 mars à 18h30 , juste avant le paris JUG au Vavin Café (18 rue Vavin 75006 Paris). Lorsque vous serez inscrites au JUG, contactez ellene(dot)dijoux(at)jduchess(dot)org qui vous accueillera Mardi prochain.
]]>Le Paris JUG c’est aussi une ‘troisième mi-temps’ dans un bar-resto après la soirée. Me retrouver après les confs pour discuter avec des passionnés sur des sujets variés (mise en place de pair programming, Groovy, se lancer en freelance …) est vraiment une composante importante de mon intérêt pour le Paris JUG !
Pour la soirée d’hier, les choses avaient été faites en grand : amphi de 500 places, bien rempli, goodies, présence de stands, buffet, énormément de gens venus de partout dont pas mal rencontrés à Devoxx. Au niveau des sujets, la keynote d’ouverture de Sacha Labourey abordait le thème ‘la révolution open-source a t elle eu lieu ?’. Se sont suivis ensuite quelques questions/réponses à Sacha et Marc Fleury, invité surprise de la soirée et fondateur de Jboss. La notion de ‘passion’ a pour la première fois été abordée et il faut reconnaître, et tous les intervenants l’ont fait lors de la soirée, que s’investir dans l’open-source est chronophage et avant tout, une affaire de passion.
Quelques business models de projets Open Source (Acceleo, XWiki, eXo Platform) ont été présentés sur la forme de quickies par les différents acteurs de ces projets, permettant de donner des exemples concrets à la keynote plus générale de Sacha. Des outils open-source (jCaptcha, jax-doclets, Play!) ont eu également le droit à leurs quickies, plus techniques.
Jean-Michel Doudoux, architecte Sfeir Benelux , a également parlé de son travail de rédaction et du choix de la license autour de Développons en Java, quelques 1888 pages en français, le tout sur 10 ans, mis à jour désormais 3 à 4 fois par ans et qui correspond à une véritable bible, en français sur le langage Java : http://www.jmdoudoux.fr/accueil_java.htm#dej . Les prochaines évolutions seront sur JEE, Android ! Un travail titanesque pour lequel il a choisit la license GNU FDL, bien conscient des problèmes pour faire valoir ses droits dès que l’on publie sur le net.
La présentation sur le framework Play! était vraiment bien mené et a éveillé pas mal les curiosités ! Ce framework web, fait par des développeurs web constitue une alternative pour développer sur une architecture REST. Ce framework permet entre autre un rechargement à chaud (la démo est faite via un simple éditeur de texte), le code modifié impactant immédiatement l’appli web, un système de gestion des exceptions à l’écran, un système de templating, avec la possibilité de créer ses propres tags ou d’utiliser le système d’expression langage de groovy et beaucoup d’autres choses encore.
Pour avoir plus d’infos : http://www.playframework.org/
Pour avoir le détail des conférences, Olivier Croisier a très bien retranscrit tout le détail dans son blog http://thecodersbreakfast.net/index.php?post/2010/02/05/Suivez-le-Paris-JUG-anniversaire-en-live !
La troisième mi-temps s’est déroulé au Dome dans le 17ème avec petits fours. J’ai eu l’occasion de discuter avec Nicolas Leroux (contributeur) et Guillaume Bort (lead developper) autour de Play! , principalement sur l’utilisation en industrie et des questions plus techniques. On en a également profité pour parler d’un projet de réseautage de la communauté féminine de Java, j’en parlerai plus en détail quand cela sera avancé mais c’était agréable de voir du monde réunis autour de la table !
Le problème, c’est que sur des integers entre -128 et 127, cela peut marcher. Dans la suite de l’exemple, integer1 et integer2 seront de type java.lang.Integer, int1 sera de type primitif int.
Integer integer1 = 127; Integer integer2 = 127; System.out.println(integer1 == integer2); // true integer1 = 128; integer2 = 128; System.out.println(integer1 == integer2); // false
Par contre, on a bien pour toutes les valeurs, y compris celles entre -128 et 127 :
integer1 = new Integer(127); integer2 = new Integer(127); System.out.println(integer1 == integer2); // false - normal on a instancié 2 objects différents integer1 = new Integer(129); int1 = 129; System.out.println(integer1 == integer2); // true
Si on regarde la doc de Sun, pour tous les objets entre -128 et 127, l’autoboxing garantit que l’objet renvoyé Integer sera identique pour un même entier primitif. Ce qui ne sera pas le cas au delà de l’intervalle. De plus, rien ne garantit qu’une implémentation étende l’intervalle de cache, raison pour laquelle il est risqué de se baser dessus.
Byte, Short, Long ont également le même type de cache. Character a également un cache mais de 0 à 127 (un nombre négatif n’aurait pas eu de sens).
d. Autoboxing is guaranteed to return the same object for integral
values in the range [-128, 127], but an implementation may, at its
discretion, cache values outside of that range. It would be bad style
http://java.sun.com/developer/JDCTechTips/2005/tt0405.html#1
Ainsi, utiliser == en se fiant à l’autoboxing et au cache est une mauvaise pratique, même pour comparer des constantes de faible valeur.
Il est préferable d’utiliser un valueOf plutôt qu’une nouvelle instanciation à l’aide du constructeur. Valable pour la certains objets immutables.
Exemple avec des types java.lang.Integer, si on regarde la javadoc pour la méthode Integer.valueOf(int i) :
public static Integer valueOf(int i)
Integer(int)
, as this method is likely to yield significantly better space and time performance by caching frequently requested values. Idem pour les Byte, Long, Character, BigInteger … Par contre, préférer quand c’est possible de construire les valeurs accessibles directement via une méthode statique (BigInteger.ONE plutôt que BigInteger.valueOf(1), Boolean.FALSE plutôt que Boolean.valueOf(false) …)
String test1 = "abc"; String test2 = "abc"; String test3 = new String("abc"); String test4 = new String("abc"); System.out.println(test1 == test2); // true System.out.println(test1 == test3); //false System.out.println(test3 == test4); //false
Utiliser new String implique de créer un nouvel object en mémoire. Les String étant immutables, avoir 2 objets différents ayant la même valeur ne fait que consommer de la mémoire. Attention à bien noter le cas 2, « abc » est différent de new String(« abc »). Cet exemple permet également de montrer l’importance d’utiliser la méthode equals pour comparer les valeurs des objets java.lang.String .
Comme précisé par Jérôme et Nicolas Le Coz en commentaire, l’instanciation d’un nouvel objet String est conseillée lors de l’utilisation de substring sur des très grandes chaînes de caractères. En effet, la valeur stockée avec un substring n’est pas uniquement la substring, mais la chaîne entière avec un offset (premier caractère à utiliser) et un compteur (nombre de caractères à utiliser).
C’est clair si l’on regarde la classe java.lang.String
public final class String implements java.io.Serializable, Comparable, CharSequence { /** The value is used for character storage. */ private final char value[]; /** The offset is the first index of the storage that is used. */ private final int offset; /** The count is the number of characters in the String. */ private final int count; [...] public String substring(int beginIndex, int endIndex) { [...] return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value); }
Plus d’infos ici http://blog.xebia.fr/2009/08/03/revue-de-presse-xebia-120/#AttentionvotremmoireavecString et dans les autres liens en commentaires.
On utilise ici 3 manières de faire pour créer un BigDecimal que l’on souhaite égal à 4.12. Néanmoins, les résultats ne sont pas conformes à ce qu’on pourrait en attendre.
Double myDouble = 4.12D; BigDecimal bd1 = new BigDecimal(myDouble); BigDecimal bd2 = new BigDecimal(String.valueOf(myDouble)); BigDecimal bd3 = BigDecimal.valueOf(myDouble); System.out.println(bd1.equals(bd2)); // false ! System.out.println(bd1.equals(bd3)); // false ! System.out.println(bd2.equals(bd3)); // true
Il n’y a donc pas égalité de valeur entre l’object construit à partir du double et les 2 autres objets. En effet, si on affiche les différentes valeurs des 3 BigDecimal, le problème est facilement visible :
RESULT 1: 4.12000000000000010658141036401502788066864013671875
RESULT 2: 4.12
RESULT 3: 4.12
Il vaut mieux donc éviter d’utiliser le constructeur BigDecimal(double double) et de lui préférer les deux autres méthodes.
public BigDecimal(double val)
double
into a BigDecimal
which is the exact decimal representation of the double
‘s binary floating-point value. The scale of the returned BigDecimal
is the smallest value such that (10scale × val) is an integer.Notes:
new BigDecimal(0.1)
in Java creates a BigDecimal
which is exactly equal to 0.1 (an unscaled value of 1, with a scale of 1), but it is actually equal to 0.1000000000000000055511151231257827021181583404541015625. This is because 0.1 cannot be represented exactly as a double
(or, for that matter, as a binary fraction of any finite length). Thus, the value that is being passed in to the constructor is not exactly equal to 0.1, appearances notwithstanding.String
constructor, on the other hand, is perfectly predictable: writing new BigDecimal("0.1")
creates a BigDecimal
which is exactly equal to 0.1, as one would expect. Therefore, it is generally recommended that the String constructor be used in preference to this one.double
must be used as a source for a BigDecimal
, note that this constructor provides an exact conversion; it does not give the same result as converting the double
to a String
using the Double.toString(double)
method and then using the BigDecimal(String)
constructor. To get that result, use the static
valueOf(double)
method.C’est 5 jours de conférences, principalement orientés java à Anvers en Belgique. La réception des devoxians à lieu au Metropolis, une salle de cinéma dans la banlieue d’Anvers, à environ 20 min de la gare centrale en tramway. Après avoir récupéré nos badges, on nous fournit un sac de goodies (sac/stylo/bloc note/Tee shirt).
La première conférence à laquelle j’ai assisté sur Kanban a duré 3 heures. Kanban est une méthode agile avec un fonctionnement plus simple que Scrum, dans le sens où elle est beaucoup moins contraignante. C’est une méthode avec une forte composante visuel où un radiateur d’information constitue un point central de la méthode. Ses trois principes sont la confiance, l’esprit d’équipe et la transparence. Il n’y a pas de notions de sprint ou d’itération . Les demandes arrivent en permanence et sont traitées au fur et à mesure.
La deuxième conférence sur JSF 2 était assez pointue, organisée en 3 parties : une heure sur la vue, une heure sur le controleur, une heure sur le modèle. L’aspect orientation composant au lieu de contenu comme c’est le cas avec JSP est assez interessant. L’inconvénient de JSF 1.2 était la quantité très importantes d’XML, JSF 2.0 passe aux annotations, ce qui la rend beaucoup plus simple à manipuler. D’autres améliorations : pouvoir passer des paramètres en GET (sans commentaire), gérer des bookmarkables URLs …
Un des points qui a été souvent abordé lors de la présentation est le futur de JSF. Par exemple, le fait d’avoir une API standard pour AJAX mais également les points bloquants (certaines lourdeurs de configurations). J’ai beaucoup aimé la franchise de Peter, qui a bien montré ce que l’on peut faire et ce qui peut être améliorer.
Place ensuite à 2 petites présentations qui m’ont laissé sur ma faim. En une quarantaine de minutes, difficile de rentrer dans un sujet technique, tout au moins d’en montrer ses limites.
Une présentation scolaire mais malgrès tout intéressante d’Hades, qui permet de simplifier l’utilisation de JPa n’a cependant pas montré les cas limites d’utilisation.
De même, la présentation NoSQL n’est pas rentrée assez dans les détails pour que cela deviennent vraiment interessant.
De 19h à 22h, place à 3 BoFs : Une session très intéressante sur le NoSQL qui a abordé beaucoup de points, comme l’efficacité, les différents types d’outils, l’importance de choisir un système qui correspond à nos attentes. Pour choisir un bon type de base de données, il est conseillé de faire ressortir 2 priorités entre : Consistency, Partition Tolerance et Availability. Ainsi, Cassandra par exemple, dans le cas de serveurs distribués, va remonter la dernière information mais sans que cela ne soit garantie (choix fait sur Partition Tolerance et Availability ). L’utilisation pour les boutiques en ligne est tout indiquée (c’est le système d’Amazon). Alors que d’autres systèmes tel que HBase garantissent eux une consistance des données, ils seront plutôt utilisés dans des environnements type CMS, où ce qui est visible à l’écran doit correspondre aux dernières données.
Sur JSF2, la BoF est venu compléter la présentation de l’après midi.
La BoF d’hibernate a été assez intéressante. Une présentation assez longue de l’API Criteria (qui n’est pas très sexy au premier abord) ne m’a pas vraiment fait changé d’avis mais néanmoins, le fait que le meta-modèle soit généré automatiquement par l’IDE simplement est quand même important. Le principal intêret de cet specification est le fait que cela soit typé, néanmoins, peut être parceque je ne suis plus habituée à utiliser un ORM (JDBC pur), je trouve cela très (trop) verbeux. Cet aspect verbeux provient du fait que l’API critéria est très puissante et supporte énormément de choses. On peut l’imaginer encapsulée dans un framework plus simple pour ne retenir que les choses ‘utiles’ à son projet dans une forme verbeuse plus simple.
Et voilà le bilan de cette première journée. D’autre articles arriveront sous peu !
]]>Pour ceux qui ont la chance d’être resté en mission ou qui n’ont pas vu leur tarif baisser, ce n’est pas le moment changer. Et pour ceux qui ont subit, le risque est de continuer à subir quand le marché aura reprit. Alors comment avoir une vue globale sur le marché de la prestation Java en France ?
Personnellement j’utilise le baromètre du site hitechpros.com. Apprendre à décrypter ce baromètre permet de se faire une idée des tendances du marché.
Premièrement, ignorer les pourcentages, ils correspondent aux ratios entre les différentes technologies. Cela n’a aucun intérêt, si ce n’est de savoir que les nouvelles technologies sont plus demandées que le reste…
La courbe bleue correspond aux offres de missions (demandes des clients), ramenée a 100% elle sert de base. La courbe rouge correspond aux prestataires disponibles (offres de SSII). Donc plus la courbe rouge baisse, mieux c’est. Pour le mois de septembre, on voit clairement que le ratio offres / demandes se rapproche de plus en plus du ratio pré crise. C’est un premier indicateur qui indique simplement que le marché s’équilibre. Cela ne veut pas dire qu’il y a un plus grand choix de mission, ni que la crise est terminée mais simplement que pour le mois en cours, il y a eu 242 missions pour 457 prestataires. On en déduit qu’il faut théorique au maximum 2 mois pour trouver une mission et en moyenne 1 mois.
Ce ratio baisse aussi si les SSII ont moins d’intercontrat. Et vue qu’elles ont stoppées les recrutements, il est mécanique qu’il baisse. Les jeunes diplômés payent aussi pour cette crise.
Pour se faire une idée de l’évolution réelle de la demande il faut donc jeter un oeil aux mois précédents. Il y avait 255 missions en Septembre, 119 en Août et autour de 200 les mois précédents. Août est traditionnellement peu dynamique;nous l’ignorons donc. Les demandes étaient donc d’environ 200 par mois pendant la crise. Si le nombre d’offres continue de progresser on peut s’attendre à un retour à des tarifs décents.
– Actuellement le nombre d’intercontrat baisse fortement et les clients commencent à avoir du mal à trouver des ressources. Les indépendants trouvent facilement des missions mais mal payées.
– Si la tendance se confirme, les SSII vont recommencer à recruter afin de répondre aux exigences des clients, les débutants et chômeurs vont réussir à trouver du travail mais moins bien payé qu’avant 2007, car les tarifs n’auront pas encore remontés.
– Lorsque le vivier d’inter-contrats et de jeunes diplômés sera absorbé, le prix commenceront à remonter et les salaires feront de même. Il sera temps pour les indépendants de profiter de cette dynamique avant la prochaine crise.
Pour conclure, n’oubliez pas que le meilleur moyen de rester employable, crise ou pas crise est de se former ! En mission comme à la maison ou en dehors. Ainsi que de bien choisir ses missions !
Et vous comment vivez vous ou avez vous vécu cette crise ? Quelle est votre stratégie pour les mois à venir ?
]]>En général, le podcast contient les rubriques suivantes:
Gage de qualité, les intervenants sont des pointures dans leur domaine.
]]>