Let's take the following example where we want to :
- Map Java Exception to specific HTTP response with localized messages
- Inject parameters of any type with custom validation
- Define a specialized annotation for specific injectable parameters
@Path("/test") @Produces("application/json") public class TestEndpoint { private static final Logger log = Logger.getLogger(TestEndpoint.class.getName()); private static final ResourceBundle resource = ResourceBundle.getBundle("com.blogspot.avianey"); @Inject TestService service; @POST @Path("{pathParam: \\d+}") public String testMethod( @PathParam("pathParam") Long id, @FormParam("date") String dateStr, @Context HttpServletRequest request) { // verifying that the user is logged in User user = (User) request.getSession().getAttribute("user"); if (user == null) { throw new WebApplicationException(Status.UNAUTHORIZED); } // verifying the format of the sent date Date date = null; SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); try { date = sdf.parse(dateStr); } catch (ParseException pe) { throw new WebApplicationException(Status.BAD_REQUEST); } try { // calling the business logic return service.testOperation(id, date, user); } catch (BusinessException boe) { log.log(Level.FINER, "Business problem while executing testMethod", boe); throw new WebApplicationException( Response.status(Status.BAD_REQUEST) .type(MediaType.TEXT_PLAIN) .entity(resource.getString(boe.getMessage())) .build()); } catch (TechnicalException te) { log.log(Level.FINER, "Technical problem while executing testMethod", te); throw new WebApplicationException( Response.status(Status.INTERNAL_SERVER_ERROR) .type(MediaType.TEXT_PLAIN) .entity(resource.getString("error.internal")) .build()); } } }It's easy to think about the number of duplicated lines of code we would have if such a solution is applied for all our exposed methods... Now it's time to go deeper into Jersey and JAX-RS.
Custom Exception mapping with Jersey
In a Three-Tier architecture, the Logic-Tier may throw an application specific BusinessException, the Data-Tier may throw a TechnicalException, and so on... As BusinessException are relative to business rule violations it might be interresting to alert the user with a comprehensive message in the Presentation-Tier. On the contrary, a synthetic message will be displayed to the user for TechnicalException that refers to problems that are not likely to happen.Jersey makes it possible to bind Java Exception with specialized HTTP response. All we have to do is to register an ExceptionMapper for each Java Exception we want to handle in a generic manner.
@Provider public class BusinessExceptionMapper implements ExceptionMapper<BusinessException> { private static final Logger log = Logger.getLogger(TestEndpoint.class.getName()); private static final ResourceBundle resource = ResourceBundle.getBundle("com.blogspot.avianey"); @Override public Response toResponse(BusinessException e) { log.log(Level.FINER, "Business problem while executing testMethod", e); return Response.status(Status.BAD_REQUEST) .type(MediaType.TEXT_PLAIN) .entity(resource.getString(e.getMessage())) .build(); } }
@Provider public class TechnicalExceptionMapper implements ExceptionMapper<TechnicalException> { private static final Logger log = Logger.getLogger(TestEndpoint.class.getName()); private static final ResourceBundle resource = ResourceBundle.getBundle("com.blogspot.avianey"); @Override public Response toResponse(TechnicalException e) { log.log(Level.FINER, "Technical problem while executing testMethod", e); return Response.status(Status.INTERNAL_SERVER_ERROR) .type(MediaType.TEXT_PLAIN) .entity(resource.getString("error.internal")) .build(); } }Just as service classes, @Provider classes should be placed in a package that is scanned by the Jersey Servlet at startup so they can be used at runtime.
<servlet> <servlet-name>Jersey</servlet-name> <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class> <init-param> <param-name>com.sun.jersey.config.property.packages</param-name> <param-value>pkg.provider; pkg.endpoint</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>We no longer have to handle explicitly BusinessException and TechnicalException in our exposed method :
@Path("/test") @Produces("application/json") public class TestEndpoint { private static final Logger log = Logger.getLogger(TestEndpoint.class.getName()); @Inject TestService service; @POST @Path("{pathParam: \\d+}") public String testMethod( @PathParam("pathParam") Long id, @FormParam("date") String dateStr, @Context HttpServletRequest request) { // verifying that the user is logged in User user = (User) request.getSession().getAttribute("user"); if (user == null) { throw new WebApplicationException(Status.UNAUTHORIZED); } // verifying the format of the sent date Date date = null; SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); try { date = sdf.parse(dateStr); } catch (ParseException pe) { throw new WebApplicationException(Status.BAD_REQUEST); } // calling the business logic // no need to catch exception here anymore return service.testOperation(id, date, user); } }
Custom Jersey parameters
JAX-RS Param annotations like QueryParam, FormParam and PathParam can be apply to any Java Object that have a constructor with a single String argument. When calling a method with such an annotated parameter, Jersey instantiate a new object with the param value and pass it to the method.We can easily use this feature to centralize the validation of date parameters accross all of our exposed methods :
/** * A DateParam to validate the format of date parameters received by Jersey */ public class DateParam { private Date date; public DateParam(String dateStr) { SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); try { this.date = sdf.parse(dateStr); } catch (ParseException pe) { throw new WebApplicationException(Status.BAD_REQUEST); } } public Date value() { return this.date; } }Once again, our original code gained in maintainability :
@Path("/test") @Produces("application/json") public class TestEndpoint { private static final Logger log = Logger.getLogger(TestEndpoint.class.getName()); @Inject TestService service; @POST @Path("{pathParam: \\d+}") public String testMethod( @PathParam("pathParam") Long id, @FormParam("date") DateParam date, @Context HttpServletRequest request) { // verifying that the user is logged in User user = (User) request.getSession().getAttribute("user"); if (user == null) { throw new WebApplicationException(Status.UNAUTHORIZED); } // calling the business logic return service.testOperation(id, date.value(), user); } }
Contextual object injection
Now we will see how it possible to directly inject the logged User into our exposed method. There is two diferent approaches doing this :- Use the @Context annotation with a custom provider to inject the desired Object
- Create a custom annotation and its Injectable and associated InjectableProvider
@Provider public class LoggedUserProvider extends AbstractHttpContextInjectable<User> implements InjectableProvider<Context, Type> { private final HttpServletRequest r; public LoggedUserProvider(@Context HttpServletRequest r) { this.r = r; } /** * From interface InjectableProvider */ @Override public Injectable<user> getInjectable(ComponentContext ic, Context a, Type c) { if (c.equals(User.class)) { return this; } return null; } /** * From interface InjectableProvider * A new Injectable is instanciated per request */ @Override public ComponentScope getScope() { return ComponentScope.PerRequest; } /** * From interface Injectable * Get the logged User associated with the request * Or throw an Unauthorized HTTP status code */ @Override public User getValue(HttpContext c) { final User user = Contestr.getSessionUser(r); if (user == null) { throw new WebApplicationException(Response.Status.UNAUTHORIZED); } return user; } }The exposed method consist now of one line of code only !
@Path("/test") @Produces("application/json") public class TestEndpoint { @Inject TestService service; @POST @Path("{pathParam: \\d+}") public String testMethod( @PathParam("pathParam") Long id, @FormParam("date") DateParam date, @Context User user) { // calling the business logic return service.testOperation(id, date.value(), user); } }In the second approach, we need to define a new annotation :
@Target({ ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface LoggedUser {}Just as seen before, we need to associate the @LoggedUser with a new Injectable for the User type :
@Provider public class LoggedUserProvider implements Injectable<User>, InjectableProvider<LoggedUser, Type> { private final HttpServletRequest r; public LoggedUserProvider(@Context HttpServletRequest r) { this.r = r; } @Override public Injectable<user> getInjectable(ComponentContext cc, LoggedUser a, Type c) { if (c.equals(User.class)) { return this; } return null; } /** * From interface InjectableProvider * A new Injectable is instanciated per request */ @Override public ComponentScope getScope() { return ComponentScope.PerRequest; } /** * From interface Injectable * Get the logged User associated with the request * Or throw an Unauthorized HTTP status code */ @Override public User getValue() { final User user = (User) r.getSession().getAttribute("user"); if (user == null) { throw new WebApplicationException(Response.Status.UNAUTHORIZED); } return user; } }This solution is much more flexible than the previous one as we can build more than one annotation for the same Type :
- @LoggedUser : retrieve the logged in User and throw an HTTP 401 unauthorized status code if no logged in User is associated with the current request.
- @AdminLoggedUser : retrieve the logged in User and throw an HTTP 401 unauthorized status code if no logged in User is associated with the current request or if the User is not an administrator.
@Path("/test") @Produces("application/json") public class TestEndpoint { @Inject TestService service; @POST @Path("{pathParam: \\d+}") public String testMethod( @PathParam("pathParam") Long id, @FormParam("date") DateParam date, @LoggedUser User user) { // calling the business logic return service.testOperation(id, date.value(), user); } }In a next post, I will cover the integration of Jersey with Guice or any other JSR-330 compliant IOC framework.
Great post. When will write a follow up ? (Jersey integration with Guice etc).
RépondreSupprimerI created custom annotation and then inject @LoggedUser approach but getting SEVERE: A message body reader for Custom Java class not found error.
SupprimerCan someone help me?
Ce commentaire a été supprimé par l'auteur.
SupprimerTry to add the @XmlRootElement annotation to your Custom class declaration just like this :
Supprimer@XmlRootElement
public class Custom {
...
}
In this blog post case , the User class should be annotated with @XmlRootElement.
Hi,
RépondreSupprimerI will never write the follow up for the reason that the upcomming JAX-RS 2 (Jersey 2) release uses the HK2 for for dependency injection (JSR-330 implementation).
see project pages here
HK2 project at java.net
Jersey project page at java.net
Jersey 2 source code and samples
Trying to implement the error handling I'm always getting "The RuntimeException could not be mapped to a response"
RépondreSupprimerI have the following configuration:
@Provider
public class DefaultErrorHandler implements ExceptionMapper {
...
web.xml
Users Jersey Servlet
com.sun.jersey.spi.container.servlet.ServletContainer
com.sun.jersey.config.property.packages
com.test.resources.userAdmin;com.test.resources.errors
1
Users Jersey Servlet
/v1/test/*
Any idea what might be the problem ?
You must specify the type of the Exception handled by your DefaultErrorHandler and implement ExceptionMapper<RuntimeException> to catch RuntimeException. Note that subclass of RuntimeException will not be catched... The ExceptionMapper must match your exception type exactly, not one of its supertype.
RépondreSupprimerHey !
RépondreSupprimerI tried to create a custom class to validate the params being passed in to the resource method.
The code enters the custom constructor and ends up creating a WebApplicationException, but when hitting the endpoint to trigger the exception, it is never being thrown from the custom class, or it is being "swallowed" by the framework.
Have you experienced this?
When throwing the same exception in the resource everything looks fine.
hey thanks, part of this code saved me a lot of time
RépondreSupprimer