août 05

JMX (Java Management Extensions) est très pratique pour le monitoring d’une application.
Malheureusement, lorsque l’application grandit et que, par exemple, des traitements nocturnes apparaissent, ces derniers tournent dans des machines virtuelles distinctes.

A ce moment là, JMX pose problème pour la remontée d’informations.

L’idée que je vais vous proposer est la suivante :

  • Une JVM hébergeant un MBeanServer tourne en continu (éventuellement hébergé dans un serveur d’application).
  • Chacun des traitements se connectera à cette JVM en utilisant un JMX Connector et invoquera à distance des Notifications JMX à destination du MBeanServer.

Grâce à cette architecture, les ports des JVM ‘clientes’ ne sont plus un problème : on laisse le mécanisme par défaut de JMX se débrouiller pour en trouver un libre.

Les personnes souhaitant monitorer ces traitements n’ont qu’à lancer JConsole et se connecter sur la JVM ‘serveur’ qui héberge le MBeanServer.

Le schéma suivant présente le principe :

Cliquez pour agrandir

JMX Notifier va être un objet faisant appel au NotificationPublisher de JMX.
Ce NotificationPublisher envoi des notifications au serveur MBean.

Le JMX Notifier va être exposé par des JMX Connectors aux clients et sera proxyfié.
Le traitement peut alors utiliser de façon invisible cet objet pour envoyer des évènements à la JVM distante.

Passons maintenant au code !

JMX Notifier (serveur) :

@ManagedResource(objectName="batch:name=BatchJMX", description="JMX Notifier")
public class JmxNotifierImpl implements JmxNotifier, NotificationPublisherAware {

  protected NotificationPublisher publisher;

  @ManagedOperation(description="Annonce Début batch.")
  @ManagedOperationParameters(
  @ManagedOperationParameter(name="id", description= "Batch ID."))
  public void annonceDebut(String id) {
    super.publisher.sendNotification(
                    new Notification("Debut du Batch " + id,
                                           this,
                                           0,
                                           Calendar.getInstance().getTimeInMillis()));
  }

  public void setNotificationPublisher(NotificationPublisher publisher) {
    this.publisher = publisher;
  }
}


On notera l’utilisation de l’interface NotificationPublisherAware afin de se faire injecter le NotificationPublisher par Spring.

JMX Notifier Service (client) :

public class JmxNotifierServiceImpl implements InitializingBean {

  private String objectName;
  private String url;
  private JMXConnector jmxConnector;
  private JmxNotifier notifier;

