SpringMVC 4, Rest & Test avec JSON Date Conversions (Jackson2)

springRest_logojackson2-logo-onlyjson

Le titre est déjà tout un programme ! L'objectif est d'illustrer avec plusieurs exemples les nouveautés Rest API dans Spring MVC 4 et en particulier sur le support Jackson2 dans Spring4.

Nous allons voir de quelle manière nous pouvons ignorer certains attributs de classe, puis comment filtrer certains champs dans les conversions/sérialisations Json (ou encore Xml). Les conversions (sérialisation et désérialisation) des dates seront également traitées.

Le test unitaire JUnit à l'aide de RestTemplate permet de mettre en pratique toutes ces notions.

Le projet démo est basé sur spring-boot mais il n'est pas nécessaire pour comprendre ce billet.

1. Configurer le projet maven démo

Une seule dépendance est nécessaire puisque notre pom hérite de celui de spring-boot.

	<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

Or comme nous produisons des tests unitaires, on ajoute l'artifact "spring-boot-starter-test".

Ainsi le pom du projet contient ceci:

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>1.2.0.RC2</version>
	<relativePath/> 
</parent>
 <properties><java.version>1.8</java.version></properties>
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
         <!--  test -->
	<dependency>
	 <groupId>org.springframework.boot</groupId>
	 <artifactId>spring-boot-starter-test</artifactId>
	 <scope>test</scope>
	</dependency>
    </dependencies>	 
pom.xml

Notez que java 8 est utilisé, par conséquent il est important d'avoir installé jdk8 en amont.

2. Le use-case

Notre use case est simple mais réaliste. Il s'agit de répondre à une requête client : lister une ou plusieurs personnes répertoriée(s) dans notre base de données.

En fonction d'un critère (ou profil), on souhaite pouvoir afficher la totalité ou une partie des informations de ces personnes.

