Toute application d'une certaine envergure expose un certain nombre des variétés de son exécution sous forme de paramètres, chacun composé d'un nom associé à une valeur. Il est commun que ces paramètres soient regroupés dans un fichier, dit "de configuration" que l'application lira au démarrage. On peut y trouver de tout : la couleur du fond d'écran, l'URL de connexion à une base de donnée (et son user/password), la taille du cache, le nom d'une classe concrète à utiliser pour rendre un service donné, l'âge du capitaine, etc.
Pour une application Java, ce fichier de configuration sera le moins souvent un fichier XML, et le plus souvent un fichier plat, les fameux fichiers
Oui mais voilà, ces propriétés sont accessibles par une clé qui n'est qu'une simple String, et disponibles sous la forme d'une valeur qui, également, n'est qu'une simple String. Ne pourrait-on pas faire mieux, tout en restant simple ?
Si, bien sûr...
Pour une application Java, ce fichier de configuration sera le moins souvent un fichier XML, et le plus souvent un fichier plat, les fameux fichiers
.properties
. Pour faciliter son traitement, Java offre la classe Properties
éponyme avec les moyens de lire (ou de sauvegarder après modification) ces propriétés.
Oui mais voilà, ces propriétés sont accessibles par une clé qui n'est qu'une simple String, et disponibles sous la forme d'une valeur qui, également, n'est qu'une simple String. Ne pourrait-on pas faire mieux, tout en restant simple ?
Si, bien sûr...
Une propriété bien peu pratique d'accès |
Un exemple
Voici un exemple typique de fichier de propriété, le fichier "
Et bien moi, ce que je voudrais, c'est pouvoir charger ce fichier dans un objet qui reflète son contenu : les membres de cet objet seraient les noms des propriétés, et les valeurs seraient des objets typés. Par exemple, si je chargeais ce fichier dans une instance "conf" comme ceci :
...je voudrais ensuite pouvoir y accéder comme cela :
Ci-dessus, une fois l'objet
conf.properties
" :
# Properties definining the GUI gui.window.width = 500 gui.window.height = 300 gui.colors.background = #FFFFFF gui.colors.foreground = #000080 # Properties definining datasources service.url = https://some-services.example.org/someService service.uuid = 5da66c77-7062-4b30-97fc-e747eb64570a db.driver = com.fakesql.jdbc.Driver db.url = jdbc:fakesql://localhost:3306/localdb?autoReconnect=true db.account.login = theLogin db.account.password = thePassword
Et bien moi, ce que je voudrais, c'est pouvoir charger ce fichier dans un objet qui reflète son contenu : les membres de cet objet seraient les noms des propriétés, et les valeurs seraient des objets typés. Par exemple, si je chargeais ce fichier dans une instance "conf" comme ceci :
// unmarshall the property file to an org.example.Conf object Conf conf = Conf.unmarshall(new FileInputStream("conf.properties"));
...je voudrais ensuite pouvoir y accéder comme cela :
// look, we have an int : int width = Math.min(conf.gui.window.width, 400); // we can also use typed data in place : Dimension size = new Dimension(width, conf.gui.window.height); // look, we have an URI : URLConnection extService = conf.service.url.toURL().openConnection(); // we can also supply an adapter for other types // such as java.awt.Color Color darkBg = conf.gui.colors.background.darker(); // darker() is a method of java.awt.Color // you can handle groups of fields at once Connection dbConnection = getConnection(conf.db); // in fact, the class org.example.Conf wraps the class org.example.Conf.Db and others
Ci-dessus, une fois l'objet
conf
obtenu, l'accès à la propriété dans le code Java utilise le même chemin que le nom de la propriété dans le fichier, tel que conf.gui.window.height
, et son type est celui attendu, à savoir une valeur numérique.
Génération de la classe Conf
Comme je considère que le fichier de configuration ci-dessus se suffit (presque) à lui-même, un petit générateur de notre classe cible
Voyons comment l'utiliser.
Bien qu'il soit possible de se passer de Maven, le plus simple est d'utiliser le générateur prévu à cet effet. Dans votre projet, ajoutez la dépendance suivante :
...qui vous permettra de charger le fichier de configuration, et le plugin suivant :
...qui vous permettra de générer la classe cible.
A cette fin, copiez votre fichier de propriétés "
Si vous utilisez un IDE, faites en sorte que ce dernier répertoire fasse partie des sources à compiler, de sorte qu'il vous soit possible d'utiliser immédiatement cette classe dans votre projet.
Conf
devrait être facile à réaliser. Je l'ai donc fait.
Voyons comment l'utiliser.
Bien qu'il soit possible de se passer de Maven, le plus simple est d'utiliser le générateur prévu à cet effet. Dans votre projet, ajoutez la dépendance suivante :
<dependency> <groupId>ml.alternet</groupId> <artifactId>alternet-tools</artifactId> <version>1.0</version> </dependency>
...qui vous permettra de charger le fichier de configuration, et le plugin suivant :
<plugin> <groupId>ml.alternet</groupId> <artifactId>prop-bind-maven-plugin</artifactId> <version>1.0</version> <executions> <execution> <?m2e execute?> <id>generate</id> <goals> <goal>generate</goal> </goals> </execution> </executions> </plugin>
...qui vous permettra de générer la classe cible.
A cette fin, copiez votre fichier de propriétés "
conf.properties
" dans "${basedir}/src/main/properties/conf.template.properties
", ce qui aura pour effet de produire le code source de la classe Conf
dans “${project.build.directory}/generated-sources/prop-bind
” lors de la compilation (pour être précis, lors de la phase generate-sources de Maven).
Si vous utilisez un IDE, faites en sorte que ce dernier répertoire fasse partie des sources à compiler, de sorte qu'il vous soit possible d'utiliser immédiatement cette classe dans votre projet.
Modifications du template
Pour que cela fonctionne correctement, il faut tout de même opérer quelques modifications : à minima, les mots de passe ne devraient pas être conservés dans le template, mais remplacés par des "
Il faut aussi préciser le nom qualifié de la classe à générer ; cela se fait en ajoutant une propriété spéciale au début du fichier :
"
Pour le reste du fichier, le générateur va créer les propriétés avec le type approprié en fonction de ce qui est mis dans le template. Typiquement, si une valeur du template contient un entier, le type généré sera
Dans notre template, nous avons
Si vous voulez forcer le type, vous pouvez toujours modifier le template comme ceci :
Avez-vous remarqué l'usage de
Par exemple, nous pouvons associer une propriété à une classe donnée :
Dans ce template,
Pour autant, pour que la propriété ait la valeur appropriée, il faut indiquer comment la produire à partir d'une chaîne. Cela est pris en charge en ajoutant un "adapteur", qui n'est autre qu'une fonction qui prend en paramètre
Comme cette déclaration doit être faite dans le fichier de template, il existe une directive appropriée pour cela :
Pour les autres de types de données, le générateur reconnaît et utilisera naturellement les types URI, classes, fichiers, les dates et heures, et également les types énumérés en séparant les valeurs par un "
Et si le comportement par défaut ne vous plaît pas, vous pouvez toujours mettre la valeur entre double quote pour forcer le type chaîne de caractère.
*****
".
Il faut aussi préciser le nom qualifié de la classe à générer ; cela se fait en ajoutant une propriété spéciale au début du fichier :
. = #org.example.Conf
"
.
" est une clé spéciale du fichier de configuration utilisée pour piloter la génération du code avec quelques directives.
Pour le reste du fichier, le générateur va créer les propriétés avec le type approprié en fonction de ce qui est mis dans le template. Typiquement, si une valeur du template contient un entier, le type généré sera
int
, si une valeur contient true
ou false
, le type généré sera boolean
. Et si plusieurs valeurs sont séparées par des virgules, vous obtiendrez java.util.List<java.lang.Boolean>
Dans notre template, nous avons
gui.window.width = 500
, donc nous aurons un short
.
Si vous voulez forcer le type, vous pouvez toujours modifier le template comme ceci :
gui.window.width = $int 500
.
Avez-vous remarqué l'usage de
$
, contrairement au #
vu précédemment ? En règle générale, $
sert à marquer les types existants (y compris les primitives comme int
) et #
sert à marquer les types à générer.
Par exemple, nous pouvons associer une propriété à une classe donnée :
gui.colors.background = $java.awt.Color #FFFFFF
Dans ce template,
#FFFFFF
n'est plus qu'une valeur indicative, un exemple de valeur permise dans le fichier de configuration.
Pour autant, pour que la propriété ait la valeur appropriée, il faut indiquer comment la produire à partir d'une chaîne. Cela est pris en charge en ajoutant un "adapteur", qui n'est autre qu'une fonction qui prend en paramètre
java.lang.String
pour créer l'instance du type attendu. En pur Java, nous écririons ceci à l'aide de l'utilitaire Adapter
fourni :
Adapter.map(Color.class, Color::decode) // when a Color type is expected, //parse the text with Color.decode()
Comme cette déclaration doit être faite dans le fichier de template, il existe une directive appropriée pour cela :
. = @Adapter.map(Color.class, Color::decode)
Pour les autres de types de données, le générateur reconnaît et utilisera naturellement les types URI, classes, fichiers, les dates et heures, et également les types énumérés en séparant les valeurs par un "
|
" :
service.status = PENDING | ACTIVE | INACTIVE | DELETED
Et si le comportement par défaut ne vous plaît pas, vous pouvez toujours mettre la valeur entre double quote pour forcer le type chaîne de caractère.
Le fichier template final
Le fichier
conf.template.properties
final ressemble à ceci :
# Target class name . = #org.example.Conf # Required adapters . = @Adapter.map(Color.class, Color::decode) . = @Adapter.map(UUID.class, UUID::fromString) # Properties definining the GUI gui.window.width = $int 500 gui.window.height = 300 gui.colors.background = $java.awt.Color #FFFFFF gui.colors.foreground = $java.awt.Color #000080 # Properties definining datasources service.url = https://some-services.example.org/someService service.uuid = $java.util.UUID 5da66c77-7062-4b30-97fc-e747eb64570a db.driver = java:java.sql.Driver db.url = "jdbc:fakesql://localhost:3306/localdb?autoReconnect=true" db.account.login = theLogin db.account.password = *****
Autres fonctionnalités
Il existe quelques autres fonctionnalités dans le template :
- génération de type séparé (par défaut, tous les types générés sont imbriquées) :
gui.window. = #org.example.Gui
- support des types portant à la fois valeur et sous types :
gui.window = Sample application gui.window.width = $int 500
- support des valeurs dont la clé n'est pas connue à l'avance :
map.*.geo = $org.example.Geo 48.864716, 2.349014
- import explicite en cas de besoin :
. = !java.awt.Color
(dans notre example, cet import est inutile car le nom qualifié de la classe est mentionné par ailleurs dans le template) - génération de noms de propriétés compatibles avec Java
- fusion de fichiers de configuration multiples :
db = file:datasource/db.properties
- interpolation de variables :
gui.window.width = ${gui.window.height + 200}
- initialisation avec des valeurs par défaut
Voilà : ce générateur / chargeur de classe à partir d'un fichier de propriétés est un petit outil commode et facile à prendre en main :
- L'essentiel du code tient dans 2 classes
Binder
etGenerator
que vous pouvez consulter dans Github. - Et toute la documentation est disponible ici.
Semantic Mismatch ERR-19 : IllegalArgumentException Unable to set CORDE[] to ARC A single CORDE was expected -- In "Avoir plusieurs cordes à son arc" -- See log for details
Aucun commentaire:
Enregistrer un commentaire