  public void afterPropertiesSet() {
    try {
      ObjectName objectName = new ObjectName(this.objectName);
      JMXServiceURL jmxUrl = new JMXServiceURL(this.url);
      this.jmxConnector = JMXConnectorFactory.connect(jmxUrl);
      MBeanServerConnection mbsc = this.jmxConnector.getMBeanServerConnection();

      this.notifier = (JmxNotifier)MBeanServerInvocationHandler.newProxyInstance(
                                                                                           mbsc,
                                                                                           objectName,
                                                                                           JmxNotifier.class,
                                                                                           false);
    } catch (MalformedObjectNameException e) {
      System.err.println("Warning : could not connect to
                                     JMX server. JMX Notifications will not be sent.");
      System.err.println("Reason : " + e.getMessage());
    } catch (MalformedURLException e) {
      System.err.println("Warning : could not connect to
                                     JMX server. JMX Notifications will not be sent.");
      System.err.println("Reason : " + e.getMessage());
    } catch (IOException e) {
     System.err.println("Warning : could not connect to
                                     JMX server. JMX Notifications will not be sent.");
     System.err.println("It seems that remote server is
                                     not up and not running.");
    }
  }
}

  public void close() throws IOException {
    if ( this.jmxConnector != null ) {
      this.jmxConnector.close();
  }

  public void annonceDebu(String id) {
    if ( this.notifier != null ) {
      this.notifier.annonceDebut(id);
    }
  }
}


Une meilleure gestion des exceptions levées permettrait de gérer les cas où le serveur distant n’est pas disponible.

Côté serveur, il va vous falloir la configuration Spring suivante :

<context:annotation-config />

<bean id="jmxNotifier" class="JmxNotifierImpl" />

<bean id="registry" class="org.springframework.remoting.rmi.RmiRegistryFactoryBean" />

<bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean">
	<property name="locateExistingServerIfPossible" value="true" />
</bean>

<bean id="serverConnector" class="org.springframework.jmx.support.ConnectorServerFactoryBean">
  <property name="objectName" value="connector:name=rmi"/>
  <property name="serviceUrl" value="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/standaloneJmxServer"/>
  <property name="threaded" value="true"/>
	<property name="daemon" value="true"/>
</bean>

<bean class="org.springframework.jmx.export.MBeanExporter" lazy-init="false">
	<property name="autodetect" value="true" />
	<property name="namingStrategy" ref="namingStrategy" />
	<property name="assembler" ref="assembler" />
</bean>

<bean id="attributeSource" class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/>

<bean id="assembler" class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler">
	<property name="attributeSource" ref="attributeSource" />
</bean>

<bean id="namingStrategy" class="org.springframework.jmx.export.naming.MetadataNamingStrategy">
	<property name="attributeSource" ref="attributeSource" />
</bean>

Les points marquants sont la présence de la MBeanServerFactory, la ConnectorServerFactoryBean et la RMIRegistryFactoryBean.

Et c’est tout !
Votre code n’a plus qu’à appeler JMXNotifierService.
Après, à vous d’enrichir les 2 JMX Notifier en fonction du fonctionnel de votre application.

Les équipes responsables du monitoring n’ont qu’à connecter leur JConsole sur la JVM contenant le MBeanServer.

Tags :
août 05

Après avoir étudié les différents frameworks Java en vue de la génération de documents PDF (ici), entrainons-nous maintenant 2 exemples simples d’implémentation de la librairie iText.

Ce sample se divise en 2 parties :

  • la 1ère montre un exemple de génération de document PDF from scratch.
  • la 2ième partie va générer un document à partir d’un template que nous allons créer dans Open-Office.

Générer un document PDF ‘from scratch’ :

  • 1ère étape : générer un projet Java.

Pour cela, utilisons la commande mvn archetype:generate et choisissons de créer un maven-archetype-quickstart.
Dans notre exemple, le projet est configuré comme suit :

  <groupId>fr.infinit.sample.generationpdf</groupId>
  <artifactId>generationpdf</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <name>Sample Generation PDF</name>



  • 2ième étape : ajouter la dépendance Maven à iText.

Ajoutons le repository iText ainsi que la dépendance pour iText :

<repositories>
  <repository>
    <id>itextpdf.com</id>
    <name>Maven Repository for iText</name>
    <url>http://maven.itextpdf.com/</url>
  </repository>
</repositories>
<dependencies>
  <dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itextpdf</artifactId>
    <version>5.0.2</version>
    <scope>compile</scope>
  </dependency>
</dependencies>



  • 3ième étape : Créer un document PDF vide.

Nous allons utiliser la classe com.itextpdf.text.Document :

    /**
     * Crée un Document vide.
     *
     * @param file Chemin du fichier à créer.
     * @return Document s'il n'y a pas eu d'erreur.
     * @throws IOException s'il y a eu une erreur avec le nom de fichier fourni.
     * @throws DocumentException s'il y a eu une erreur côté iText.
     */
    private Document creerDocument(String file) throws DocumentException, IOException {
        Document document = new Document();

        PdfWriter pdfWriter = PdfWriter.getInstance(document, new FileOutputStream(file));
        pdfWriter.setPageEvent(this);
        // Préférence de lecture : 2 pages en colonne.
        pdfWriter.setViewerPreferences(PdfWriter.PageLayoutTwoColumnLeft);
        document.open();

        return document;
    }

Attention à ne pas oublier de fermer l’objet Document après avoir travaillé sur cet objet !


  • 4ième étape : Ajouter des informations Metadata.

L’ajout de ces informations se fait via la classe com.itextpdf.text.Document :

    /**
     * Ajout de données de type Metadata au document.
     *
     * @param document Document auquel il faut rajouter les metadatas.
     */
    private void addMetaData(Document document) {
        document.addTitle("Sample PDF");
        document.addSubject("Utilisant iText");
        document.addKeywords("Java, PDF, iText");
        document.addAuthor(System.getProperty("user.name"));
        document.addCreator(System.getProperty("user.name"));
    }



  • 5ième étape : Ajouter des lignes vides.

L’ajout de lignes vides se fait en ajoutant des paragraphes avec des espaces :

    /**
     * Ajoute une ligne vide number fois dans le Paragraph passé en paramètre.
     *
     * @param paragraph A remplir avec les lignes vides.
     * @param number Nombre de lignes vides à ajouter.
     */
    private void ajouterLigneVide(Paragraph paragraph, int number) {
        for (int i = 0; i < number; i++) {
            paragraph.add(new Paragraph(" "));
        }
    }



  • 6ième étape : Ajouter des paragraphes avec des polices différentes.

Commençons par définir différentes polices :

    /** Times Roman 18 Bold */
    private static final Font CATFONT = new Font(Font.getFamily("TIMES_ROMAN"), 18, Font.BOLD);

    /** Times Roman 12 Normal */
    private static final Font REDFONT = new Font(Font.getFamily("TIMES_ROMAN"), 12, Font.NORMAL);

    /** Times Roman 16 Bold */
    private static final Font SUBFONT = new Font(Font.getFamily("TIMES_ROMAN"), 16, Font.BOLD);

    /** Times Roman 12 Bold */
    private static final Font SMALLBOLD = new Font(Font.getFamily("TIMES_ROMAN"), 12, Font.BOLD);

Ensuite, ajoutons différents paragraphes à notre document :

    /**
     * Ajoute 4 paragraphes / - Titre du document. - Rapport généré par ... - Ce
     * document décrit ... - Ce document est ...
     *
     * @param document Document à enrichir
     * @throws DocumentException Si un problème survient lors de l'ajout des
     *             données.
     */
    private void addTitlePage(Document document) throws DocumentException {
        Paragraph preface = new Paragraph();

        // Ajout d'une ligne vide
        ajouterLigneVide(preface, 1);

        // Ecriture du header
        preface.add(new Paragraph("Titre du document", CATFONT));
        ajouterLigneVide(preface, 1);

        preface.add(new Paragraph("Rapport généré par: " + System.getProperty("user.name") + ", " + new Date(), SMALLBOLD));
        ajouterLigneVide(preface, 3);

        preface.add(new Paragraph("Ce document décrit quelque chose de très important ", SMALLBOLD));
        ajouterLigneVide(preface, 8);

        preface.add(new Paragraph("Ce document est une version préliminaire et n'est sujet à aucune restriction.", REDFONT));
        document.add(preface);

        // Debut d'une nouvelle page
        document.newPage();
    }


  • 7ième étape : Ajouter du contenu (Chapitre, Section, Sous-section, Anchor).

Les objets Anchor, Chapter, Section permettent de définir différentes zones de contenu dans un document.

Anchor anchor = new Anchor("Premier Chapitre", CATFONT);
anchor.setName("Premier Chapitre");

// Le 2ième paramètre est le numero du chapitre
Chapter catPart = new Chapter(new Paragraph(anchor), 1);

Paragraph subPara = new Paragraph("Sous-Catégorie 1", SUBFONT);
Section subCatPart = catPart.addSection(subPara);
subCatPart.add(new Paragraph("Coucou (pour ne pas mettre toto...)."));

subPara = new Paragraph("Sous-catégorie 2", SUBFONT);
subCatPart = catPart.addSection(subPara);
subCatPart.add(new Paragraph("Paragraphe 1"));
subCatPart.add(new Paragraph("Paragraphe 2"));
subCatPart.add(new Paragraph("Paragraphe 3"));


  • 8ième étape : Ajouter un tableau.

Un tableau se créé avec la classe com.itextpdf.text.pdf.PdfPTable.
Il se compose de com.itextpdf.text.pdf.PdfPCell :

/**
 * Crée un tableau de 3 colonnes et l'ajoute à la Section passée en
 * paramètre.
 *
 * @param subCatPart Section à remplir avec le tableau.
 * @throws BadElementException Si une erreur survient lors de l'ajout d'un
 *             élément dans la PdfPTable.
 */
private void createTable(Section subCatPart) throws BadElementException {
    // Creation d'une PdfPTable avec 3 colonnes
    final PdfPTable table = new PdfPTable(3);

    // Creation d'une PdfPCell avec un paragraphe
    final PdfPCell cell = new PdfPCell(new Paragraph("header with colspan 3"));

    // Changement du colspan de la PdfCell
    cell.setColspan(3);

    // Ajout de la PdfCell custom à la PdfPTable
    table.addCell(cell);

    // Ajout d'objets String à la PdfPTable
    table.addCell("1.1");
    table.addCell("2.1");
    table.addCell("3.1");
    table.addCell("1.2");
    table.addCell("2.2");
    table.addCell("3.2");

    // Ajout d'un espace entre la PdfPTable et l'élément précédent.
    table.setSpacingBefore(15f);

    subCatPart.add(table);
}


  • 9ième étape : Ajouter une liste à puce.

Une liste à puce dans un document PDF se créé avec l’objet com.itextpdf.text.List.
En voici un exemple :

/**
 * Crée une liste et l'ajoute dans la Section passée en paramètre.
 *
 * @param subCatPart Section à remplir avec la liste.
 */
private void createList(Section subCatPart) {
    final List list = new List(true, false, 10);
    list.add(new ListItem("Premier point"));
    list.add(new ListItem("Deuxième point"));
    list.add(new ListItem("Troisième point"));
    subCatPart.add(list);
}


  • 10ième étape : Ajouter des numéros de pages.

L’ajout des numéros de page se fait en utilisant la classe com.itextpdf.text.pdf.PdfPageEventHelper.
Elle s’utilise en l’ajoutant comme PageEvent sur le PdfWriter (méthode setPageEvent).
Il suffit ensuite de surcharger les méthodes que l’on souhaite utiliser : onOpenDocument, onEndPage, etc…

Dans notre exemple, la méthode onOpenDocument est utilisée pour créer un com.itextpdf.text.pdf.PdfTemplate qui contiendra le texte des numéros de page.

/**
 * Création du template destiné à recevoir les numéros de page.
 *
 * @param writer PdfWriter à utiliser.
 * @param document Document à enrichir.
 */
@Override
public void onOpenDocument(PdfWriter writer, Document document) {
    total = writer.getDirectContent().createTemplate(100, 100);
    total.setBoundingBox(new Rectangle(-20, -20, 100, 100));
    try {
        helv = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.WINANSI, BaseFont.NOT_EMBEDDED);
    } catch (DocumentException e) {
        LOG.error(e.getMessage(), e);
        // TODO : gérer l'exception.
    } catch (IOException e) {
        LOG.error(e.getMessage(), e);
        // TODO : gérer l'exception.
    }
}

Note : total est un com.itextpdf.text.pdf.PdfTemplate.

La méthode onEndPage est responsable du remplissage du PdfTemplate pour chaque page via un com.itextpdf.text.pdf.PdfContentByte.

Dans l’exemple suivant, les numéros de page paires sont écrits en bas à gauche et les impaires sont écrits en bas à droite :

/**
 * Ajoute des numeros de page automatiquement. Les numeros paires sont mis
 * en bas à gauche. Les numéros impaires sont mis en bas à droite.
 *
 * @param writer Pdfwriter à utiliser.
 * @param document Document à utiliser.
 */
@Override
public void onEndPage(PdfWriter writer, Document document) {
    LOG.info("Generation des numeros de page ...");
    final PdfContentByte directcontent = writer.getDirectContent();

    directcontent.saveState();
    final String text = "Page " + writer.getPageNumber();
    final float textBase = document.bottom() - 20;
    final float textSize = helv.getWidthPoint(text, 12);

    directcontent.beginText();
    directcontent.setFontAndSize(helv, 11);

    // Ecriture du numero de page a gauche pour les pages impaires.
    if ((writer.getPageNumber() % 2) == 1) {
        directcontent.setTextMatrix(document.left(), textBase);
        directcontent.showText(text);
        directcontent.endText();
        directcontent.addTemplate(total, document.left() + textSize, textBase);
    } else {
        // Ecriture du numero de page a droite pour les pages paires.
        final float adjust = helv.getWidthPoint("0", 12);
        directcontent.setTextMatrix(document.right() - textSize - adjust, textBase);
        directcontent.showText(text);
        directcontent.endText();
        directcontent.addTemplate(total, document.right() - adjust, textBase);
    }
    directcontent.restoreState();
}

Tags :
juil 20

Il existe diverses outils pour générer ou travailler sur des fichiers PDF de nos jours.
Intéressons-nous plus particulièrement aux librairies Java permettant de générer ou de parser des PDF sans avoir besoin d’un quelconque outil d’Adobe.


Aspose

Aspose est une société commerciale proposant 2 solutions payantes gravitant autour des problématiques PDF.
La 1ère, Aspose.Pdf for Java, conviendra a la plus part des utilisateurs. Cette librairie permet de lire, écrire ou manipuler des documents PDF. Celle-ci est limitée a la création de documents PDF : l’édition de documents existants n’est pas possible.
La 2ième, Aspose.Pdf.Kit for Java, englobe les fonctionnalités de la précédente mais l’enrichit de fonctionnalités avancées : gestion des champs des formulaires embarqués dans des documents PDF, édition de documents existants, etc…

Ces 2 solutions s’inscrivent dans une gamme de produits plus larges (documents Word, documents Excel, etc ..).

Points positifs :

  • 2 produits à des tarifs différents pour des utilisations différentes.
  • Intégration à une gamme de produits variés (Word, Excel, Powerpoint, etc…).

Points négatifs :

  • Produits payants, surtout la version ‘basique’ qui ne nécessite pas forcément une solution commerciale.

iText

iText est une solution Open-Source par Bruno Lowagie. Cette solution offre toutes les fonctionnalités classiques de génération de documents PDF, ainsi que la possibilité d’éditer des documents existants. Elle permet aussi de remplir les formulaires intégrés aux PDFs.
Attention : la licence d’iText a changé depuis Décembre 2009 (version 5.0). Désormais iText est sous licence Affero General Public License (AGPL).

Points positifs :

  • Le produit permet de faire tout ce qu’on attend d’une API de gestion des documents PDF.
  • Produit ayant une grande expérience (1998).
  • iText est très bon pour la génération de documents.

Points négatifs :

  • Documentation difficile à trouver: les tutoriaux ont été supprimés, désormais il faut acheter le livre ‘iText in Action’.
  • L’extraction de données n’est pas le fort d’iText.

PDFBox

PDFBox est un projet de la fondation Apache. Il permet la création de nouveaux documents PDF, la manipulation de documents existants et la possibilité d’en extraire des données.
Il ne permet pas de modifier ou de remplir les formulaires des documents PDF.

Points positifs :

  • Très bon en extraction de données (avec notamment une intégration Lucene Search Engine).
  • License Apache License v2.0.
  • Documentation nettement plus fournie qu’iText.

Points négatifs :

  • Ne gère pas les formulaires PDF.

Conclusion

  • Si votre projet utilise déjà un produit Aspose, autant garder votre environnement technique homogène.
  • Si votre principale activité est l’extraction de données (par exemple extraire des données de documents pour un moteur de recherche), alors jetez vous sur PDFBox qui est fait pour vous.
  • Si vous voulez rester ouvert à toutes les possibilités, prenez iText qui résoudras 95% de vous contraintes.
Tags :
fév 03

Suite à la mise en place de Spring Batch 2.1 chez un client, voici quelques recommandations afin d’éviter une catastrophe technique.

  1. Est-ce que votre traitement est un Batch ?
  2. Il faut savoir que Spring Batch a été développé dans la logique des Batchs tels qu’ils existaient dans les gros systèmes : un Batch est une répétition en très grand nombre d’opérations unitaires (work unit).
    Le mode de fonctionnement de Spring Batch est très lié à ce modèle.
    Il est donc complètement inutile de se lancer dans l’implémentation de ce framework si l’organisation de votre traitement ne suit pas ce modèle.
    Je pense notamment à des traitements qui executeraient des updates SQL massifs : il ne sert à rien de construire une suite de Steps qui lanceront chacunes un update massif. Il faut construire une Step unique qui lancera un traitement complet sur un élément unitaire (appelé chunk dans le langage Spring Batch).

  3. Avez-vous une gestion de la Persistance ‘Maison’ ?
  4. Pour bénéficier de la puissance de Spring Batch, il est important que votre couche de persistance soit homogène : elle doit être full Hibernate, full iBatis ou full Spring JDBC et ne pas comporter d’éléments ‘homemade’.
    La gestion des transactions dans le batch est déléguée au TransactionManager, le développeur n’a plus la main dessus.

  5. Envie de paralléliser des exécutions de requêtes SQL ?
  6. Attention !
    Si vous souhaitez multithreader des requêtes potentiellement longues avec Spring Batch, il vous faudra mettre en place du JTA. En effet, votre TransactionManager va devoir gérer une transaction ‘chapeau’ qui gèrera elle-même une transaction par thread.
    D’où JTA. Ceci est dû au fait qu’une connection à la base de données ne peut pas être accédée simultanément par plusieurs threads (l’objet Connection n’est pas thread-safe).

Tags :
déc 23

Lorsque l’on utilise Eclipse et Maven, il est très courant d’utiliser le plugin Maven Eclipse.

Je ne compte plus les fois où je dois travailler avec plusieurs versions d’un même projet Eclipse dans le même workspace Eclipse. Or Eclipse interdit d’importer 2 fois un projet du même nom.

Voici un paramètre du plugin permettant de choisir le nom du projet Eclipse généré à partir de Maven :

mvn -Declipse.projectNameTemplate=[artifactId]-1.3 eclipse:eclipse

Cette commande va créer un projet nommé toto-1.3toto est le nom du projet maven.
Vous pouvez aussi utiliser les balises [groupId] et [version].

Tags :

Creative Commons License
Blog Infin-It par Infin-It est mis à disposition selon les termes de la licence Creative Commons Paternité-Pas d'Utilisation Commerciale 2.0 France.
preload preload preload