web-dev-qa-db-fra.com

Rester DRY avec Jax-Rs

J'essaie de minimiser le code répété pour un certain nombre de gestionnaires de ressources JAX-RS, qui nécessitent tous quelques-uns des mêmes paramètres de chemin et de requête. Le modèle d'URL de base pour chaque ressource ressemble à ceci:

/{id}/resourceName

et chaque ressource a plusieurs sous-sortes:

/{id}/resourceName/subresourceName

Donc, les chemins de ressource/de sous-sorce (incl. Paramètres de requête) pourraient ressembler à

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

Les parties communes entre les ressources foo et quux sont @PathParam("id") et @QueryParam("xyz"). I pourrait mettre en œuvre les classes de ressources comme celle-ci:

// FooService.Java
@Path("/{id}/foo")
public class FooService
{
    @PathParam("id") String id;
    @QueryParam("xyz") String 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") String id;
    @QueryParam("xyz") String 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 de paramètres dans chaque méthode get*.1 C'est un bon début, mais j'aimerais pouvoir éviter la répétition entre les cours de ressources. Une approche qui fonctionne avec CDI (que j'ai aussi besoin) est d'utiliser une classe de base abstract _ FooService et QuuxService pourrait extend:

// BaseService.Java
public abstract class BaseService
{
    // JAX-RS injected fields
    @PathParam("id") protected String id;
    @QueryParam("xyz") protected String 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 */ }
}

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

Y a-t-il une solution ou une solution de contournement pour ce problème?

Étant donné que le CDI fonctionne comme je l'aimerais, je me demande si le défaut d'injecter @PathParam S (etc.) dans les sous-classes est un bogue ou une partie juste partie de la spécification JAX-RS.


Une autre approche que j'ai déjà essayée consiste à utiliser BaseService comme un point d'entrée unique qui délégué à FooService et QuuxService au besoin. Ceci est fondamentalement comme décrit dans reposant Java avec Jax-RS à l'aide de localisateurs de sous-sorce.

// BaseService.Java
@Path("{id}")
public class BaseService
{
    @PathParam("id") protected String id;
    @QueryParam("xyz") protected String xyz;
    @Inject protected SomeUtility util;

    public BaseService () {} // default ctor for JAX-RS

