Ressources

images/bluebelt.png

Il y a plusieurs niveaux possible pour mettre en place une architecture comportant des codes et des données spécifiques au déploiement. Nous avons vu la notion de service mais nous allons voir d’autres mécanismes relativement simples concernant les données (et apparentés à ce qui se faisait avant l’introduction des modules).

Il y a plusieurs manière de paramétrer des données destinées à un programme Java:

Arguments de la commande de lancement

Le lancement de l’application peut être confié à un script adapté aux conditions locales (script shell exécutable, fichier .bat sous Win* ,… ).

// ici un shell Unix
NOM_BANQUE=saltimbanque
# .... autre code shell
java $OPTIONS com.bankgalore.finance.Guichet $NOM_BANQUE $PORT_SERVEUR $BASE_DONNEES

Le main de l’application va récupérer les arguments et paramétrer des données ou des comportements adaptés.

Comme la plupart des paramètres de déploiement on a affaire à des Strings qui seront éventuellement transformées en valeur d’un autre type (entiers, double, etc.)

Ces paramètres sont "positionnels" et l’application doit documenter précisément la signification de chacun. Quand certains arguments sont optionnels cette façon de faire peut devenir compliquée.

Comme c’est au main (ou a des codes liés au main) qu’il revient de gérer ces paramètres il se pose le problème de la communication entre ce point d’entrée de l’application et les composants qui ont besoin de paramètres. Cela peut signifier que le main "connait" ces composants et peut leur communiquer des données. Architecturellement ce peut être difficile (voire impossible, ou non souhaitable).

Propriétés nommées

Il y a un moyen de passer des données depuis la ligne de commande directement vers des composants sous-jacents et ceci au travers de "propriétés nommées":

java -Dcouleur=0033FF -Dpolice=courier -Dtaille=400x500 com.bankgalore.finance.Guichet

Ces paramètres seront directement récupérés par les composants logiciels qui en ont besoin (bien entendu il faudra quand même documenter ces possibilités et les faire connaître au déploiement).

   String hexCouleur = System.getProperty("couleur", "C0C0C0") ;
   // getProperty (clef, valeur par défaut)
   // ici c'est du gris clair
   int couleur = Integer.parseInt(hexCouleur, 16) ;

   // un raccourci intéressant
   int taillePolice = Integer.getInteger("taillePolice", 12) ;

Dans un code qui exploite de nombreuses informations de ce type:

   java.util.Properties props = System.getProperties() ;
   String hexCouleur = props.getProperty("couleur", "C0C0C0") ;
   //  ..... ainsi de suite

System.getProperties(): noter l’existence d’un ensemble de propriétés qui sont initialisées par la JVM elle-même