Ainsi si le client lance l'uri /personResume , nous lui retournerons du json , juste un résumé de l'identité de la personne (ici l'id &  le nom seulement).

Par contre, la réponse json comporte l'id, le nom, la localisation et la date de naissance de la personne lorsque l'url est /personDetaille.

Mais alors comment faire? La suite de ce billet présente les étapes détaillées.

3. Le modèle

Écrivons notre classe modèle Person en tenant compte de notre cas d'utilisation. Pour l'instant Person est un POJO simple ayant ces attributs: id (int), name(String), locations(Lis<String>), dateNce(java.util.Date).

public class Person {
	private int id;

	private String name;

	private List<String> locations;

	private Date dateNce;
        //getters setters omis
}

A l'étape suivante on va enrichir la classe Person avec des annotations sur ces attributs, ce qui nous permettra de les filtrer lors de la conversion java <=> JSON or XML.

 

4. Filtrer les conversions Jackson

Pour réaliser ces filtres, nous procéderons en deux temps :

Etape 1. Écrivons ces deux interfaces vides comme suit:

public class Views {

	public interface Resume {};

	public interface Detaille extends Resume {};
}

Etape2. Enrichir la classe Person avec des annotations du filtre Jackson2:

public class Person {
	private int id;
	private String name;
	private List<String> locations;
	private Date dateNce;

       //setters annotés
	@JsonView(Views.Resume.class)
	public void setId(int id) {
		this.id = id;
	}
	@JsonView(Views.Resume.class)
	public void setName(String name) {
		this.name = name;
	}
	@JsonView(Views.Detaille.class)
	public void setDateNce(Date dateNce) {
		this.dateNce = dateNce;
	}
	@JsonView(Views.Detaille.class)
	public void setLocations(List<String> locations) {
		this.locations = locations;
	}
Person annote

Explication: com.fasterxml.jackson.annotation.JsonView est une annotation de jackson 2. Spring MVC offre désormais le support de Jackson’s Serialization Views.

5. Rest controller

Il est temps de construire le controller de notre webservice Rest pour répondre aux requêtes http des clients. Avant cela, ajoutons cette méthode simple du service :

import static java.util.Arrays.asList;
import java.util.*;
@Component
public class PersonService implements IPersonService {
	@Override
	public Person getPersonDetail(Integer id){
		Person p = new Person();
		p.setId(id);		
		p.setName("Abdou");
		List<String> locs = new ArrayList<>(asList("Paris", "Rouen"));
		p.setLocations(locs);
		p.setDateNce(Calendar.getInstance().getTime());
		return p;
	}
}

La méthode getPersonDetail simule la récupération de données de notre BD. Voici le contenu du controller Rest qui ne fait qu'appeler le service:

@RestController
public class PersonController {

    @Autowired IPersonService personService;

    /*****@JsonView(Views.Resume.class)***/
    @RequestMapping(value = "/personResume")
    public Person getPerson(
            @RequestParam(value = "id", required = false, defaultValue = "0") Integer id) {
        return personService.getPersonDetail(id);
    }

    /******@JsonView(Views.Detaille.class)*****/

    @RequestMapping(value = "/personDetaille")
    public Person getPersonDetail(
         @RequestParam(value = "id", required = false, defaultValue = "0") Integer id) {
        return personService.getPersonDetail(id);
    }
}

Quelques points méritent que l'on apporte de petites précisions :

  • La classe est annotée avec @RestController (@RestController combine @ResponseBody and @Controller, comme le dit la doc de réf de Spring),
  • Deux méthodes du controller utilisant la même méthode du service: La première getPerson pour répondre à l'uri /personResume.json La seconde répond à /personDetaille.json,
  • Enfin, la seule différence entre ces deux méthodes correspond aux deux annotations laissées pour l'instant en commentaire:
    @JsonView(Views.Resume.class) et @JsonView(Views.Detaille.class)

    Il est donc indispensable de dé-commenter ces deux lignes pour activer les filtres de sérialisation/désérialisation définis dans le modèle à l'étape2.

NOTE. Il est indispensable d'appliquer l'annotation @JsonView ( Views.xxxxx.class) aux méthodes getPersonXXXX du controller afin d'activer ces filtres.

Par exemple, appliquer l'annotation @JsonView (Views.Detaille.class) à la méthode getPersonDetail fournit un json avec l'ensemble des attributs de Person.

6. Tester le RestController

Le test JUnit permet de valider les méthodes de notre webservice Rest. Là encore Spring rend les choses faciles encore plus simples.

En effet, une classe JUnit saupoudrée avec quelques annotations de Spring et puis RestTemplate fait presque tout. Voici donc le code du test unitaire JUnit 4:

import com.moi.jacksonSpring4.config.AppConfig;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.client.RestTemplate;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=AppConfig.class)
@WebAppConfiguration()

public class TestsRestSpring4 {
    //adapter selon le context path de l'appli web
    final String BASE_URL = "http://localhost:8080/Spring4/";

    final String URI_RESUME = BASE_URL + "personResume";
    final String URI_DETAILLE = BASE_URL + "personDetaille";

    @Autowired PersonController personCtrl;
    @Autowired <code>RestTemplate restTemplate;
Test Junit

Attention dans ce test il y a des adaptations à faire  :

  • Il manque la classe AppConfig permettant de configurer Spring4, voir ci-dessous.
  • La constante BASE_URL doit être adaptée selon votre web contexte (context path).

Notez l'assertion sur p.getLocations() à null dans la méthode testGetPersonResume.

Ceci confirme que le filtre @JsonView (Views.Resume.class) ne retourne pas les locations de Person comme sera illustré ci-après. Et comme indiqué, voici le code de la classe AppConfig ainsi que l'initialisation par java du projet web puisqu'on utilise pas web.xml.

import org.springframework.context.annotation.*;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@ComponentScan("com.moi.xxxxxxxxxxx")
@EnableWebMvc
public class AppConfig {
    //used only for tests
    @Bean(name = "restTemplate")
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
AppConfig

Pensez à adapter l'argument de @ComponentScan avec le nom de votre package. Et pour finir la classe WebApplicationInitializer nécessaire à l'initialisation du projet web (lorsqu'on utilise pas entièrement spring-boot).

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

public class WebAppInitializer implements WebApplicationInitializer {
  public void onStartup(ServletContext servletContext) throws ServletException {
    AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
    ctx.register(AppConfig.class);
    ctx.setServletContext(servletContext);
    Dynamic dyn = servletContext.addServlet("dispatcher",new DispatcherServlet(ctx));
    dyn.addMapping("/");
    dyn.setLoadOnStartup(1);
  }
}

 

7. Exécuter le test

Avant de lancer le test, assurez-vous que votre projet web est lancé sur un serveur web (tomcat ou autre) et est en écoute sur le port (par défaut) 8080.
La première capture illustre l'appel à /personResume qui retourne uniquement l'id et le nom.
RestResumePerson

Alors que getPersonDetaille retourne la liste des locations comme le montre la capture ci-dessous :RestDetaillePerson

Ooups! La date de naissance (dateNce) est mal affichée. C'est illisible !
La seconde partie de ce billet tente de remédier à cela.
Mais avant, attendez un peu!
Où déclare t-on la conversion de Jackson2 ?

Spring et RestTemplate viennent avec une liste de "MessageConverters" configurée par défaut. En fonction du type de média  (content-type défini dans le header de la requête) le bon converter est fixé automatiquement.

Par exemple, lorsque le type média est "application/json" la réponse (au format json) est fournie avec le converter nommé "MappingJacksonHttpMessageConverter".

A l'aide du plugin "Advanced Rest Client", vous pouvez tester Rest et voir le type média (sur la capture application/json).

CaptureAdvRestClient

A bientôt!

 

Un commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.