BeanValidation e JAX-RS

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 qua su CoseNonJaviste, 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 ciclo di vita di una richiesta 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 (che abbiamo già avuto modo di vedere insieme). 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:

<dependency>
   <groupId>org.jboss.resteasy</groupId>
   <artifactId>resteasy-jaxrs</artifactId>
   <version>2.3.1.GA</version>
   <scope>provided</scope>
</dependency>

<dependency>
   <groupId>org.jboss.resteasy</groupId>
   <artifactId>resteasy-hibernatevalidator-provider</artifactId>
   <version>2.3.1.GA</version>
</dependency>

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<MethodConstraintViolationException> {
    
       @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.

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<ResteasyViolationException> {
   @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:
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-bom</artifactId>
            <version>3.0.6.Final</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>
 
<dependencies>
    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-jaxrs</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-validator-provider-11</artifactId>
        <scope>provided</scope>
    </dependency>
</dependencies>

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)

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.glassfish.jersey</groupId>
			<artifactId>jersey-bom</artifactId>
			<version>2.12</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>
<dependencies>
	<dependency>
		<groupId>org.glassfish.jersey.containers</groupId>
		<artifactId>jersey-container-servlet</artifactId>
	</dependency>
	<dependency>
		<groupId>org.glassfish.jersey.media</groupId>
		<artifactId>jersey-media-moxy</artifactId>
	</dependency>
	<dependency>
		<groupId>org.glassfish.jersey.ext</groupId>
		<artifactId>jersey-bean-validation</artifactId>
	</dependency>
</dependencies>

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):

<servlet>
	<servlet-name>Jersey Web Application</servlet-name>
	<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
	<init-param>
		<param-name>com.sun.jersey.config.property.packages</param-name>
		<param-value>it.cosenonjaviste.web.rest.services</param-value>
	</init-param>
	<init-param>
		<param-name>jersey.config.beanValidation.enableOutputValidationErrorEntity.server</param-name>
		<param-value>true</param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
</servlet>

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!

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio curriculum pubblico. Follow me on Twitter - LinkedIn profile - Google+