One JAX-RS Resource Method Designation Per Java Method
JAX-RS literature warns that only one JAX-RS method designation annotation is allowed per method in a Java class resource. In other words, I should only be able to apply @GET or @POST or @PUT or @DELETE to a method, but never apply more than one of them to the same method. This actually seems sensible, but it prevents some of the "trickery" one commonly saw in servlet applications where the doPost and doGet methods were set up so that one called the other or they both called the same piece of code.
To find out what the negative consequences are of specifying multiple JAX-RS method designators on the same method, I took the method
addMovieOfTheDay
from my previous blog post (JAX-RS and the REST Uniform Interface) and added the @POST
annotation on top of its existing @PUT
annotation. The altered code is shown next:Two JAX-RS Method Designation Annotations for a Single Method
/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@PUT
@POST
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDay(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
final Map<Integer, String> moviesOfTheMonth =
MOVIE_OF_THE_DAY.get(month-1);
if (moviesOfTheMonth != null)
{
moviesOfTheMonth.put(date, title);
}
return generateAddedMovieHtml(title);
}
The above code compiles without incident. This is not surprising because, as the JAX-RS 1.1 specification states (Section 3.3 Resource Models, emphasis added), "A request method designator is a runtime annotation that is annotated with the @HttpMethod annotation." However, when I try to deploy it following the steps outlined in my blog post JAX-RS with Jersey: An Introduction, I see an error in the GlassFish v3 web-based Administrative Console. A screen snapshot of that is shown next.
The output states that an IllegalStateException has been thrown and suggests seeing the logs for additional details. There is a very lengthy stack trace in the logs, but the most significant piece of it is this:
[#|2010-02-14T19:43:38.495-0700|SEVERE|glassfishv3.0|com.sun.jersey.server.impl.application.WebApplicationImpl|_ThreadID=25;_ThreadName=Thread-1;|A (sub-)resource method, public java.lang.String rmoug.td2010.rest.MovieOfTheDay.addMovieOfTheDay(java.lang.Integer,java.lang.Integer,java.lang.String), should have only one HTTP method designator. It currently has the following designators defined: [@javax.ws.rs.PUT(), @javax.ws.rs.POST()]|#]
With the context of the situation in mind, this is pretty straightforward: "only one HTTP method designation" is allowed for a resource method and the method "[addMovieOfTheDay(Integer,String)] currently has the designators ... @PUT ... @POST [defined]." Okay, so that is bad. Note to self: The advice to only use one method designator per class resource method should be heeded.
As I wrote this post, I verified that NetBeans 6.8 continues the tradition of creating new Java servlets with the automatically generated
doPost
and doGet
methods calling the same protected method (processRequest
in NetBeans 6.8, though I seem to recall this method had a different name previously, such as doProcess()
). I decided the next thing to try was annotating two separate methods with respective annotations, but have them call the same underlying method. My assumption was that this would work fine. The code for this is shown in the next listing.Two Designators/Two Methods But with Single Common Method
/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@PUT
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDay(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovie(month, date, title);
}
/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@PUT
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDayPost(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovie(month, date, title);
}
/**
* "Common" method for adding/updating a movie of the day that is intended
* to support PUT and POST.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
private String addMovie(final Integer month, final Integer date, final String title)
{
final Map<Integer, String> moviesOfTheMonth =
MOVIE_OF_THE_DAY.get(month-1);
if (moviesOfTheMonth != null)
{
moviesOfTheMonth.put(date, title);
}
return generateAddedMovieHtml(title);
}
This code compiles without incident because I appended "Post" to the method name of the added method. However, I "forgot" to change the annotation to
@Post
for the new method and this became obvious when I tried to deploy to GlassFish. I saw the same general error as before in the console, but the logs had details on the new problem:
[#|2010-02-14T20:43:06.557-0700|SEVERE|glassfishv3.0|com.sun.jersey.server.impl.application.WebApplicationImpl|_ThreadID=25;_ThreadName=Thread-1;|A resource, class rmoug.td2010.rest.MovieOfTheDay, has ambiguous sub-resource method for HTTP method PUT, URI path template /{month}/{date}/{title}, and output mime-type: text/html. The problematic mime-type sets (as defined by @Produces annotation at Java methods addMovieOfTheDay and addMovieOfTheDayPost) are [text/html] and [text/html]|#]
This SEVERE-level log message is telling us that the two methods annotated with
@PUT
are ambiguous because they have the same URI path and same MIME type. One of these needs to be different to allow the JAX-RS provider to differentiate them. For our purposes, I simply change the new method addMovieOfTheDayPost
to have the @POST
annotation rather than the @PUT
annotation.Method Intended for POST Annotated with @POST
/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@POST
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDayPost(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovie(month, date, title);
}
The code again compiles and even deploys this time. I don't show it here, but using RESTClient to access the resource via PUT and via POST works for both HTTP methods. In short, it does appear that one can delegate functionality from two different Java methods associated with different HTTP methods to the same common method.
The last thing I wanted to try in this area was having one method call the other rather than two methods calling a third, common method. To try this, I changed the method added for POST support to simply call the method for PUT:
POST-Supporting Method Calling PUT-Supporting Method
/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@PUT
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDay(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
final Map<Integer, String> moviesOfTheMonth =
MOVIE_OF_THE_DAY.get(month-1);
if (moviesOfTheMonth != null)
{
moviesOfTheMonth.put(date, title);
}
return generateAddedMovieHtml(title);
}
/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@POST
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDayPost(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovieOfTheDay(month, date, title);
}
This approach compiled successfully, deployed successfully, and I was able to invoke both the PUT-supporting and POST-supporting Java methods successfully. So, the approach of having one method directly call the other seems to work as well if a developer wants to get around the limitation of not having multiple HTTP method designations on the same Java method.
Only Public Methods Allowed as Resource Methods
Section 3.3.1 ("Visibility") of the JAX-RS 1.1 specification states: "Only public methods may be exposed as resource methods. An implementation SHOULD warn users if a
non-public method carries a method designator or @Path annotation."
Let's see what happens with Jersey in this situation.
To test this one out, I change the just-added method supporting POST to be package scope by removing its
public
modifier.Package-level @POST-annotated Method
/**
* Add a new entry or update an existing entry representing a movie of the day
* via HTTP POST.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@POST
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
/*public*/ String addMovieOfTheDayPost(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovieOfTheDay(month, date, title);
}
In this case, the code compiles and the WAR deploys without apparent incident, but things go less smoothly when a client attempts to use POST to add/change a movie. The package-level method is not available and this is made evident in the RESTClient GUI as shown in the next screen snapshot.
As the above image demonstrates, an HTTP response code of 405 ("Method Not Allowed") is returned when there is no
public
method associated with the specified HTTP method. As the specification suggests, Jersey does warn of this condition with the following WARNING-level logged statement:
[#|2010-02-14T21:13:31.226-0700|WARNING|glassfishv3.0|com.sun.jersey.server.impl.application.WebApplicationImpl|_ThreadID=28;_ThreadName=Thread-1;|A sub-resource method, java.lang.String rmoug.td2010.rest.MovieOfTheDay.addMovieOfTheDayPost(java.lang.Integer,java.lang.Integer,java.lang.String), MUST be public scoped otherwise the method is ignored|#]
Conclusion
The messages Jersey logs related to problems associated with resource method designations are relatively clear and straightforward. That is good news because it means that when these errors are unintentionally caused, they are more likely to be readily identified and addressed.
3 comments:
Thanks. Keep up the great Jersey articles.
That's a nice little job you did there. Very interesting.
I'm trying to do something similar with two methods having the same @Path but a different request; namely
a @Get and a @Put.
like this:
@POST
@Path("/{month}/{date}/{title}")
public String addMovieOfTheDay(...
@GET
@Path("/{month}/{date}/{title}")
public String getMovieOfTheDay( ...
Weird - but I get a 405 (Method not found)
Do you know if this is supposed to work?
Good article.
I'm doing something similar but running into a 405
Surely this is not ambiguous?
@PUT
@Path("/{month}/{date}/{title}")
public void addMovieOfTheDay(
@GET
@Path("/{month}/{date}/{title}")
public String getMovieOfTheDay(
Do you happen to know if this is supposed to be ambiguous?
Post a Comment