web-dev-qa-db-fra.com

Spring MVC 3: renvoyer une page de données de printemps au format JSON

J'ai une couche d'accès aux données réalisée avec Spring-Data. Je crée maintenant une application Web par dessus. Cette méthode de contrôleur devrait renvoyer une Spring-Data Page formatée en JSON.

Une telle page est une liste avec des informations supplémentaires sur la pagination, telles que le nombre total d'enregistrements, etc.

Est-ce possible et si oui comment?

Et directement lié à cela, puis-je définir le mappage des noms de propriété? Par exemple. ce qui signifie que j'aurais besoin de définir comment les propriétés d'informations de pagination sont nommées en JSON (différemment de celles de la page). Est-ce possible et comment?

23
beginner_

Il existe un soutien pour un scénario comme celui-ci à venir dans Spring HATEOAS et Spring Data Commons. Spring HATEOAS est livré avec un objet PageMetadata qui contient essentiellement les mêmes données qu’un Page mais de manière moins contraignante, de sorte qu’il puisse être plus facilement marshalé et non mélangé.

Une autre raison pour laquelle nous implémentons cela en combinaison avec les communes Spring HATEOAS et Spring Data est qu’il est peu utile de simplement marshaler la page, son contenu et les métadonnées, mais aussi de générer les liens vers les pages suivantes ou précédentes existantes. le client n'a pas besoin de construire des URI pour parcourir ces pages lui-même.

Un exemple

Supposons une classe de domaine Person:

class Person {

  Long id;
  String firstname, lastname;
}

ainsi que son référentiel correspondant:

interface PersonRepository extends PagingAndSortingRepository<Person, Long> { }

Vous pouvez maintenant exposer un contrôleur Spring MVC comme suit:

@Controller
class PersonController {

  @Autowired PersonRepository repository;

  @RequestMapping(value = "/persons", method = RequestMethod.GET)
  HttpEntity<PagedResources<Person>> persons(Pageable pageable, 
    PagedResourcesAssembler assembler) {

    Page<Person> persons = repository.findAll(pageable);
    return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);
  }
}

Il y a probablement beaucoup à expliquer ici. Passons aux choses étape par étape:

  1. Nous avons un contrôleur Spring MVC qui connecte le référentiel. Cela nécessite que les données de ressort soient configurées (via @Enable(Jpa|Mongo|Neo4j|Gemfire)Repositories ou les équivalents XML). La méthode du contrôleur est mappée sur /persons, ce qui signifie qu'il acceptera toutes les requêtes GET à cette méthode.
  2. Le type de noyau renvoyé par la méthode est un PagedResources - un type de Spring HATEOAS qui représente un contenu enrichi avec Links plus un PageMetadata
  3. Lorsque la méthode est appelée, Spring MVC devra créer des instances pour Pageable et PagedResourcesAssembler. Pour que cela fonctionne, vous devez activer le support Web Spring Data soit par l'annotation @EnableSpringDataWebSupport sur le point d'être introduite dans le prochain jalon de Spring Data Commons, soit via des définitions de beans autonomes (documenté ici ).

    Pageable sera renseigné avec les informations de la demande. La configuration par défaut transformera ?page=0&size=10 en Pageable demandant la première page avec une taille de page de 10.

    PageableResourcesAssembler vous permet de transformer facilement un Page en une instance de PagedResources. Cela ajoutera non seulement les métadonnées de page à la réponse, mais également les liens appropriés vers la représentation en fonction de la page à laquelle vous accédez et de la configuration de votre résolution Pageable.

Un exemple de configuration JavaConfig pour l'activer pour JPA ressemblerait à ceci:

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
@EnableJpaRepositories
class ApplicationConfig {

  // declare infrastructure components like EntityManagerFactory etc. here
}

Un exemple de demande et de réponse

Supposons que nous ayons 30 Persons dans la base de données. Vous pouvez maintenant déclencher une requête GET http://localhost:8080/persons et vous verrez quelque chose de similaire à ceci:

