Rester au sec avec JAX-RS

J’essaie de minimiser le code répété pour un certain nombre de gestionnaires de ressources JAX-RS, qui requièrent tous quelques mêmes parameters de chemin d’access et de requête. Le modèle d’URL de base pour chaque ressource ressemble à ceci:

/{id}/resourceName 

et chaque ressource a plusieurs sous-ressources:

 /{id}/resourceName/subresourceName 

Ainsi, les chemins de ressources / sous-ressources (y compris les parameters de requête) peuvent ressembler à

 /12345/foo/bar?xyz=0 /12345/foo/baz?xyz=0 /12345/quux/abc?xyz=0 /12345/quux/def?xyz=0 

Les parties communes aux ressources foo et quux sont @PathParam("id") et @QueryParam("xyz") . Je pourrais implémenter les classes de ressources comme ceci:

 // FooService.java @Path("/{id}/foo") public class FooService { @PathParam("id") Ssortingng id; @QueryParam("xyz") Ssortingng xyz; @GET @Path("bar") public Response getBar() { /* snip */ } @GET @Path("baz") public Response getBaz() { /* snip */ } } 

 // QuuxService.java @Path("/{id}/quux") public class QuxxService { @PathParam("id") Ssortingng id; @QueryParam("xyz") Ssortingng xyz; @GET @Path("abc") public Response getAbc() { /* snip */ } @GET @Path("def") public Response getDef() { /* snip */ } } 

J’ai réussi à éviter de répéter l’injection du paramètre dans chaque méthode get* . 1 C’est un bon début, mais j’aimerais pouvoir éviter la répétition dans toutes les classes de ressources. Une approche qui fonctionne avec CDI (dont j’ai aussi besoin) consiste à utiliser une classe de base abstract que FooService et QuuxService pourraient extend :

 // BaseService.java public abstract class BaseService { // JAX-RS injected fields @PathParam("id") protected Ssortingng id; @QueryParam("xyz") protected Ssortingng xyz; // CDI injected fields @Inject protected SomeUtility util; } 

 // FooService.java @Path("/{id}/foo") public class FooService extends BaseService { @GET @Path("bar") public Response getBar() { /* snip */ } @GET @Path("baz") public Response getBaz() { /* snip */ } } 

 // QuuxService.java @Path("/{id}/quux") public class QuxxService extends BaseService { @GET @Path("abc") public Response getAbc() { /* snip */ } @GET @Path("def") public Response getDef() { /* snip */ } } 

A l’intérieur des méthodes get* , l’injection CDI (miraculeusement) fonctionne correctement: le champ util n’est pas nul. Malheureusement, l’injection JAX-RS ne fonctionne pas ; id et xyz sont null dans les méthodes get* de FooService et QuuxService .

Existe-t-il un correctif ou une solution de contournement pour ce problème?

Étant donné que le CDI fonctionne comme je le souhaite, je me demande si l’incapacité à injecter @PathParam s (etc.) dans des sous-classes est un bogue ou une partie de la spécification JAX-RS.


Une autre approche que j’ai déjà essayée est d’utiliser BaseService comme point d’entrée unique qui délègue à FooService et QuuxService si nécessaire. Ceci est fondamentalement comme décrit dans RESTful Java avec JAX-RS utilisant des localisateurs de sous-ressources.

 // BaseService.java @Path("{id}") public class BaseService { @PathParam("id") protected Ssortingng id; @QueryParam("xyz") protected Ssortingng xyz; @Inject protected SomeUtility util; public BaseService () {} // default ctor for JAX-RS // ctor for manual "injection" public BaseService(Ssortingng id, Ssortingng xyz, SomeUtility util) { this.id = id; this.xyz = xyz; this.util = util; } @Path("foo") public FooService foo() { return new FooService(id, xyz, util); // manual DI is ugly } @Path("quux") public QuuxService quux() { return new QuuxService(id, xyz, util); // yep, still ugly } } 

 // FooService.java public class FooService extends BaseService { public FooService(Ssortingng id, Ssortingng xyz, SomeUtility util) { super(id, xyz, util); // the manual DI ugliness continues } @GET @Path("bar") public Response getBar() { /* snip */ } @GET @Path("baz") public Response getBaz() { /* snip */ } } 

 // QuuxService.java public class QuuzService extends BaseService { public FooService(Ssortingng id, Ssortingng xyz, SomeUtility util) { super(id, xyz, util); // the manual DI ugliness continues } @GET @Path("abc") public Response getAbc() { /* snip */ } @GET @Path("def") public Response getDef() { /* snip */ } } 