(noter sur cet exemple qu’il important que les propriétés aient un nom hiérarchique -en général lié au nom de package- ceci permet d'éviter des ambiguités quand deux composants indépendants l’un de l’autre réclament une propriété qui porte un nom identique. Donc, même si c’est lourd, ne pas hésiter à nommer une propriété com.bankgalore.finance.ihm.couleur).

Fichier de configuration

Les objets java.util.Properties disposent d’une méthode pour charger des paires clef-valeur depuis un flot (stream) de texte. voir la documentation de la méthode load de Properties (noter qu’il y a également une méthode loadFromXML avec une syntaxe particulière du texte dans le fichier).

Le format standard de fichier .properties est documenté ici.

Maintenant si pour configurer une classe Produit on a besoin d’un fichier taxes.properties va-t’on déployer le fichier?

  • Le déploiement doit être indépendant de la plate-forme: on ne doit pas écrire un manuel qui dirait "sous Windows il est dans C:\Program Files\MonAppli\config\taxes.properties. et sous Solaris dans /opt/MonAppli/config/taxes.properties , etc."
  • Les données devraient être physiquement liées aux composants qu’elles paramètrent: si on charge dynamiquement un code depuis un serveur alors le fichier est sur le serveur pas sur le poste client! (Le cas contraire est possible mais demande un code très spécial avec plusieurs ClassLoaders)
  • Les données doivent être logiquement liées aux composants qu’elles paramètrent: donc si la classe Produit est dans le package com.monbiz.produits alors la ressource doit être à un endroit qui est logiquement dépendant de ce package.

    Nous aurons deux sortes de désignation de la ressource:

    • la désignation relative: par ex. config/taxes.properties est relative au package de la classe Produit qui la recherche
    • désignation absolue: par ex. /com/monbiz/produits/config/taxes.properties . Ici la désignation commence par "/"

Déploiement en portée locale

Rappel: nous avions utilisé ce terme (non standard) de "portée locale" pour caractériser les codes qui s’exécutent dans un environnement non-modulaire ou à l’intérieur même d’un module.

Le ClassLoader qui a chargé la classe Produit sait où trouver cette classe, où trouver le package correspondant ET où trouver les ressources associées! Les services du ClassLoader peuvent être indirectement accédés depuis l’objet de type Class qui représente la classe de Produit.

package com.monbiz.produits ;
//  imports divers
public class Produit {

   public static Properties taxProps = new Properties() ;

   // un BLOC STATIQUE pour initialiser une donnée statique
   static {
      // ATTENTION: en anglais ressource ne prend qu'un seul "s" !
      // Produit.class est un objet de type java.lang.Class
      try (
          InputStream is = Produit.class.getResourceAsStream("taxes.properties") ) {
         taxProps.load(is) ;
      } catch (Exception exc) {
          // au RAPPORT, puis EXIT
          // on peut considérer que c'est une erreur fatale
      }
   }

Dans ce cas taxes.properties devrait être:

  • soit là ou se trouve Produit.class (par exemple par rapport à un répertoire rattaché au CLASSPATH dans le sous répertoire com/monbiz/produits
  • soit dans un autre sous-répertoire de désignation com/monbiz/produits accessible depuis le CLASSPATH (par exemple dans une autre archive jar que celle qui contient Produit.class).

Une meilleure stratégie serait de nommer la ressource config/taxes.properties et de mettre le fichier taxes.properties dans un cheminom com/monbiz/produits/config. config peut être un pseudo-package situé dans un jar de déploiement distinct du jar applicatif qui contient la classe Produit. (Les jars gérés par les équipes de développement applicatifs ne doivent pas être modifiés!).

[Note]

images/blackbelt.png Les spécifications de Java sont malheureusement imprécises en ce qui concerne l’ordre de recherche des ressources. Implicitement les ClassLoaders devraient assurer une relation d’ordre entre les jars: ainsi les ressources contenues dans un jar "prioritaire" devraient avoir priorité sur une ressource "par défaut" placée dans une librairie. (On pourrait ainsi avoir une configuration par défaut qui est remplacée par une configuration personnalisée). Il y a une relation d’ordre certaine entre ce qui est géré par le ClassLoader "parent" et les ClassLoaders "enfants" et il devrait y en avoir aussi une entre les jars gérés par un même ClassLoader. Malheureusement certains ClassLoaders n’appliquent pas cette dernière règle et il s’ensuit que des systèmes de configuration ont alors recours à des mécanismes particuliers … ce qui est dommageable pour l’application d’un standard. Avantage de l’architecture modulaire: il ne peut pas y avoir de duplication de nom de package; un même package ne peut pas exister dans deux modules différents!

(Voir aussi l’option -order-resources de jlink)

Déploiement de ressources avec des modules

Ici les choses se compliquent un peu car il n’y a pas (pas encore!) de stratégie standard avec l’architecture modulaire. La méthode getResourceAsStream de Class ne convient pas pour des requêtes traversant des frontières de modules. (ce sera un problème récurrent avec des méthodes dont l’exécution suppose d’opérer au travers de frontières de modules. Certains codes de java vérifient l’origine de l’invocation pour bloquer certains appels)

La première option est de demander au ClassLoader de rechercher la ressource. (cela a l’avantage de fonctionner - à peu près - que ce soit en architecture modulaire ou non).

Les ClassLoaders ne recherchent pas une ressource de la même manière qu’une classe: il faut leur fournir un chemin d’accès depuis la racine des packages. Donc dans notre exemple le chemin serait "com/monbiz/produits/config/taxes.properties"

package com.monbiz.produits ;
//  imports divers
public class Produit {

   public static Properties taxProps = new Properties() ;

   // un BLOC STATIQUE pour initialiser une donnée statique
   static {
      try (
          ClassLoader classLoader = Produit.class.getClassLoader() ;
          String name = Produit.class.getPackage().getName().replace('.','/') ;
          InputStream is = classLoader.getResourceAsStream(name +"/config/taxes.properties") ) {
         taxProps.load(is) ;
      } catch (Exception exc) {
          // au RAPPORT, puis EXIT
          // on peut considérer que c'est une erreur fatale
      }
   }

Attention: ceci peut supposer que la lecture des données se fera dans un autre module et dans ce cas il faudra permettre cette lecture par des directives open situées dans le module-info du module fournisseur.

module com.monbiz.produits.config {
   ...
   // directive opens
   opens com.monbiz.produits.config ;
}

ou

//tout le module est "ouvert"
open module com.monbiz.produits.config {
  ...
}
[Avertissement]

La mise à contribution du ClassLoader pour rechercher une ressource n’est pas de portée générale. Elle est limitée à des cas simples. Nous verrons ultérieurement le cas des architectures avancées où il faudra éviter cette solution.

Il pourrait être préférable d’avoir une définition de Service comme:

public interface PropertiesFactory {
   Properties get(String domain) ;
}

Les codes rendant le service pourront alors rechercher localement (dans l’espace de leur propre module) les données à lire.

Contenu de fichier Properties

Le contenu de la ressource config/taxes.properties peut ressembler à ceci :

# taxes properties  : configuration des taxes de produit
#
# les taxes sont décrites sous forme de facteur multiplicatif
# donc 1.196  est pour 19.6 %
#

Livre = 1.055
Disque = 1.196
JeuVideo = 1.196

et le montant d’un taux de taxe peut être récupéré par:

   String valStr = Product.taxProps.getProperty(typeProduit) ;
   BigDecimal taxVal = new BigDecimal(valStr) ;

Mais attention s’il y a une erreur de saisie dans ce fichier. Par exemple:

com.autrebiz.smurf = 1.169

Et bien cette erreur ne sera pas détectée de sitôt!

Il serait plus sûr d’utiliser des valeurs symboliques que l’on pourrait tester.

// ceci est un CODE DE DEPLOIEMENT
// destiné à être modifié par des programmeurs de déploiement
// on pourrait encore le compliquer en lui  faisant utiliser sa propre ressource

// dans un package distinct
package com.monbiz.produits.config ;

//Noter la technique de l'enum avec un constructeur!
// et les constantes qui invoquent ce constructeur
public enum  TauxTaxe{
   // liste des instances légales
   REDUIT("1.055") , NORMAL("1.196"), LUXE("1.33") ;

   private BigDecimal value ; // champs d'instance

   TauxTaxe(String vl) { this.value = new BigDecimal(vl) ; }

   public BigDecimal getValue() { return this.value ; }
}

Maintenant le fichier de configuration peut ressembler à ceci:

# taxes properties  : chaque produit est associé à un TauxTaxe
#
#

Livre = REDUIT
Disque = NORMAL
JeuVideo = NORMAL

Les valeurs sont récupérées (et testées!) par:

   // dans un bloc try/catch !
   String valStr = Product.taxProps.getProperty(categorieProduit) ;
   TauxTaxe taxe = TauxTaxe.valueOf(valStr) ;
   BigDecimal taux = taxe.getValue() ;

On voit ici que dans le cas d’une architecture avec module il serait intéressant de mettre ces codes dans le module de déploiement et qu’un service approprié pourrait alors rendre la valeur du taux de taxe! (Le service rendrait par exemple un Map<String,BigDecimal>)

[Note]

Un autre exemple de fichier .properties qui utilise des caractères UNICODE (il s’agit là d’un utilitaire qui permet de traduire des programmes sources écrits en chinois mais pour les traductions nous verrons plus loin une autre façon de faire)

\u5faa\u73af=for
\u6574\u6570=int

URL de ressources

Les types de ressources peuvent être divers et elles ne sont pas nécessairement chargées au travers d’un flot (stream). Une U.R.L (Uniform Resource Locator) peut être obtenue d’un ClassLoader.

Exemple: un objet javax.swing.ImageIcon peut être initialisé à partir de l’URL d’une image.

// ATTENTION code en portée seulement!
// import divers
import java.net.URL ;

public class IHMSociete extends JPanel { // code Swing !
   private static Class clazz = IHMSociete.class ;
   public IHMSociete(String imageRef) {
      super(new BorderLayout()) ; // IHM

      URL urlImage = clazz.getResource(imageRef) ;
      // this .getClass() est une source potentielle de bug
      // sauf si on veut que chaque sous-classe trouve sa ressource propre
      if(null != urlImage ) {
         Icon logoSociété = new ImageIcon(urlImage) ;
         //.......
      }
   }
}

Dans ce code imageRef est une chaîne qui fait partie d’une URL et qui doit être manipulée de la manière suivante:

  • Si imageRef est une désignation relative ( "maboite.gif" ou "images/maboite.gif"): alors l’URL résultante est construite à partir de l’URL de la classe. par exemple si la classe est chargée depuis: http://www.maboite.com/java/codebase/com/maboite/ihm/IHMSociete.class alors la nouvelle URL est: http://www.maboite.com/java/codebase/com/maboite/ihm/maboite.gif (ou ihm/images/maboite.gif)
  • Si imageRef est une désignation absolue ("/images/maboite.gif"): alors l’URL résultante est batie à partir du codebase. La nouvelle URL sera: http://www.maboite.com/java/codebase/images/maboite.gif
  • Comme mentionné dans le commentaire le code précédent est correct dans une situation où on ne traverse pas une frontière de module. Sinon il faudrait passer par le ClassLoader (avec une conventions de nommage en absolu mais ne commençant par "/") ou, mieux, par une définition de service.

    Toujours pareil: si la "lecture" de données traverse des frontières de modules (voir par ex. URL.openStream) cela exige des directives open d’autorisation dans le module.info du module "exportateur".

[Note]Note importante

En ce qui concerne les ressources il faut tenir compte des éléments suivants:

  • Il y a les ressources qui sont spécifiées avec le développement de l’application et celles qui le sont au déploiement. Ce dernier cas doit être traité soigneusement. A priori ces ressources de déploiement seront hébergées dans un jar différent (appelons le deploy.jar dans ce qui suit).

    • Il est fortement conseillé d’avoir ces données dans un package différent (le sous-package config dans les exemples précédents)
    • Après il y a deux cas de figure:

      • Les données abritées dans un module sans nom ou un module automatique.

        On peut rajouter ces jars au Class-Path (ou référencer leur répertoire par l’option --class-path ) ou les mettre dans un module automatique qui sera référencé par l’option --add-modules (voir les paramètres de cette option comme ALL-MODULE-PATH).

        Il est, aussi, possible d’utiliser l’option -patch-module de l’exécuteur java pour ajouter un jar de déploiement à l’exécution. Voici un exemple de patch dans le script UNIX d’une application générée par Jlink (le module com.maboite.withprops a été complété par les ressources d’un jar de déploiement nommé ici config.jar)

        #!/bin/sh
        DIR=`dirname $0`
        JLINK_VM_OPTIONS='--patch-module com.maboite.withprops=../conf/config.jar'
        $DIR/java $JLINK_VM_OPTIONS -m com.maboite.testing/com.maboite.testing.MainProps $@
      • les données abritées dans un vrai module: préférer alors une architecture de service.
  • Les URL de ressources peuvent prendre des formes différentes

    • des URL de jar de la forme jar:url:….!chemin (exemple jar:file:/Users/shadoko/java/config.jar!/com/maboite/withprops/config/values.properties )
    • des URL internes à un image générée par jlink (exemple: jrt:/com.maboite.withprops/com/maboite/withprops/config/vals.properties)

--exercices--