{ "links" : [
    { "rel" : "next", "href" : "http://localhost:8080/persons?page=1&size=20 }
  ],
  "content" : [
    … // 20 Person instances rendered here
  ],
  "pageMetadata" : {
    "size" : 20,
    "totalElements" : 30,
    "totalPages" : 2,
    "number" : 0
  }
}

Notez que l'assembleur a généré l'URI correct et sélectionne également la configuration par défaut présente pour résoudre les paramètres dans un Pageable pour une requête à venir. Cela signifie que si vous modifiez cette configuration, les liens adhéreront automatiquement à la modification. Par défaut, l'assembleur pointe vers la méthode du contrôleur dans laquelle il a été appelé, mais peut être personnalisé en définissant un Link personnalisé à utiliser comme base pour créer les liens de pagination vers les surcharges de la méthode PagedResourcesAssembler.toResource(…).

Perspective

Les bits PagedResourcesAssembler seront disponibles dans la prochaine version jalon du train Spring Data Babbage release . Il est déjà disponible dans les instantanés actuels. Vous pouvez en voir un exemple de travail dans mon Spring RESTBucks exemple d'application . Il suffit de le cloner, d'exécuter mvn jetty:run et de curl http://localhost:8080/pages.

51
Oliver Drotbohm

Oliver, votre réponse est excellente et je la marque comme une réponse. Ici, juste pour être complet, ce que j’ai imaginé pour le moment pourrait être utile pour quelqu'un d’autre.

J'utilise JQuery Datatables comme widget de grille/tableau. Il envoie un paramètre très spécifique au serveur et exclut une réponse très spécifique: voir http://datatables.net/usage/server-side .

Pour ce faire, un objet d'assistance personnalisé est créé, reflétant les attentes de datatables. Notez que getter et setter doivent être nommés comme ils le sont autrement, le json produit est incorrect (les noms de propriété sensibles à la casse et les tables de données utilisent cette "pseudo notation hongroise" ...).

public class JQueryDatatablesPage<T> implements Java.io.Serializable {

    private final int iTotalRecords;
    private final int iTotalDisplayRecords;
    private final String sEcho;
    private final List<T> aaData;

    public JQueryDatatablesPage(final List<T> pageContent,
            final int iTotalRecords,
            final int iTotalDisplayRecords,
            final String sEcho){

        this.aaData = pageContent;
        this.iTotalRecords = iTotalRecords;
        this.iTotalDisplayRecords = iTotalDisplayRecords;
        this.sEcho = sEcho;
    }

    public int getiTotalRecords(){
        return this.iTotalRecords;
    }

    public int getiTotalDisplayRecords(){
        return this.iTotalDisplayRecords;
    }

    public String getsEcho(){
        return this.sEcho;
    }

    public List<T> getaaData(){
        return this.aaData;
    }
}

La deuxième partie est une méthode dans le contrôleur correspondant:

@RequestMapping(value = "/search", method = RequestMethod.GET, produces = "application/json")
public @ResponseBody String search (
        @RequestParam int iDisplayStart,
        @RequestParam int iDisplayLength,
        @RequestParam int sEcho, // for datatables draw count
        @RequestParam String search) throws IOException {

    int pageNumber = (iDisplayStart + 1) / iDisplayLength;
    PageRequest pageable = new PageRequest(pageNumber, iDisplayLength);
    Page<SimpleCompound> page = compoundService.myCustomSearchMethod(search, pageable);
    int iTotalRecords = (int) (int) page.getTotalElements();
    int iTotalDisplayRecords = page.getTotalPages() * iDisplayLength;
    JQueryDatatablesPage<SimpleCompound> dtPage = new JQueryDatatablesPage<>(
            page.getContent(), iTotalRecords, iTotalDisplayRecords,
            Integer.toString(sEcho));

    String result = toJson(dtPage);
    return result;

}

private String toJson(JQueryDatatablesPage<?> dt) throws IOException {
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new Hibernate4Module());
    return mapper.writeValueAsString(dt);
}

compoundService est pris en charge par un référentiel Spring-Data. Il gère les transactions et la sécurité au niveau de la méthode. La méthode toJSON() utilise Jackson 2.0 et vous devez enregistrer le module approprié dans le mappeur, dans mon cas pour hibernation 4.

Si vous avez des relations bidirectionnelles, vous devez annoter toutes vos classes d’entités avec 

@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="jsonId")

Cela permet à Jackson 2.0 de sérialiser les dépendances circulaires (n’était pas possible dans les versions antérieures et nécessite que vos entités soient annotées).

Vous devrez ajouter les dépendances suivantes:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.2.1</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-hibernate4</artifactId>
    <version>2.2.1</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.2.1</version>
    <type>jar</type>
</dependency>
5
beginner_

En utilisant Spring Boot (et pour Mongo DB), j’ai pu obtenir les résultats suivants:

@RestController
@RequestMapping("/product")
public class ProductController {
   //...
    @RequestMapping(value = "/all", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE })
       HttpEntity<PagedResources<Product>> get(@PageableDefault Pageable p, PagedResourcesAssembler assembler) {
       Page<Product> product = productRepository.findAll(p);
       return new ResponseEntity<>(assembler.toResource(product), HttpStatus.OK);
    }
}

et la classe de modèle est comme ceci:

@Document(collection = "my_product")
@Data
@ToString(callSuper = true)
public class Product extends BaseProduct {
    private String itemCode;
    private String brand;
    private String sku;    
}
0
Pramod Alagambhat