Dans ce chapitre, nous allons découvrir les annotations, un système introduit avec Java EE 5 qui va nous permettre de nous débarrasser de notre fichier web.xml !
Présentation
Pour commencer, parce qu'il est toujours utile de s'intéresser aux raisons d'être d'une technologie, nous allons étudier le sujet de manière globale et nous attarder quelques instants sur la théorie. Si vous connaissez déjà cette fonctionnalité, qui comme nous allons le voir existe depuis quelque temps déjà sur la plate-forme Java, vous pouvez survoler cette introduction et passer directement au paragraphe qui traite spécifiquement des annotations sous Java EE.
Une annotation est tout simplement une description d'un élément. Elle peut ainsi s'appliquer à un package, une classe, une interface, un constructeur, une méthode, un champ, un argument de méthode, une variable locale ou encore à une autre annotation.
Quel est l'intérêt de décrire ces différents éléments ?
Pour répondre entièrement à cette question, nous allons aborder différents aspects.
Écrire des méta-données
Vous avez peut-être déjà entendu parler de méta-données ou de méta-programmation. Il s'agit d'un concept très simple : une méta-donnée désigne une donnée qui donne une information sur une autre donnée. Voilà pour la définition littérale. Intéressons-nous maintenant à l'intérêt de ces méta-données dans le cadre de notre projet !
Premièrement, parlons Java. En réalité, Java a toujours proposé une forme de méta-programmation que vous connaissez tous mais que vous n'avez probablement jamais appelée ainsi : il s'agit de la Javadoc ! Eh oui, la Javadoc n'est, ni plus ni moins, qu'un outil permettant de décrire certaines portions de votre code… Dans ce cas, les méta-données sont utilisées par le développeur, qui va tout simplement lire les informations contenues dans la documentation.
Deuxièmement, abordons le cas de Java EE. Là encore, même constat : la plate-forme a toujours proposé une forme de méta-programmation par le biais du fameux fichier web.xml, dans lequel nous avons saisi les descriptions de certains composants de notre application. Dans ce cas, les méta-données sont utilisées par le conteneur, qui analyse le contenu du fichier au démarrage du serveur afin d'identifier quels composants web agissent sur quelles requêtes.
Ainsi, voici la définition que nous pouvons retenir : les méta-données sont de simples informations accompagnant le code d'une application, et qui peuvent être utilisées à des fins diverses.
Pallier certaines carences
Il y a quelques années, Java ne permettait pas encore de faire de méta-programmation de manière simple. La seule solution qui s'en approchait était la Javadoc et son système de tags. Elle permettait uniquement de rédiger de la documentation destinée au développeur. À l'époque, certaines solutions avaient alors vu le jour afin d'en détourner l'usage, notamment XDoclet qui permettait de définir des tags Javadoc personnalisés et de générer du code à partir des méta-données saisies dans ce semblant de documentation.
Reconnaissant les manques de la plate-forme à ce niveau et surtout le besoin d'un système plus robuste, plus flexible et surtout homogène, l'équipe en charge de l'évolution de Java a introduit le concept des annotations avec l’avènement du JDK 1.5, en 2004. Par ailleurs, sachez que cette version a probablement été la plus riche de toutes en termes d'apport de nouveautés : les annotations comme nous venons de le voir, mais également la programmation générique, la syntaxe for
adaptée au parcours de collections, les imports statiques, l'autoboxing, la notation varargs… Bref, les développeurs n'ont pas chômé !
C'est par ailleurs après cette mise à jour majeure du JDK que J2EE est devenu Java EE 5 en 2006, et c'est donc uniquement à partir de cette version que les annotations ont été rendues disponibles pour le développement d'applications web. Dans toutes les versions antérieures de la plate-forme (c'est-à-dire l'ensemble des versions estampillées J2EE), cette fonctionnalité n'existait pas.
Depuis Java EE 6, lancé fin 2009, le support des annotations est total et leur gestion fait partie intégrante du compilateur Java.
Simplifier le développement
La raison d'être des annotations est sans équivoque : elles permettent de simplifier considérablement le processus de développement et de maintenance d'une application. Pourquoi ? Principalement parce qu'une annotation est écrite directement dans le code, au sein d'une classe ou d'une méthode. Ainsi, l'information associée à un élément du code est accessible et visible directement. Il n'est pas nécessaire de se référer à des données écrites dans des fichiers externes. De même, lors de la phase de développement il n'est pas nécessaire de créer et maintenir ces fichiers externes.
Principe
Maintenant que nous savons de quoi il retourne, découvrons comment sont construites les annotations.
Syntaxe sans paramètres
Une annotation, sous sa forme la plus simple, est uniquement constituée d'un mot-clé précédé du signe @
. Nous allons prendre pour exemple l'annotation @Override
, que vous avez déjà pu observer dans le code de notre classe com.sdzee.dao.UtilisateurDaoImpl
:
1 2 3 4 5 6 7 8 9 | public class UtilisateurDaoImpl implements UtilisateurDao { /* Implémentation de la méthode trouver() définie dans l'interface UtilisateurDao */ @Override public Utilisateur trouver( String email ) throws DAOException { ... } ... } |
Exemple d'annotation Java
Il s'agit là d'une annotation Java qui sert à décrire une méthode, en l'occurrence la méthode trouver()
. Elle est constituée du caractère @
suivi du mot-clé Override, et elle est placée juste avant le début de la méthode.
Pour la petite histoire, sachez que cette annotation est doublement utile :
- elle permet de préciser au compilateur qu'une méthode redéfinit une méthode d'une interface. En cas d'erreur - c'est-à-dire si le nom de la méthode ne correspond à aucune méthode d'aucune interface - alors le compilateur doit prévenir le développeur et éventuellement faire échouer la compilation.
- elle rend le code plus lisible, en différenciant les méthodes redéfinies des autres. Ceci est renforcé par les comportements intuitifs introduits dans la plupart des IDE, qui marquent visuellement de telles méthodes.
Je vous montre ici cette annotation en particulier pour illustrer la syntaxe utilisée, mais ne vous y trompez pas, il en existe bien d'autres. Il est d'ailleurs possible de créer ses propres annotations si besoin, mais ceci a uniquement trait au Java et sort par conséquent du cadre de ce cours. Nous, ce qui nous intéresse, c'est le Java EE !
Syntaxe avec paramètres
Il existe une seconde forme d'annotation, qui attend en argument des paramètres. Elle se construit de la manière suivante :
1 | @WebServlet( name="TestServlet", urlPatterns = {"/test", "/ok"} ) |
Exemple d'annotation avec paramètres
Explications :
- une annotation peut attendre un ou plusieurs paramètres, séparés par une virgule et placés entre parenthèses juste après l'annotation. Dans cet exemple, les paramètres sont nommés name et urlPatterns ;
- un paramètre peut attendre une ou plusieurs valeurs :
- si une seule valeur est attendue, celle-ci est placée entre guillemets et liée au paramètre par le symbole =. Ici, le paramètre name attend une unique valeur définie à
"TestServlet"
; - si plusieurs valeurs sont attendues, celles-ci sont placées entre guillemets, séparées par une virgule et l'ensemble ainsi formé est placé entre accolades, puis lié au paramètre par le symbole = (en somme, la syntaxe d'initialisation d'un tableau). Ici, le paramètre urlPatterns reçoit deux valeurs définies respectivement à
"/test"
et"/ok"
.
- si une seule valeur est attendue, celle-ci est placée entre guillemets et liée au paramètre par le symbole =. Ici, le paramètre name attend une unique valeur définie à
Voilà tout ce qu'il est nécessaire de retenir concernant la syntaxe des annotations.
Avec l'API Servlet 3.0
Nous sommes enfin prêts pour aborder quelques annotations spécifiques à Java EE 6, introduites par l'API Servlet en version 3.0. Celles-ci font toutes partie du package javax.servlet.annotation
.
WebServlet
La première à laquelle nous allons nous intéresser est celle qui permet de déclarer une servlet : @WebServlet
. Comme vous pouvez le voir dans sa documentation, elle peut accepter tous les paramètres qu'il est possible de définir dans une section <servlet>
ou <servlet-mapping>
du fichier web.xml.
Prenons pour exemple notre servlet Inscription. Pour rappel, sa déclaration dans le fichier web.xml était la suivante :
1 2 3 4 5 6 7 8 | <servlet> <servlet-name>Inscription</servlet-name> <servlet-class>com.sdzee.servlets.Inscription</servlet-class> </servlet> <servlet-mapping> <servlet-name>Inscription</servlet-name> <url-pattern>/inscription</url-pattern> </servlet-mapping> |
/WEB-INF/web.xml
L'annotation qui remplace littéralement cette description est :
1 2 3 4 5 | ... @WebServlet( name="Inscription", urlPatterns = "/inscription" ) public class Inscription extends HttpServlet { ... |
com.sdzee.servlets.Inscription
Remarquez bien les points suivants :
- il faut placer l'annotation juste avant la déclaration de la classe dans le code de votre servlet ;
- il n'est pas nécessaire de préciser le contenu de
<servlet-class>
dans l'annotation : puisque celle-ci se trouve directement dans le code de la servlet, le compilateur sait déjà à quelle classe elle s'applique.
Ajoutez l'annotation à votre servlet, supprimez la déclaration de votre web.xml et redémarrez Tomcat. Vous pourrez alors vérifier par vous-mêmes que votre application fonctionne exactement comme avant, en vous rendant sur la page http://localhost:8080/pro/inscription
.
En outre, certains d'entre vous auront peut-être également remarqué que la propriété name de la servlet n'est plus utile. Dans le fichier web.xml, le champ <servlet-name>
servait à établir un lien entre les sections <servlet>
et <servlet-mapping>
, mais maintenant que nous précisons directement l'url-pattern dans l'annotation de la servlet, son nom ne nous sert plus à rien. Par conséquent, il est possible d'écrire l'annotation sous sa forme la plus simple :
1 2 3 4 5 | ... @WebServlet( "/inscription" ) public class Inscription extends HttpServlet { ... |
com.sdzee.servlets.Inscription
Ainsi, seul l'url-pattern sur lequel est mappée notre servlet est nécessaire à son bon fonctionnement. Là encore, vous pouvez confirmer ce comportement en modifiant l'annotation dans votre servlet Inscription et en vérifiant que l'application fonctionne toujours.
Par ailleurs, puisque les méta-données sont maintenant présentes directement dans le code de votre servlet, il n'est plus nécessaire de redémarrer Tomcat à chaque modification du nom de la servlet ou encore de son url-pattern : les modifications sont prises en compte presque instantanément !
WebFilter
Très similaire à la précédente, il existe une annotation qui permet de déclarer un filtre : @WebFilter
. Comme pour sa cousine dédiée à la servlet, vous constaterez dans sa documentation qu'elle accepte toutes les propriétés définissables depuis une section <filter>
ou <filter-mapping>
du fichier web.xml.
Prenons pour exemple le filtre RestrictionFilter que nous avions mis en place pour tester la restriction d'accès sur un groupe de pages. Pour rappel, sa déclaration dans notre fichier web.xml était la suivante :
1 2 3 4 5 6 7 8 | <filter> <filter-name>RestrictionFilter</filter-name> <filter-class>com.sdzee.filters.RestrictionFilter</filter-class> </filter> <filter-mapping> <filter-name>RestrictionFilter</filter-name> <url-pattern>/restreint/*</url-pattern> </filter-mapping> |
/WEB-INF/web.xml
De la même manière que pour notre servlet Inscription, l'annotation qui remplace littéralement cette description peut s'écrire :
1 2 3 4 5 | ... @WebFilter( urlPatterns = "/restreint/*" ) public class RestrictionFilter implements Filter { ... |
Dans notre application, nous avions désactivé le filtre pour ne pas être embêtés dans les autres exemples du cours. Si vous le souhaitez, vous pouvez remettre en place le filtre de restriction dans votre projet en ajoutant simplement cette annotation à la classe RestrictionFilter, et ainsi vérifier que le seul ajout de cette annotation implique bel et bien une activation du filtre, et ce même sans redémarrage de Tomcat ! Pour le désactiver, il vous suffira ensuite de supprimer ou de commenter l'annotation.
WebInitParam
Il existe également une annotation qui ne peut être utilisée qu'au sein d'une annotation @WebServlet
ou @WebFilter
: @WebInitParam
. Comme son nom l'indique, elle est destinée à remplacer la section <init-param>
qu'il est possible d'inclure aux déclarations d'une servlet ou d'un filtre dans le fichier web.xml. Sa documentation nous confirme que seuls deux attributs sont requis : un attribut name et un attribut value, remplaçant respectivement <param-name>
et <param-value>
.
Prenons pour exemple notre servlet Download. Pour rappel, sa déclaration dans le fichier web.xml était la suivante :
1 2 3 4 5 6 7 8 9 10 11 12 | <servlet> <servlet-name>Download</servlet-name> <servlet-class>com.sdzee.servlets.Download</servlet-class> <init-param> <param-name>chemin</param-name> <param-value>/fichiers/</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>Download</servlet-name> <url-pattern>/fichiers/*</url-pattern> </servlet-mapping> |
/WEB-INF/web.xml
L'annotation qui remplace cette description devient :
1 2 3 4 5 | ... @WebServlet( urlPatterns = "/fichiers/*", initParams = @WebInitParam( name = "chemin", value = "/fichiers/" ) ) public class Download extends HttpServlet { ... |
com.sdzee.servlets.Download
Remarquez bien l'utilisation de l'annotation fille @WebInitParam
au sein de l'annotation mère @WebServlet
.
Là encore, si vous souhaitez vérifier le bon fonctionnement de l'annotation, il vous suffit de l'ajouter à votre servlet Download, de supprimer la déclaration de votre web.xml, puis de redémarrer Tomcat et de tenter de télécharger un des fichiers présents sur votre disque en vous rendant sur la page http://localhost:8080/pro/fichiers/...
(où les … correspondent au nom ou chemin du fichier) !
WebListener
Reposons-nous un instant avec l'annotation dédiée aux Listener : @WebListener
. Comme vous pouvez le constater en parcourant sa documentation, elle ne requiert aucun paramètre.
D'ailleurs, vous devez vous souvenir de la courte déclaration de notre Listener InitialisationDaoFactory :
1 2 3 | <listener> <listener-class>com.sdzee.config.InitialisationDaoFactory</listener-class> </listener> |
/WEB-INF/web.xml
L'annotation qui remplace cette description est tout bonnement :
1 2 3 4 5 | ... @WebListener public class InitialisationDaoFactory implements ServletContextListener { ... |
com.sdzee.config.InitialisationDaoFactory
Comme annoncé, elle n'attend aucun paramètre et suffit à déclarer une classe en tant que Listener au sein d'une application web. Pratique, n'est-ce pas ?
MultipartConfig
Pour terminer, nous allons nous intéresser à l'annotation dédiée au traitement des requêtes de type Multipart : @MultipartConfig
. Sa documentation nous confirme que, tout comme nous l'avions fait dans la section <multipart-config>
de notre web.xml, nous pouvons préciser quatre paramètres : location, fileSizeThreshold, maxFileSize et maxRequestSize. Je ne reviens pas sur leur signification, nous les avons déjà découverts dans le chapitre expliquant l'envoi de fichier.
Pour rappel, voici comment nous avions déclaré notre servlet d'upload :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <servlet> <servlet-name>Upload</servlet-name> <servlet-class>com.sdzee.servlets.Upload</servlet-class> <init-param> <param-name>chemin</param-name> <param-value>/fichiers/</param-value> </init-param> <multipart-config> <location>c:/fichiers</location> <max-file-size>10485760</max-file-size> <!-- 10 Mo --> <max-request-size>52428800</max-request-size> <!-- 5 x 10Mo --> <file-size-threshold>1048576</file-size-threshold> <!-- 1 Mo --> </multipart-config> </servlet> <servlet-mapping> <servlet-name>Upload</servlet-name> <url-pattern>/upload</url-pattern> </servlet-mapping> |
/WEB-INF/web.xml
C'est un cas d'étude intéressant, puisque la description contient à la fois un paramètre d'initialisation et une section Multipart. L'annotation correspondante est :
1 2 3 4 5 6 | ... @WebServlet( urlPatterns = "/upload", initParams = @WebInitParam( name = "chemin", value = "/fichiers/" ) ) @MultipartConfig( location = "c:/fichiers", maxFileSize = 10 * 1024 * 1024, maxRequestSize = 5 * 10 * 1024 * 1024, fileSizeThreshold = 1024 * 1024 ) public class Upload extends HttpServlet { ... |
com.sdzee.servlets.Upload
Eh oui, nous avons besoin de deux annotations différentes appliquées sur la même classe pour remplacer intégralement la précédente déclaration de la servlet ! En effet, alors que @WebInitParam
est incluse dans le corps de l'annotation @WebServlet
, l'annotation @MultipartConfig
est, quant à elle, indépendante et doit donc être spécifiée à part.
Côté syntaxe, vous pouvez relever deux points intéressants :
- lorsqu'un paramètre attendu est de type numérique, il ne faut pas l'entourer de guillemets comme nous le faisions jusqu'alors pour tous nos paramètres de type
String
; - lorsqu'un paramètre attendu est de type numérique, il est possible d'utiliser des opérateurs mathématiques simples dans sa valeur. Ici, j'ai utilisé l'opérateur de multiplication * afin de rendre visible au premier coup d’œil le fait que notre servlet limite les tailles respectivement à 10 Mo, 5x10 Mo et 1 Mo.
Pour tester, supprimez la déclaration du fichier web.xml et ajoutez ces deux annotations à votre servlet Upload. Redémarrez alors Tomcat, puis rendez-vous sur http://localhost:8080/pro/upload
et tentez d'envoyer un fichier trop volumineux, un fichier sans description, et enfin un fichier correspondant aux critères de validation.
Et le web.xml dans tout ça ?
Avec ces quelques annotations simples, vous allez pouvoir mettre au régime votre fichier web.xml de manière drastique !
Toutefois, si vous faites vous-mêmes l'expérience et tentez de remplacer chacune des déclarations par leurs annotations équivalentes, tôt ou tard vous vous rendrez compte que tout ne va pas disparaître. En réalité, le fichier web.xml est encore utile pour plusieurs raisons, que nous allons rapidement découvrir.
1. Déclarer la version de l'API servlet
La première utilisation, évidente pour vous je l'espère, est la déclaration de la version de l'API Servlet requise par votre application. Autrement dit, vous avez toujours besoin de la balise <web-app>
:
1 2 3 4 5 6 7 8 | <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> ... |
/WEB-INF/web.xml
J'en profite pour vous rappeler que les annotations ne sont disponibles qu'à partir de Java EE 6, et qu'il vous faut donc impérativement déclarer l'API Servlet 3.0 pour pouvoir les utiliser dans votre code.
2. Spécifier un ordre de filtrage
Souvenez-vous du chapitre sur les filtres, je vous y avais annoncé que l'ordre de déclaration des filtres dans le fichier web.xml était important, car il déterminait l'ordre dans lequel vos filtres allaient être appliqués aux requêtes entrantes.
Eh bien sachez que ce niveau de configuration n'est pas atteignable avec de simples annotations. En effet, en utilisant des annotations il est impossible de préciser dans quel ordre vous souhaitez appliquer différents filtres mappés sur une même requête. Ainsi, si vous souhaitez organiser vos filtres et définir un ordre d'exécution précis, vous devrez continuer à les déclarer dans votre fichier web.xml.
Toutefois, si vous souhaitez limiter l'encombrement de votre web.xml, vous pourrez toujours utiliser des annotations pour remplacer les sections <filter>
, et vous contenter d'y écrire les sections <filter-mapping>
. C'est tout ce dont le conteneur a besoin pour définir l'ordre d'exécution des filtres !
Notez cependant que ce découpage, à savoir les annotations d'un côté pour remplacer les sections <filter>
, et le fichier web.xml de l'autre pour écrire les sections <filter-mapping>
, ne fonctionne avec Tomcat que depuis la version 7.0.28 ! Auparavant, le bug 53354 empêchait le bon fonctionnement. Autrement dit, si vous utilisez une version antérieure de Tomcat et que vous souhaitez donner un ordre à vos filtres, alors vous devrez les déclarer intégralement dans votre web.xml, et ne pourrez pas utiliser les annotations @WebFilter
.
3. Exécuter un filtre externe
Toujours à propos des filtres, il existe un cas particulier dans lequel vous ne pourrez pas utiliser l'annotation @WebFilter
: lorsque le filtre que vous appliquez ne fait pas partie de votre application ! Sans accès au code source, vous n'avez pas vraiment le choix…
Par exemple, dans notre projet nous utilisons le filtre natif de Tomcat nommé Set Character Encoding, afin de compléter la gestion de l'UTF-8. Eh bien pour ce cas précis, nous n'avons pas d'autre choix que de déclarer le filtre à la main dans le web.xml de notre application.
4. Surcharger les valeurs définies dans une annotation
Pour conclure, sachez que les valeurs précisées dans toutes vos annotations sont en réalité considérées par le conteneur comme des valeurs par défaut. En d'autres termes, si vous déclarez à la fois une annotation et une section dans le fichier web.xml, alors les données de la section seront prises en compte quoi qu'il arrive.
Concrètement, cela signifie que par exemple si vous mappez en même temps une servlet sur une URL depuis une section dans le web.xml, et sur une autre URL depuis une annotation, alors la servlet sera en fin de compte mappée sur les deux URL.
Par ailleurs, dans le cas de paramètres à valeur unique comme les @WebInitParam
, en cas de double définition c'est la valeur contenue dans le web.xml qui sera prise en compte, et celle contenue dans l'annotation sera ignorée.
- Une annotation est une méta-donnée, une indication liée à l’élément qu'elle cible.
- Les fichiers de configuration externes sont pénibles à utiliser et difficilement maintenables.
- Il existe des annotations propres à Java EE, apparues avec la version 6 de la plate-forme :
@WebServlet
permet de déclarer une servlet ;@WebFilter
permet de déclarer un filtre ;@WebInitParam
permet de préciser un paramètre d'initialisation ;@WebListener
permet de déclarer un listener ;@MultipartConfig
permet d'activer la gestion des requêtes de type multipart depuis une servlet.
- Le fichier web.xml devient presque inutile !
- La lisibilité et la compréhension d'une application sont facilitées, les informations de configuration étant maintenant directement présentes au sein du code !