Con la piattaforma Java Enterprise 6, tra le varie nuove specifiche, è stata introdotta anche quella conosciuta come Bean Validation. Come abbiamo già avuto modo di parlare in un post su MokaByte e , la Bean Validation dà la possibilità di definire in modo semplice i vincoli e le validazioni direttamente sul modello del nostro dominio. Data la genericità del framework, è possibile applicare le valutazioni dichiarative a tutti i value object che vogliamo. Perché quindi non validare automaticamente i questi oggetti quando sono input dei nostri servizi REST? Vediamo insieme come fare.
Desiderata
Nonostante tutto l’amore o l’odio che si possa provare per un framework come JSF, è innegabile la praticità con la quale il permetta di separare le responsabilità di validazione degli input da quelle di logica applicativa. Purtroppo JSF è molto orientato alle pagine e fortemente imbrigliato ad esse: lo sviluppo web di oggi non è finalizzato esclusivamente alla fruizione di contenuti tramite pagine HTML. E’ più “economico” quindi pensare a servizi (è una vita che si parla di SOA…) e pensare alle pagine HTML (e tanto javascript) come una delle possibili applicazioni client.
Come nel caso di backing bean JSF, quello che vorrei è la garanzia che se una chiamata web viene eseguita correttamente dal mio servizio JAX-RS, significa che il suo input è stato validato e non me ne devo preoccupare nel corpo del servizio stesso: la responsabilità della validazione è delegata a terzi.
Chiariamoci subito le idee con un po’ di codice. Ammettiamo di avere un servizio REST di ricerca del proprietario di un auto in base alla targa:
@Path("/car/info") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class SearchRestService { @Inject private CarInfoService carInfoService; @GET @Path("/{plateNumber}") public Response getCarInfo(@PathParam("plateNumber") String plateNumber) { CarInfo info = carInfoService.find(plateNumber); if (info != null) { return Response.ok(info).build(); } else { return Response.status(Status.NOT_FOUND).build(); } } }
Dove CarInfo
è un semplice oggetto che contiene numero di targa e proprietario.
Il servizio così com’è è perfettamente funzionante, purtroppo però nessuno verifica che plateNumber
sia del formato corretto. Come fare quindi ad esternalizzare il controllo dal servizio usando la bean validation?
Per tutte le questioni ortogonali alla logica applicativa l’AOP ci viene incontro. L’idea quindi sarebbe quella di delegare ad un interceptor la logica di validazione.
In Java EE 6, la bean validation 1.0 e JAX-RS 1.0/1.1 non interagiscono tra di loro: le specifiche quindi non ci vengono incontro, non permettono nemmeno di definire degli interceptors se non tramite CDI (). L’idea è buona ma non reinvestiamo la ruota, c’è chi l’ha già fatto per noi!
RESTEasy e JBoss AS 7
Se siete legati a JAX-RS 1 e se la vostra implementazione di JAX-RS è RESTEasy (come nel caso di JBoss 7), è disponibile un interceptor già pronto per noi, basta includere le dipendenze Maven:
org.jboss.resteasy resteasy-jaxrs 2.3.1.GA provided org.jboss.resteasy resteasy-hibernatevalidator-provider 2.3.1.GA
La prima ci serve per accedere all’implementazione di RESTEasy in compilazione: occhio allo scope provided, non dobbiamo portarcela a runtime. La seconda invece serve per attivare l’interceptor: questa invece la vogliamo a runtime!
Quindi? Come si modifica il codice per attivare la validazione? Basta cambiare la firma del nostro servizio come segue:
@ValidateRequest public CarInfo getCarInfo( @Pattern(regexp = "[a-zA-Z]{2}[0-9]{3}[a-zA-Z]{2}") @PathParam("plateNumber") String plateNumber) { ...
Una volta indicato di voler validare la richiesta (@ValidateRequest
), possiamo usare qualsiasi annotazione della bean validation per validare i parametri in ingresso. A questo punto possono accadere due cose:
-
Validazione corretta: il metodo
getCarInfo
viene correttamente invocato -
Validazione non corretta: viene lanciata una eccezione di validazione non corretta:
org.hibernate.validator.method.MethodConstraintViolationException
Per gestire opportunamente questa eccezione (e mostrare al client un messaggio in JSON o XML per esempio), è necessario implementare un ExceptionMapper:
@Provider public class ValidationExceptionHandler implements ExceptionMapper
{ @Override public Response toResponse(MethodConstraintViolationException exception) { ... return Response.status(Status.BAD_REQUEST).entity( ... ).type(MediaType.APPLICATION_JSON).build(); } }
Da notare due cose in questo caso:- L’annotazione
@Provider
è necessaria per attivare il mapper - La risposta dovrebbe avere un codice HTTP 400 (Bad Request) per essere semanticamente corretta (e più parlante). Essendo una eccezione infatti, il server genera automaticamente un codice 500 (troppo generico…) e maschera il messaggio corretto.
- L’annotazione
RESTEasy e Wildfly
Nella nuova specifica Java EE 7 le cose si sono fatte leggermente più semplici. Prendiamo per esempio Wildfly (a.k.a. JBoss 8), La bean validation è stata aggiornata alla versione 1.1 e JAX-RS alla versione 2.0: quest’ultima supporta l’integrazione con la bean validation! L’unica differenza con JBoss 7 è quindi che non avremo più riferimento diretto a RESTEasy:
public CarInfo getCarInfo( @Pattern(regexp = "[a-zA-Z]{2}[0-9]{3}[a-zA-Z]{2}") @PathParam("plateNumber") String plateNumber) { ...
Il riferimento invece rimane se vogliamo gestire l’eccezione con un Exception Mapper: questa volta l’eccezione da gestire è org.jboss.resteasy.api.validation.ResteasyViolationException
. RESTEasy dispone di un opportuno oggetto che fa da report della validazione fallita, semplificando il mapper:
@Provider public class ValidationExceptionHandler implements ExceptionMapper{ @Override public Response toResponse(ResteasyViolationException exception) { return Response.status(Status.BAD_REQUEST).entity(new ViolationReport(exception)).build(); } }
Per poter usare queste classi è necessario includere l’implementazione di RESTEasy in compilazione:
org.jboss.resteasy resteasy-bom 3.0.6.Final import pom org.jboss.resteasy resteasy-jaxrs provided org.jboss.resteasy resteasy-validator-provider-11 provided
Ulteriori interessanti riferimenti a riguardo li trovate su questo post.
Jersey e Tomcat
Ovviamente Jersey, la Reference Implementation di JAX-RS, non è da meno (anzi, la sua implementazione
della Client API, con qualche modifica, è entrata nella specifica 2.0)!
Ammettiamo di lavorare con Tomcat: essendo un servlet container, a differenza di un application server come JBoss, dobbiamo “riempirlo” con le nostre dipendenze. Includiamo quindi Jersey e il modulo per la bean validation (che si porterà dietro hibernate validator)
org.glassfish.jersey jersey-bom 2.12 pom import org.glassfish.jersey.containers jersey-container-servlet org.glassfish.jersey.media jersey-media-moxy org.glassfish.jersey.ext jersey-bean-validation
Il codice del servizio sarà lo stesso visto per Wildfly, ma stranamente non verrà lanciata nessuna eccezione se la validazione fallisce. Come mai? L’estensione di Jersey per la bean validation richiede che il messaggio di errore sia attivato esplicitamente tramite un parametro nel web.xml (dove si abilita Jersey stesso):
Jersey Web Application org.glassfish.jersey.servlet.ServletContainer com.sun.jersey.config.property.packages it.cosenonjaviste.web.rest.services jersey.config.beanValidation.enableOutputValidationErrorEntity.server true 1
In questo modo non dovremo definire il mapper per le eccezioni, se ci accontentiamo di un report di errori fatto così:
@XmlRootElement public final class ValidationError { private String message; private String messageTemplate; private String path; private String invalidValue; ... }
Maggiori dettagli su come personalizzare la risposta si possono trovare sulla documentazione ufficiale
Conclusioni
Abbiamo visto quindi la bean validation integrata con i nostri servizi REST un po’ in tutte le salse. Trovo molto pulito l’approccio di separare la validazione dell’input dalla logia stessa del servizio: vale quindi la pena perdere un po’ di tempo per configurarla in modo opportuno.
Dagli esempi visti non emerge ancora una lacuna (a mio avviso). Quando infatti i dati di input sono articolati (come un xml o un json) viene facile mapparli in un oggetto Java. Per riusare questo oggetto, sarebbe comodo poter usare il concetto di gruppi di validazione (come già visto insieme nel post su MokaByte), ovvero un gruppo di attributi da validare insieme.
Purtroppo al momento questa possibilità non sembra essere supportata in modo dichiarativo: aspettiamo e vediamo se nei prossimi aggiornamenti lo potremmo fare!