L’inconvénient de cette approche est que ni l’injection CDI ni l’injection JAX-RS ne fonctionnent dans les classes de sous-ressources. La raison en est assez évidente 2 , mais cela signifie que je dois réinjecter manuellement les champs dans le constructeur de la sous-classe, ce qui est désordonné, moche, et ne me laisse pas facilement personnaliser l’injection. Exemple: disons que je voulais @Inject une instance dans FooService mais pas QuuxService . Comme BaseService explicitement les sous-classes de BaseService , l’injection CDI ne fonctionnera pas, donc la laideur se poursuit.


tl; dr Quelle est la bonne façon d’éviter d’injecter de manière répétée des champs dans les classes de gestionnaires de ressources JAX-RS?

Et pourquoi les champs hérités n’ont-ils pas été injectés par JAX-RS, alors que CDI n’a aucun problème avec cela?


Modifier 1

Avec un peu de direction de @Tarlog , je pense avoir trouvé la réponse à l’une de mes questions,

Pourquoi les champs hérités ne sont-ils pas injectés par JAX-RS?

Dans JSR-311 §3.6 :

Si une sous-classe ou une méthode d’implémentation comporte des annotations JAX-RS, toutes les annotations de la super-classe ou de la méthode d’interface sont ignorées.

Je suis sûr qu’il y a une vraie raison à cette décision, mais malheureusement, ce fait travaille contre moi dans ce cas d’utilisation particulier. Je suis toujours intéressé par les solutions possibles.


1 La mise en garde concernant l’utilisation de l’injection au niveau du champ est que je suis maintenant lié à l’instanciation des classes de ressources par requête, mais je peux vivre avec cela.
2 Parce que c’est moi qui appelle le new FooService() plutôt que le conteneur / l’implémentation JAX-RS.

Voici une solution de rechange que j’utilise:

Définissez un constructeur pour le BaseService avec “id” et “xyz” comme parameters:

 // BaseService.java public abstract class BaseService { // JAX-RS injected fields protected final Ssortingng id; protected final Ssortingng xyz; public BaseService (Ssortingng id, Ssortingng xyz) { this.id = id; this.xyz = xyz; } } 

Répétez le constructeur sur toutes les sous-classes avec les injections:

 // FooService.java @Path("/{id}/foo") public class FooService extends BaseService { public FooService (@PathParam("id") Ssortingng id, @QueryParam("xyz") Ssortingng xyz) { super(id, xyz); } @GET @Path("bar") public Response getBar() { /* snip */ } @GET @Path("baz") public Response getBaz() { /* snip */ } } 

En regardant le JIRA de Jax, il semble que quelqu’un ait demandé un inheritance d’annotation comme jalon pour JAX-RS.

La fonctionnalité que vous recherchez n’existe pas encore dans JAX-RS. Cependant, cela fonctionnerait-il? C’est moche, mais empêche l’injection récurrente.

 public abstract class BaseService { // JAX-RS injected fields @PathParam("id") protected Ssortingng id; @QueryParam("xyz") protected Ssortingng xyz; // CDI injected fields @Inject protected SomeUtility util; @GET @Path("bar") public abstract Response getBar(); @GET @Path("baz") public abstract Response getBaz(); @GET @Path("abc") public abstract Response getAbc(); @GET @Path("def") public abstract Response getDef(); } 

 // FooService.java @Path("/{id}/foo") public class FooService extends BaseService { public Response getBar() { /* snip */ } public Response getBaz() { /* snip */ } } 

 // QuuxService.java @Path("/{id}/quux") public class QuxxService extends BaseService { public Response getAbc() { /* snip */ } public Response getDef() { /* snip */ } } 

Ou dans une autre solution de contournement:

 public abstract class BaseService { @PathParam("id") protected Ssortingng id; @QueryParam("xyz") protected Ssortingng xyz; // CDI injected fields @Inject protected SomeUtility util; @GET @Path("{stg}") public abstract Response getStg(@Pathparam("{stg}") Ssortingng stg); } 

 // FooService.java @Path("/{id}/foo") public class FooService extends BaseService { public Response getStg(Ssortingng stg) { if(stg.equals("bar")) { return getBar(); } else { return getBaz(); } } public Response getBar() { /* snip */ } public Response getBaz() { /* snip */ } } 

Mais en voyant combien tu es sensible, franchement, je doute que ta frustration disparaisse avec ce code moche 🙂

Dans RESTEasy, on peut construire une classe, annoter avec @ * Param comme d’habitude et terminer en annotant la classe @Form. Cette classe @Form peut alors être un paramètre d’injection dans tout appel de méthode d’un autre service. http://docs.jboss.org/restasy/docs/2.3.5.Final/userguide/html/_Form.html

Vous pouvez append un fournisseur personnalisé, en particulier via AbstractHttpContextInjectable:

 // FooService.java @Path("/{id}/foo") public class FooService { @Context CommonStuff common; @GET @Path("bar") public Response getBar() { /* snip */ } @GET @Path("baz") public Response getBaz() { /* snip */ } } @Provider public class CommonStuffProvider extends AbstractHttpContextInjectable implements InjectableProvider { ... @Override public CommonStuff getValue(HttpContext context) { CommonStuff c = new CommonStuff(); c.id = ...initialize from context; c.xyz = ...initialize from context; return c; } } 

Certes, vous devrez extraire les parameters de chemin d’access et / ou les parameters de requête de la manière la plus difficile depuis HttpContext, mais vous le ferez une fois au même endroit.

J’ai toujours eu l’impression que l’inheritance des annotations rendait mon code illisible, car il n’était pas évident d’où / comment il était injecté (par exemple, à quel niveau de l’arbre d’inheritance serait-il injecté et où était-il surchargé)? tout)). De plus, vous devez rendre la variable protégée (et probablement PAS finale), ce qui fait que la super-classe perd son état interne et peut aussi introduire des bogues (au moins, je me demanderais toujours quand j’appelle une méthode étendue: la variable protégée ?). À mon humble avis, il n’a rien avec DRY, car ce n’est pas une encapsulation de la logique, mais une encapsulation de l’injection, ce qui me semble exagéré.

