java - jax - jersey servlet
Permanecer seco con JAX-RS (8)
Estoy tratando de minimizar el código repetido para un número de manejadores de recursos JAX-RS, todos los cuales requieren algunos de los mismos parámetros de ruta y consulta. La plantilla de URL básica para cada recurso tiene este aspecto:
/{id}/resourceName
y cada recurso tiene múltiples subrecursos:
/{id}/resourceName/subresourceName
Por lo tanto, las rutas de recursos / recursos secundarios (incluidos los parámetros de consulta) pueden parecerse
/12345/foo/bar?xyz=0
/12345/foo/baz?xyz=0
/12345/quux/abc?xyz=0
/12345/quux/def?xyz=0
Las partes comunes de los recursos foo
y quux
son @PathParam("id")
y @QueryParam("xyz")
. Podría implementar las clases de recursos de esta manera:
// 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 */ }
}
Me las he arreglado para evitar repetir la inyección de parámetros en cada método get*
. 1 Este es un buen comienzo, pero me gustaría poder evitar la repetición en las clases de recursos también. Un enfoque que funciona con CDI (que también necesito) es usar una clase base abstract
que FooService
y QuuxService
podrían 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 */ }
}
Dentro de los métodos get*
, la inyección CDI (milagrosamente) funciona correctamente: el campo util
no es nulo. Desafortunadamente, la inyección JAX-RS no funciona; id
y xyz
son null
en los métodos get*
de FooService
y QuuxService
.
¿Hay alguna solución o solución para este problema?
Dado que el CDI funciona como me gustaría, me pregunto si la falla al inyectar @PathParam
s (etc.) en subclases es un error o solo parte de la especificación JAX-RS.
Otro enfoque que ya he probado es usar BaseService
como un único punto de entrada que delegue a FooService
y QuuxService
según sea necesario. Esto es básicamente como se describe en RESTful Java con JAX-RS utilizando localizadores de subrecursos.
// 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 */ }
}
La desventaja de este enfoque es que ni la inyección de CDI ni la inyección de JAX-RS funcionan en las clases de recursos. La razón para esto es bastante obvia 2 , pero lo que eso significa es que tengo que volver a inyectar manualmente los campos en el constructor de las subclases, lo cual es desordenado, feo y no me permite personalizar más la inyección. Ejemplo: decir que quería @Inject
una instancia en FooService
pero no QuuxService
. Como estoy BaseService
explícitamente las subclases de BaseService
, la inyección de CDI no funcionará, por lo que la fealdad continúa.
tl; dr ¿Cuál es la forma correcta de evitar la inyección repetida de campos en las clases de manejador de recursos JAX-RS?
¿Y por qué JAX-RS no inyecta campos heredados, mientras que CDI no tiene problemas con esto?
Editar 1
Con un poco de dirección de @Tarlog , creo que encontré la respuesta a una de mis preguntas,
¿Por qué los campos heredados no son inyectados por JAX-RS?
En JSR-311 §3.6 :
Si una subclase o método de implementación tiene alguna anotación JAX-RS, entonces todas las anotaciones en la superclase o el método de interfaz se ignoran.
Estoy seguro de que hay una razón real para esta decisión, pero desafortunadamente ese hecho está trabajando en contra de mí en este caso de uso particular. Todavía estoy interesado en posibles soluciones temporales.
1 La advertencia al usar la inyección de campo es que ahora estoy vinculado a la creación de instancias de clases de recursos por solicitud, pero puedo vivir con eso.
2 Porque soy el que llama new FooService()
lugar del contenedor / la implementación de JAX-RS.
¿Cuál es la motivación de evitar las inyecciones de parámetros?
Si la motivación es evitar la repetición de cadenas codificadas, para que pueda cambiar el nombre fácilmente, puede reutilizar "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 */ }
}
(Perdón por publicar la segunda respuesta, pero fue demasiado larga para ponerla en un comentario de la respuesta anterior)
Aquí hay una solución que estoy usando:
Defina un constructor para el BaseService con ''id'' y ''xyz'' como params:
// 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;
}
}
Repite el constructor en todas las subclases con las inyecciones:
// 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 */ }
}
En RESTEasy uno puede construir una clase, anotar con @ * Param como de costumbre, y terminar anotando la clase @Form. Esta clase @Form puede ser una inyección de parámetro en cualquier llamada a método de cualquier otro servicio. http://docs.jboss.org/resteasy/docs/2.3.5.Final/userguide/html/_Form.html
En cuanto a JIRA de Jax, parece que alguien pidió herencia de anotación como un hito para JAX-RS.
La función que está buscando simplemente no existe en JAX-RS, sin embargo, ¿funcionaría esto? Es feo, pero evita la inyección recurrente.
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 */ }
}
O en otra solución:
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 */ }
}
Pero al ver lo sensible que eres, francamente, dudo que tu frustración se vaya con este feo código :)
En lugar de utilizar @PathParam
, @QueryParam
o cualquier otro parámetro, puede usar @Context UriInfo
para acceder a cualquier tipo de parámetro. Entonces tu código podría ser:
// 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 */ }
}
Preste atención a que getIdParameter
es estático, por lo que puede ponerlo en alguna clase de utilidad y reutilizar acorss varias clases.
Se garantiza que UriInfo es seguro para los hilos, por lo que puede mantener la clase de recurso como singleton.
Puede agregar un proveedor personalizado, particularmente a través de 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<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;
}
}
Por supuesto, tendrás que extraer los parámetros de la ruta y / o los parámetros de la búsqueda de la manera más difícil desde HttpContext, pero lo harás una vez en un solo lugar.
Puedes probar @BeanParam para todos los params que se repiten. así que, en lugar de inyectarlos cada vez, puedes simplemente inyectarte tu CustomBean, que hará el truco.
Otro enfoque que es más limpio es que puedes inyectar
@Context UriInfo
o
@Context ExtendedUriInfo
a su clase de recursos y en un método muy simple puede acceder a ellos. UriInfo es más flexible porque su jvm tendrá un archivo fuente java menos para administrar y, sobre todo, una única instancia de UriInfo o ExtendedUriInfo le ofrece un control de muchas cosas.
@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
}
Siempre tuve la sensación de que la herencia de la anotación hace que mi código no se pueda leer, ya que no es obvio desde dónde y cómo se inyecta (por ejemplo, en qué nivel del árbol de herencia se inyectaría y dónde fue anulado (o fue anulado en todas)). Además, tienes que proteger la variable (y probablemente NO es definitiva), lo que hace que la superclase pierda su estado interno y también puede introducir algunos errores (al menos siempre me preguntaría cuando llamo a un método extendido: ¿la variable protegida se cambia allí? ?). En mi humilde opinión no tiene nada con DRY, ya que esto no es un encapsulamiento de la lógica, sino una encapsulación de la inyección, que me parece exagerada.
Al final, citaré de la especificación JAX-RS 3.6 Herencia de anotación
Para mantener la coherencia con otras especificaciones de Java EE, se recomienda repetir siempre las anotaciones en lugar de confiar en la herencia de la anotación.
PD: admito que solo uso algunas veces la herencia de anotación, pero en el nivel de método :)