[java] Mit JAX-RS trocken bleiben



Answers

Wenn man Jax's JIRA betrachtet , scheint jemand nach Annotationsvererbung als Meilenstein für JAX-RS gefragt zu haben.

Die Funktion, nach der du suchst, existiert in JAX-RS noch nicht, würde das aber funktionieren? Es ist hässlich, verhindert aber eine wiederkehrende Injektion.

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 */ }
}

Oder in einer anderen Problemumgehung:

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 */ }
}

Aber wenn ich sehe, wie empfindlich du bist, ehrlich gesagt, bezweifle ich, dass deine Frustration mit diesem hässlichen Code verschwinden wird :)

Question

Ich versuche, wiederholten Code für eine Anzahl von JAX-RS-Ressourcenhandlern zu minimieren, die alle ein paar der gleichen Pfad- und Abfrageparameter erfordern. Die grundlegende URL-Vorlage für jede Ressource sieht folgendermaßen aus:

/{id}/resourceName

und jede Ressource hat mehrere Unterressourcen:

/{id}/resourceName/subresourceName

Ressourcen- / Unterressourcenpfade (inkl. Abfrageparameter) könnten also aussehen

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

Die gemeinsamen Teile über Ressourcen foo und quux sind @PathParam("id") und @QueryParam("xyz") . Ich könnte die Ressourcenklassen folgendermaßen implementieren:

// 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 */ }
}

Ich habe es vermieden, die Parameterinjektion in jede einzelne get* -Methode zu wiederholen. 1 Das ist ein guter Anfang, aber ich möchte auch die Wiederholung über Ressourcenklassen hinweg vermeiden können. Ein Ansatz, der mit CDI funktioniert (was ich auch brauche), ist die Verwendung einer abstract Basisklasse, die FooService und QuuxService könnten:

// 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 */ }
}

Innerhalb der get* -Methoden funktioniert die CDI-Injektion (wie durch ein Wunder) korrekt: Das util Feld ist nicht null. Leider funktioniert die JAX-RS-Injektion nicht ; id und xyz sind null in den Methoden get* von FooService und QuuxService .

Gibt es eine Behebung oder Abhilfe für dieses Problem?

Angesichts der Tatsache, dass die CDI so funktioniert, wie ich es möchte, frage ich mich, ob der Fehler, @PathParam s (etc.) in Unterklassen zu injizieren, ein Fehler oder nur ein Teil der JAX-RS-Spezifikation ist.

Ein anderer Ansatz, den ich bereits ausprobiert habe, ist die Verwendung von BaseService als einen einzigen Einstiegspunkt, der nach FooService an FooService und QuuxService delegiert wird. Dies wird im Wesentlichen in RESTful Java mit JAX-RS unter Verwendung von Subresource Locators beschrieben.

// 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 */ }
}

Der Nachteil dieses Ansatzes besteht darin, dass weder CDI-Injektion noch JAX-RS-Injektion in den Subresource-Klassen funktionieren. Der Grund dafür ist ziemlich offensichtlich 2 , aber das bedeutet, dass ich die Felder manuell in den Konstruktor der Unterklassen einfügen muss, was unordentlich und hässlich ist und mich nicht einfach weitere Injektionen anpassen lässt. Beispiel: Ich möchte @Inject eine Instanz in FooService aber nicht in FooService QuuxService . Da ich die Unterklassen von BaseService explizit instanziiere, BaseService CDI-Injektion nicht, sodass die Hässlichkeit fortgesetzt wird.

tl; dr Was ist der richtige Weg, um das wiederholte Einfügen von Feldern in JAX-RS-Ressourcenhandlerklassen zu vermeiden?

Und warum werden nicht vererbte Felder von JAX-RS injiziert, während CDI keine Probleme damit hat?

Bearbeiten 1

Mit ein bisschen Anleitung von @Tarlog glaube ich, dass ich die Antwort auf eine meiner Fragen gefunden habe,

Warum werden von JAX-RS nicht vererbte Felder injiziert?

In JSR-311 §3.6 :

Wenn eine Unterklasse oder eine Implementierungsmethode über JAX-RS-Annotationen verfügt, werden alle Annotationen in der Superklassen- oder Interface-Methode ignoriert.

Ich bin mir sicher, dass es einen wirklichen Grund für diese Entscheidung gibt, aber unglücklicherweise arbeitet diese Tatsache in diesem speziellen Anwendungsfall gegen mich. Ich bin immer noch an möglichen Problemumgehungen interessiert.

1 Der Nachteil bei der Verwendung von Field-Level-Injection ist, dass ich jetzt an die Instanziierung pro Anfrage-Ressourcenklasse gebunden bin, aber damit kann ich leben.
2 Weil ich derjenige bin, der den new FooService() und nicht den Container / die JAX-RS-Implementierung new FooService() .




Anstatt @PathParam , @QueryParam oder einen anderen Parameter zu verwenden, können Sie mit @Context UriInfo auf alle Arten von Parametern zugreifen. Also könnte dein Code sein:

// 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 */ }
}

getIdParameter Sie, dass getIdParameter statisch ist, sodass Sie es in eine Dienstprogrammklasse einfügen und mehrere Klassen wiederverwenden können.
UriInfo ist garantiert threadsicher, sodass Sie die Ressourcenklasse als Singleton beibehalten können.




Was ist die Motivation, Parameterinjektionen zu vermeiden?
Wenn die Motivation darin besteht, hartcodierte Strings nicht zu wiederholen, können Sie sie einfach umbenennen, indem Sie "Konstanten" wiederverwenden:

// 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 */ }
}

(Es tut mir leid, dass ich die zweite Antwort gepostet habe, aber es war zu lang, um sie in einen Kommentar der vorherigen Antwort zu schreiben)




Sie können einen benutzerdefinierten Provider hinzufügen, insbesondere über AbstractHttpContextInjective:

// 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;
    }
}

Zugegeben, Sie müssen die Pfadparameter und / oder die Abfrageparameter auf die harte Tour aus HttpContext extrahieren, aber Sie werden es an einer Stelle tun.




Links