    // ctor for manual "injection"
    public BaseService(String id, String 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(String id, String 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(String id, String 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 de CDI ni l'injection JAX-RS ne fonctionne dans les classes de sous-séduction. La raison de cela est assez évidente2, mais ce que cela signifie est que je dois réinjecter manuellement les champs dans le constructeur des sous-classes, qui est en désordre, laid et ne le fait pas laissez-moi facilement personnaliser une injection supplémentaire. Exemple: disons que je voulais @Inject Une instance dans FooService mais pas QuuxService. Parce que je suis explicitement instanciant des sous-classes de BaseService, l'injection CDI ne fonctionnera pas, de sorte que la laideur est poursuivie.


tL; DR Quelle est la bonne façon d'éviter à plusieurs reprises des champs d'injection dans des cours de gestionnaire de ressources JAX-RS?

Et pourquoi les champs hérités ne sont-ils pas injectés par JAX-RS, tandis que CDI n'a aucun problème avec cela?


Éditer 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 méthode de sous-classe ou de mise en œuvre contient des annotations JAX-RS , tout des annotations de la méthode Super Class ou Interface est ignorée.

Je suis sûr qu'il y a une vraie raison de cette décision, mais malheureusement, ce fait travaille contre moi dans ce cas d'utilisation particulière. Je suis toujours intéressé par des solutions de contournement possibles.


1 La mise en garde avec une injection de niveau de terrain est que je suis maintenant liée à une instanciation de classe de ressources par demande, mais je peux vivre avec cela.
2 Parce que je suis celui appelant new FooService() plutôt que le conteneur/la mise en œuvre JAX-RS.

62
Matt Ball

Voici une solution de contournement que j'utilise:

Définissez un constructeur pour la bassevice avec "ID" et "XYZ" comme param.

// BaseService.Java
public abstract class BaseService
{
    // JAX-RS injected fields
    protected final String id;
    protected final String xyz;

    public BaseService (String id, String xyz) {
        this.id = id;
        this.xyz = xyz;
    }
}

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

// FooService.Java
@Path("/{id}/foo")
public class FooService extends BaseService
{
    public FooService (@PathParam("id") String id, @QueryParam("xyz") String xyz) {
        super(id, xyz);
    }

    @GET @Path("bar")
    public Response getBar() { /* snip */ }

    @GET @Path("baz")
    public Response getBaz() { /* snip */ }
}
6
Lucky

En regardant Jira de Jax's Il semble que quelqu'un a demandé à une héritage d'annotation en tant que 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 String id;
    @QueryParam("xyz") protected String 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 String id;
    @QueryParam("xyz") protected String xyz;

    // CDI injected fields
    @Inject protected SomeUtility util;

    @GET @Path("{stg}")
    public abstract Response getStg(@Pathparam("{stg}") String stg);

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

    public Response getBaz() { /* snip */ }
}

Mais voyez-vous à quel point vous êtes délicieux, franchement, je doute que votre frustration disparaisse avec ce code laid :)

4
Gepsens

En restant facile, on peut construire une classe, annotant avec @ * param comme d'habitude et finir en annotant la classe @form. Cette classe @form peut alors être une injection de paramètre dans tout appel de la méthode de l'autre service. http://docs.jboss.org/resserasy/docs/2.3.5.final/userguide/html/_form.html

3
rektide

J'ai toujours eu un sentiment, que l'héritage d'annotation rend mon code illisible, car il n'est pas évident d'où/comment il est injecté (par exemple, auquel le niveau de l'arbre d'héritage serait injecté et où était-il envahi (ou était-il envahi à tous)). De plus, vous devez faire la variable protégée (et probablement non finale), ce qui rend la supercalclaplasse fuir son état interne et peut également introduire certains bugs (au moins je me demanderais toujours lorsque vous appelez une méthode étendue: la variable protégée est-elle modifiée là-bas. ?). IMHO, cela n'a rien avec sec, car ce n'est pas l'encapsulation de la logique, mais l'encapsulation d'injection, qui me semble exagérée.

À la fin, je citerai de la spécification JAX-RS, ,6 héritage d'annotation

Pour la cohérence avec d'autres Java Spécifications, il est recommandé de toujours répéter les annotations au lieu de s'appuyer sur l'héritage d'annotation.

PS: J'admets que je n'utilise que parfois l'héritage d'annotation, mais sur le niveau de la méthode :)

3
Andrei I

Vous pouvez ajouter un fournisseur personnalisé, en particulier via ABSTRACTHTTPCONTEXTABLE:

// 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<CommonStuff>
    implements InjectableProvider<Context, Type>
{

    ...

    @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 paramètres de chemin et/ou les paramètres de requête à la dure de httpcontext, mais vous le ferez une fois au même endroit.

2
grzes

Quelle est la motivation d'éviter les injections de paramètres?
[.____] Si la motivation évite de répéter des chaînes codées dures, vous pouvez facilement les renommer, vous pouvez réutiliser "Constantes":

// FooService.Java
@Path("/" +  FooService.ID +"/foo")
public class FooService
{
    public static final String ID = "id";
    public static final String XYZ= "xyz";
    public static final String BAR= "bar";

    @PathParam(ID) String id;
    @QueryParam(XYZ) String 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) String id;
    @QueryParam(FooService.XYZ) String 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 pour le mettre dans un commentaire de la réponse précédente)

1
Tarlog

Vous pouvez essayer @beanparam pour tous les paramètres répétitifs. Donc, plutôt que de les injecter à chaque fois que vous pouvez simplement vous injecter CustomBean, ce qui fera le tour.

Une autre approche qui est plus propre est que vous pouvez injecter

@Context UriInfo 

ou

@Context ExtendedUriInfo

à votre classe de ressources et en très méthode, vous pouvez simplement y accéder. URIInfo est plus flexible car votre JVM aura un de moins Java Fichier source pour gérer et surtout une instance unique d'Uriinfo ou ExtenduduInfo vous donne une poignée de 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, matrix, path params
     //return response object
}
1
Najeeb Arif

À la place d'utiliser @PathParam, @QueryParam ou tout autre paramètre, vous pouvez utiliser @Context UriInfo Pour accéder à tous types de paramètres. Donc, votre code pourrait être:

// FooService.Java
@Path("/{id}/foo")
public class FooService
{
    @Context UriInfo uriInfo;

    public static String 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 est statique, vous pouvez donc la mettre dans certaines catégories de services publics et la réutilisation accenche plusieurs classes.
[.____] Uriinfo est garanti d'être threadsafe, de sorte que vous puissiez garder la classe de ressources comme singleton.

0
Tarlog