Il s'agit d'un problème récurrent lorsque l'on développe une application JEE et pour lequel on n'est jamais vraiment au point : "comment gérer la configuration de mon application sur les différents environnements ?" Pourtant on dispose de tous les ingrédients pour y parvenir, et Maven n'est pas forcément le seul...
J'exagère un peu mais depuis qu'on développe des applications JEE, on doit gérer différents environnements :
La plupart du temps, on maintenait plusieurs fichiers properties et au moment de générer le package, c'était la galère : on prenait le bon fichier ("quel environnement je livre déjà ?"), on le copiait dans le bon répertoire ("euh, quel environnement j'ai dit ?"). Avec Ant, on s'en sortait plus ou moins automatiquement mais au prix de nombreuses lignes d'XML!
... sans s'presser ! Et surtout sans rien révolutionner. Du moins dans les premiers temps car on refaisait avec Maven 1 (à coup de scripts jelly) ce qu'on faisait avant avec Ant. Mais l'arrivée des profiles a changé la donne !
Je ne vais pas refaire le chapitre 11 de l'excellent livre de sonatype ni reprendre la doc Maven à 0 mais pour résumer, il est possible d'avoir différent profils, qui comme leurs noms l'indiquent, permettent de définir plusieurs configurations et donc de gérer plusieurs environnements. Pour chaque profile, on peut définir des dépendances particulières, des plugins particuliers ... mais surtout un build particulier et notamment les ressources (voir ici pour le détail du contenu de la balise profiles).
Le plus simple pour comprendre est encore de prendre un exemple :
<project>
[...]
<profiles>
<profile>
<id>dev</id>
<activation>
<activebydefault>true</activebydefault>
</activation>
<build>
<resources>
<resource>
<directory>${basedir}/src/main/resources_dev</directory>
</resource>
</resources>
[...]
</build>
[...]
</profile>
<profile>
<id>prod</id>
<build>
<resources>
<resource>
<directory>${basedir}/src/main/resources_prod</directory>
</resource>
</resources>
[...]
</build>
[...]
</profile>
</profiles>
</project>
Et on a donc une arborescence qui ressemble à ça :
MyProject | - src
| - main
| - resources
- common.properties | - resources_dev
- specific.properties | - resources_prod - specific.properties
Ainsi, sur votre poste de travail, vous n'aurez qu'à faire : mvn package
pour que Maven sélectionne (par défaut : activeByDefault
) le profile de dev et donc avoir en sortie les fichiers common.properties (qui provient par convention du répertoire src/main/resources) et specific.properties provenant du répertoire src/main/resources_dev.
Ensuite, lorsque vous devrez livrer votre application, vous devrez sélectionner le profile de prod et donc exécuter mvn package **-Pprod**
. En sortie, vous aurez toujours votre common.properties, mais par contre vous aurez le specific provenant de src/main/resources_prod.
Avantage : On peut sélectionner différents fichiers de configuration en fonction de l'environnement sur lequel on veut déployer.
Inconvénients :
Il s'agit là encore d'une nouvelle possibilité bien pratique de Maven, filtrer les différentes propriétés de vos ressources. En clair, vous avez :
${mavariable}
.Exemple : je reprends l'exemple fourni par sonatype dans son livre, vous pouvez l'avoir en détail ici.
Vous avez un fichier Spring qui définit une datasource, et comme je le disais des variables ... variables :
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="someDao" class="com.example.SomeDao">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" destroy-method="close"
class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>
Pour faire simple, ce fichier se trouve dans le répertoire par défaut src/main/resources. Et donc associé à cela, vous avez évidemment le pom.xml magique :
<project>
[...]
<!--
On définit ici les valeurs par défaut des propriétés
qui apparaîtront dans le fichier Spring.
-->
<properties>
<jdbc .driverClassName>com.mysql.jdbc.Driver
</jdbc><jdbc .url>jdbc:mysql://localhost:3306/development_db
</jdbc><jdbc .username>dev_user
</jdbc><jdbc .password>s3cr3tw0rd
</jdbc></properties>
[...]
<!--
On est obligé de redéfinir la ressource car on active le filtrage.
Cet élément est indispensable puisqu'il permet de dire à Maven
que les ressources se trouvant dans ce répertoire doivent être
filtrées, c'est à dire parsées pour remplacer les variables par
des valeurs.
-->
<build>
<resources>
<resource>src/main/resources</resource>
<filtering>true</filtering>
</resources>
</build>
[...]
<!--
On peut ensuite définir des valeurs particulières en fonction
d'un profile, et donc d'un environnement particulier.
-->
<profiles>
<profile>
<id>production</id>
<properties>
<jdbc .driverClassName>oracle.jdbc.driver.OracleDriver
</jdbc><jdbc .url>jdbc:oracle:thin:@proddb01:1521:PROD
</jdbc><jdbc .username>prod_user
</jdbc><jdbc .password>s00p3rs3cr3t
</jdbc></properties>
</profile>
</profiles>
</project>
Avantage par rapport à la solution précédente : On ne maintient qu'un seul fichier, tout est dans le pom.xml.
Inconvénients : Par contre, on se retrouve avec les autres problèmes : un package testé avec un profile peut ne pas marcher avec un autre profile. De même, il vous faudra connaître l'environnement pour configurer correctement l'application.
Les profiles, c'est pratique, ça permet de simplement gérer différents environnements (simplement mais avec pas mal de XML supplémentaire dans le pom). Le problème, nous l'avons vu, c'est que cela nécessite de connaître les environnements sur lesquels sera déployée l'application, chose relativement rare dans les grandes sociétés, où la production gère la plupart des environnements.
Remarque 1 : Je ne comprends pas cette page de Maven où les profiles et le plugin antrun sont décrits comme étant le moyen de gérer divers environnements. Effectivement, c'en est un, je viens de le décrire mais il y a tout de même mieux... ne serait-ce qu'en n'utilisant pas le plugin antrun !
Remarque 2 : Les IDE ne gèrent pas (ou très mal) les profiles, donc si vous avez une configuration relativement complexe, vous devrez absolument avoir la ligne de commande à côté de l'éditeur pour vous en sortir.
Et oui, depuis tout ce temps, on avait la solution sous les yeux sans vraiment s'en servir. JEE fournit une extension pour configurer correctement votre application : Java Naming and Directory Interface (JNDI). Cette extension permet de configurer votre application directement sur le serveur d'application. Je ne vais pas entrer dans les détails de JNDI, ce n'est pas l'objet de ce billet mais sachez simplement que vous pouvez configurer une url qui pointe vers un fichier et par exemple votre ficher de configuration.
Soit en standard avec le lookup :
try {
URL configURL = (URL)initialContext.lookup("java:comp/env/url/MyAppConfig");
InputStream inputstream = configURL.openStream();
Properties properties = new Properties();
properties.load(inputstream);
} catch (Exception e) { // ... }
Soit si vous utilisez spring :
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<ref bean="appConfig" />
[...]
</list>
</property>
</bean>
<bean id="appConfig" class="org.springframework.core.io.UrlResource">
<constructor -arg ref="appConfigJndiUrlResource"/>
</bean>
<bean id="appConfigJndiUrlResource"
class="org.springframework.jndi.<strong>JndiObjectFactoryBean">
<property name="jndiName" value="java:comp/env/url/MyAppConfig" />
</bean>
Bien entendu on référence la ressource dans le web.xml :
<resource -ref>
<res -ref-name>myAppConfigResource</res>
<res -type>java.net.URL</res>
<jndi -name>java:comp/env/url/MyAppConfig</jndi>
</resource>
Tout dépend du serveur dont vous disposez mais vous n'avez qu'à configurer la resource URL url/MyAppConfig vers le fichier properties qui convient (ex : file:/usr/Websphere/6.0/config/specific.properties).
Évidemment, il faut déployer le fichier au bon endroit sur le serveur et donc le livrer avec l'application mais en dehors de l'ear ou du war et c'est là que Maven entre à nouveau en jeu !
Le plugin assembly de Maven permet de générer un paquetage (pour ne pas dire package) zip, targz... regroupant un ou plusieurs packages et des fichiers divers (et notamment ceux qui nous intéressent, les properties). Encore une fois, je ne saurais vous conseiller le livre de sonatype pour les détails sur la construction d'assemblies. Ici, je vais simplement construire un zip contenant l'ear et les fichiers properties externalisés.
Dans un premier temps, il faut créer un projet supplémentaire :
<project>
[...]
<dependencies>
<dependency>
<groupid>com.octo</groupid>
<artifactid>project-ear</artifactid>
<version>1.2</version>
<type>ear</type>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactid>maven-assembly-plugin</artifactid>
<version>2.2-beta-2</version>
<executions>
<execution>
<id>create-zip-package</id>
<phase>package</phase>
<goals>
<goal>attached</goal>
</goals>
<configuration>
<descriptor>
<strong>src/main/assembly/dep.xml</strong>
</descriptor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Ce pom dépend de l'ear et précise qu'il a besoin du plugin assembly et d'un fichier dep.xml décrivant comment sera fait l'assemblage :
<assembly>
<id>project-assembly</id>
<formats>
<format>zip</format>
</formats>
<filesets>
<fileset>
<directory>../project-war/src/main/resources</directory>
<outputdirectory>configuration</outputdirectory>
<includes>
<include>**/*.properties</include>
</includes>
</fileset>
</filesets>
<dependencysets>
<dependencyset>
<scope>runtime</scope>
<outputdirectory>/</outputdirectory>
<unpack>false</unpack>
</dependencyset>
</dependencysets>
</assembly>
Ce petit fichier dep.xml nous dit qu'il faudra prendre les fichiers properties se trouvant dans les resources du projet war pour les mettre dans le répertoire configuration : c'est le fileSets
. Il nous dit également qu'il faudra prendre les dépendances (l'ear) et le mettre à la racine sans le décompresser (remarque : unpack
false est la valeur par défaut) : c'est le dependencySets
.
En sortie de ce projet, après avoir saisi la commande mvn assembly:assembly
, vous aurez un zip contenant l'ear à la racine et les fichiers resources dans un répertoire configuration. L'ear est donc indépendant de l'environnement, et la production se chargera de configurer les spécificités de chaque plateforme (base de données, resources spécifiques... et bien sûr le jndi qui vous permettra d'aller chercher cette configuration). Bien entendu, il faudra penser à livrer un minimum de documentation pour que la production puisse faire ce travail efficacement.
Maven fournit de nombreux moyens de gérer plusieurs environnements (mes exemples étaient simples, on peut pousser la configuration bien plus loin). Il faudra donc choisir celui qui s'adapte le plus à votre contexte :
A défaut d'employer Maven pour générer un package avec le plugin assembly, vous pouvez toujours utiliser JNDI et fabriquer vous même le zip avec l'ensemble des binaires et de la config.
Après, si on est dans un process d'intégration continue un peu poussé (cf. mon billet sur l'intéraction avec la production), on pourra songer à utiliser Maven, relativement agréable une fois mis en place.