À la fin, je citerai la spécification JAX-RS, 3.6 Annotation Inheritance

Pour assurer la cohérence avec les autres spécifications Java EE, il est recommandé de toujours répéter les annotations au lieu de s’appuyer sur l’inheritance des annotations.

PS: J’avoue que je n’utilise parfois que l’inheritance d’annotation, mais au niveau de la méthode 🙂

Quelle est la motivation d’éviter les injections de parameters?
Si la motivation est d’éviter de répéter les chaînes codées en dur, vous pouvez facilement les renommer, vous pouvez réutiliser les “constantes”:

 // FooService.java @Path("/" + FooService.ID +"/foo") public class FooService { public static final Ssortingng ID = "id"; public static final Ssortingng XYZ= "xyz"; public static final Ssortingng BAR= "bar"; @PathParam(ID) Ssortingng id; @QueryParam(XYZ) Ssortingng xyz; @GET @Path(BAR) public Response getBar() { /* snip */ } @GET @Path(BAR) public Response getBaz() { /* snip */ } } // QuuxService.java @Path("/" + FooService.ID +"/quux") public class QuxxService { @PathParam(FooService.ID) Ssortingng id; @QueryParam(FooService.XYZ) Ssortingng xyz; @GET @Path("abc") public Response getAbc() { /* snip */ } @GET @Path("def") public Response getDef() { /* snip */ } } 

(Désolé d’avoir posté la deuxième réponse, mais il était trop long de la mettre dans un commentaire de la réponse précédente)

Vous pouvez essayer @BeanParam pour tous les parameters répétés. Ainsi, plutôt que de les injecter à chaque fois, vous pouvez simplement vous injecter un customBean qui fera l’affaire.

Une autre approche plus propre est que vous pouvez injecter

 @Context UriInfo 

ou

 @Context ExtendedUriInfo 

à votre classe de ressources et dans la méthode même, vous pouvez simplement y accéder. UriInfo est plus flexible car votre fichier jvm aura un fichier source Java moins important à gérer et surtout, une seule instance de UriInfo ou ExtendedUriInfo vous donnera access à beaucoup de choses.

 @Path("test") public class DummyClass{ @Context UriInfo info; @GET @Path("/{id}") public Response getSomeResponse(){ //custom code //use info to fetch any query, header, masortingx, path params //return response object } 

Au lieu d’utiliser @PathParam , @QueryParam ou tout autre paramètre, vous pouvez utiliser @Context UriInfo pour accéder à tous les types de parameters. Donc votre code pourrait être:

 // FooService.java @Path("/{id}/foo") public class FooService { @Context UriInfo uriInfo; public static Ssortingng getIdParameter(UriInfo uriInfo) { return uriInfo.getPathParameters().getFirst("id"); } @GET @Path("bar") public Response getBar() { /* snip */ } @GET @Path("baz") public Response getBaz() { /* snip */ } } // QuuxService.java @Path("/{id}/quux") public class QuxxService { @Context UriInfo uriInfo; @GET @Path("abc") public Response getAbc() { /* snip */ } @GET @Path("def") public Response getDef() { /* snip */ } } 

Faites attention à ce que getIdParameter soit statique, vous pouvez donc le mettre dans une classe utilitaire et réutiliser les accors de plusieurs classes.
UriInfo est garanti être threadsafe, vous pouvez donc garder la classe de ressources en singleton.