6
votes

Spring Boot ne parvient pas à renvoyer JSON pour un objet mais pas pour la liste des objets

Je développe ma première application Spring Boot et j'ai rencontré un problème étrange. La configuration est très basique:

const doesNotWork = 'https://boot.exmpledomain.com:8443/get/guy/1'; 
const doesWork = 'https://boot.exmpledomain.com:8443/get/guys'; 
const headers = {
    headers: {
    'Content-Type': 'application/json;charset=UTF-8'
    }
}
axios.get(doesNotWork, headers)
    .then(res => {
        console.log(res); 
    })
    .catch(error => {   
        console.log("error", error);
        const errorMsg = 'There was an error fetching the menu';
    });

Nous avons du code Javascript sur une page appelant ces deux services. Lorsque le contrôleur renvoie un objet Guy dans la première méthode, nous obtenons une réponse vide:

return dispatch => {
        dispatch(fetchMenuStart());
        const url = 'https://boot.ourcompany.com:8443/get/guy/1'; 
        const headers = {
            headers: {
                'Content-Type': 'application/json'
            }
        }
        axios.get(url, headers)
            .then(res => {
                console.log(res); 
                dispatch(fetchMenuSuccess(res.data.categories, res.data.restaurant));
            })
            .catch(error => {   
                console.log("error", error);
                const errorMsg = 'There was an error fetching the menu';
                dispatch(fetchMenuFail(errorMsg)); 
            });
    };

Lorsque nous retournons une liste d'objets Guy à partir de la deuxième méthode, cependant, nous obtenons la structure Json complète

package com.pawsec.kitchen.controller;

import java.util.ArrayList;
import java.util.List;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.pawsec.kitchen.model.Guy;

@RestController
public class GuyController {

    @RequestMapping(value="/get/guy/{guyId}", method=RequestMethod.GET,
            headers={"Accept=application/json"})
    public Guy getGuy(@PathVariable("guyId") int guyId) {
        Guy someGuy = new Guy(guyId, "Walter Sobchak", 45);
        return someGuy;
    }

    @RequestMapping(value="/get/guys", method=RequestMethod.GET,
            headers={"Accept=application/json"})
    public List<Guy> getGuys() {
        Guy walter = new Guy(1, "Walter Sobchak", 45);
        Guy theDude = new Guy(2, "Jeffrey Lebowski", 42);
        Guy donny = new Guy(3, "Theodore Donald Kerabatsos", 39);
        List<Guy> guys = new ArrayList<Guy>();
        guys.add(walter);
        guys.add(theDude);
        guys.add(donny);
        return guys;
    }

}

Le contrôleur ressemble à ceci:

back:
{data: Array(3), status: 200, statusText: "", headers: {…}, config: {…}, …}
config: {adapter: ƒ, transformRequest: {…}, transformResponse: {…}, timeout: 0, xsrfCookieName: "XSRF-TOKEN", …}
data: Array(3)
0: {guyId: 1, name: "Walter Sobchak", age: 45}
1: {guyId: 2, name: "Jeffrey Lebowski", age: 42}
2: {guyId: 3, name: "Theodore Donald Kerabatsos", age: 39}
length: 3
: Array(0)
headers: {content-type: "application/json;charset=UTF-8", cache-control: "private", expires: "Thu, 01 Jan 1970 00:00:00 GMT"}
request: XMLHttpRequest {onreadystatechange: ƒ, readyState: 4, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload, …}
status: 200
statusText: ""
: Object

Étrangement, si j'appelle ces deux services depuis un navigateur, j'obtiens la structure Json correcte pour les deux appels.

Quand j'exécute une dépendance mvn: tree, les dépendances Jackson attendues qui viennent avec un projet de démarrage de base sont là.

Voici à quoi ressemble le code JavaScript:

    {data: "", status: 200, statusText: "", headers: {…}, config: {…}, …}
config: {adapter: ƒ, transformRequest: {…}, transformResponse: {…}, timeout: 0, xsrfCookieName: "XSRF-TOKEN", …}
data: ""
headers: {}
request: XMLHttpRequest {onreadystatechange: ƒ, readyState: 4, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload, …}
status: 200
statusText: ""
: Object

Quelqu'un peut-il suggérer ce qui pourrait en être la cause ou des étapes à tester pour résoudre le problème?

Nouveau code d'exemple javascript:

    <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.pawsec</groupId>
    <artifactId>kitchen</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>kitchen</name>
    <description>The Kitchen restaurant system</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency> 
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.pawsec</groupId>
            <artifactId>common</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <executable>true</executable>
                </configuration>
            </plugin>
        </plugins>
    </build>


</project>


26 commentaires

avez-vous défini le type de contenu comme application / json dans l'appel ajax?


Si vous obtenez la bonne réponse du navigateur mais incorrecte du code js, il y a évidemment un problème avec le code js. Pourriez-vous s'il vous plaît ajouter l'extrait de code js que vous utilisez?


Avec quelle URL / chemin essayez-vous d'accéder à "/ get / guy"? ... Je m'attendrais à ce que / get / guy / 1 , / get / guy / 2 et / get / guy / 3 fonctionnent. (Avez-vous noté la "variable de chemin" {guyId} ?)


veuillez fournir le code JavaScript à partir duquel vous appelez le service


@ xerx593 - ceci est juste un exemple de code simplifié pour illustrer le problème


..peut nous montrer "du code Javascript".


J'ai ajouté le code JavaScript


@MatsAndersson, avez-vous corrigé cette erreur?


@ MichałZiober non, nous n'avons pas. Nous attendons de l'aide ici


@MatsAndersson, puisque vous obtenez la bonne réponse lorsque vous appelez cela via le navigateur. Le problème est dans votre code frontal. Pouvez-vous ajouter le code frontal complet? Ajoutez également le code de l'autre requête


@MatsAndersson, j'ai configuré l'application Spring Boot comme dans votre exemple et cela fonctionne pour moi. Cela doit être un problème côté client. Vous utilisez la bibliothèque axios pour charger les données depuis le serveur. Pourquoi utilisez-vous URL avec le domaine? Vous ne pouvez pas simplement utiliser const url = '/ get / guy / 1'; Chargez-vous des données d'un autre domaine? Avez-vous une configuration globale personnalisée pour axios ? En outre, la réponse côté serveur avec 200 , ce qui signifie que le côté serveur a renvoyé une chaîne vide comme un résultat réussi.


@ MichałZiober. Oui, le client est sur un domaine différent des services, qui sont dans une application Spring Boot, qui fournit nos micro-services. La même configuration axios est utilisée pour tous les appels de service. Ils fonctionnent tous sauf celui-ci. Si j'appelle ce service à partir d'un navigateur Web, je peux voir clairement que la réponse n'est pas vide. De plus, si j'appelle cette URL de Postman, j'obtiens du succès et une réponse contenant la structure de données correcte.


code d'exemple javascript ajouté


avez-vous dit à spring de convertir le corps de la chaîne de réponse en JSON? Vous devez soit ajouter produit = "application / json" comme paramètre de l'annotation @RequestMapping OU vous pouvez utiliser l'annotation @ResponseBody au niveau du contrôleur.


Utilisez @JsonSerialize pour la classe Guy et ajoutez @Produces (MediaType.APPLICATION_JSON) sur votre méthode


Pouvez-vous essayer d'utiliser ObjectMapper pour sérialiser le modèle avant de renvoyer la réponse dans votre classe de contrôleur?


Vous ne spécifiez pas le type de retour. @RequestMapping (value = "/ get / guy / {guyId}", method = RequestMethod.GET, produit = MediaType.APPLICATION_JSON_VALUE)


Merci @agam. J'ai essayé ce que vous avez suggéré. J'ai essayé ceux-ci et il n'y a eu aucun changement. Veuillez comprendre que je peux appeler ces deux services à partir d'un navigateur et qu'ils renvoient tous les deux Json.


Merci @EddieB. J'ai essayé ceci et nous obtenons exactement les mêmes résultats. Veuillez comprendre que je peux appeler ces deux services à partir d'un navigateur et qu'ils renvoient tous les deux Json.


Merci @RamachandraAPai. Nous avons plusieurs projets Spring qui produisent tous automatiquement Json, donc passer à une solution où chaque méthode de contrôleur devrait utiliser un ObjectMapper pour produire manuellement Json ne semble pas être un changement attrayant. J'espère que je n'interprète pas mal votre réponse. Si je le suis, dites-moi ce que vous voulez dire.


Merci @sankar. Je suis assez sûr que ce n'est pas une solution Spring. Nous avons de nombreux services qui renvoient des représentations Json de classes qui ne sont pas annotées (at) JsonSerialize. En appelant ces services à partir d'un navigateur, je peux voir qu'ils renvoient tous les deux Json.


Nous avons fait d'autres recherches à ce sujet. Il semble que lorsque nous retournons nos propres classes (par exemple Guy), nous obtenons une erreur. Si, cependant, nous renvoyons par exemple une ArrayList ou une String, nous n'obtenons pas cette erreur. En outre, un ArrayList fonctionne très bien.


Aussi quelques informations supplémentaires sur l'erreur elle-même: le code javascript appelant obtient une chaîne vide comme données de retour et ce message d'erreur: "xhr.js: 173 Cross-Origin Read Blocking (CORB) a bloqué la réponse cross-origin boot.exampledomain.com:8443/get/guy/1 avec le type MIME application / json. Voir < a href = "https://www.chromestatus.com/feature/5629709824032768" rel = "nofollow noreferrer"> chromestatus.com/feature/5629709824032768 pour plus de détails. "


D'accord. Le seul indice auquel je pourrais penser est que Guy est un bean personnalisé et que les restes sont tous des types de données ou des collections intégrés. On dirait qu'ajax n'est pas capable d'interpréter le modèle comme json. En utilisant ObjectMapper ou Json Annotations comme suggéré par @sankar, vous indiquez clairement que vous avez besoin de la conversion. Je chercherai des moyens plus simples si possible. Peut-être que je manque quelque chose.


@MatsAndersson - Si cela fonctionne dans le navigateur, votre problème n'est pas le backend. Au moment où le JSON passe sur le fil, il n'y a pas de type Guy, il n'y a pas de "haricots personnalisés" et ainsi de suite. Le json est du texte brut et votre interface JS doit savoir comment travailler avec cela. Pouvez-vous ajouter des intercepteurs pour la demande et les réponses et coller la sortie sur les deux? github.com/axios/axios#interceptors


En première lecture, cela semble être un problème JavaScript, mais voici quelques choses que vous pouvez essayer du côté Spring: 1. Testez vos points de terminaison avec Postman pour obtenir plus de détails sur les en-têtes et co (et envoyez-nous les résultats), 2. Supprimer headers = {"Accept = application / json"} à partir de votre point de terminaison (Spring traite et produit JSON par défaut), 3. Essayez d'accepter id comme String au lieu d'int comme @PathVariable (" guyId ") Chaîne guyId


6 Réponses :


-1
votes

Vous devez ajouter l'annotation @ResponseBody avant votre méthode.

@ResponseBody
public Guy ....


1 commentaires

@RestController n'a pas besoin de @ResponseBody - voir cette réponse



0
votes

Comme votre javascript est sur un domaine différent du service spring-boot, vous devez configurer CORS .

Cela pourrait être fait globalement en ajoutant @CrossOrigin comme ceci:

@RestController
@CrossOrigin
public class GuyController {


3 commentaires

Ne pensez pas que ce serait le problème, car la requête qui renvoie la liste des objets personnalisés fonctionne bien.


@MadhuBhat: J'ai fait un test avec spring-boot & axiom et cela fonctionne avec 1 objet et une liste d'objets servant le JS depuis le spring-boot sans aucun problème. En utilisant différents serveurs Web, j'ai reproduit une exception CORB avec 1 objet qui n'est pas présent avec une liste d'objets, désactivant CORS cela fonctionne dans les deux cas.


Désolé tout le monde, je suis malade depuis quelques jours. Je peux ajouter que nous avons plusieurs systèmes basés sur Spring en cours d'exécution. Ils ont des centaines de méthodes de contrôleur renvoyant à la fois nos propres classes et listes et d'autres classes. Nous appelons ces méthodes en utilisant la même méthode javascript et nous n'avons jamais eu ce problème auparavant. Cependant, c'est la première fois que nous utilisons une application de démarrage Spring avec son support Json intégré. C'est aussi la première fois que nous effectuons les appels depuis un client React qui n'est pas intégré côté serveur. Nos autres systèmes utilisent JSP pour l'interface graphique.



0
votes

Si vous utilisez spring, vous devez utiliser ResponseEntity au lieu de renvoyer directement l'objet:

@RestController
@RequestMapping(USERS)
public class UserController {

  @Autowired
  private UserService userService;

  @Autowired
  private RoleService roleService;

  @Autowired
  private LdapUserDetailsManager userDetailsService;

  @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<?> list(PagedResourcesAssembler<User> pageAssembler, @PageableDefault(size = 20) Pageable pageable, UserDTO condition) {
    Page<User> page = userService.findAll(pageable, condition);
    PagedResources<?> resources = pageAssembler.toResource(page, new UserResourceAssembler());
    return ResponseEntity.ok(resources);
  }

  @GetMapping(value = CoreHttpPathStore.PARAM_ID, produces= MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<UserResource> get(@PathVariable("id") Long id) {
    User user = userService.get(id);
    UserResource resource = new UserResourceAssembler().toResource(user);
    return ResponseEntity.ok(resource);
  }

  private void preProcessEntity(@RequestBody UserDTO entity) {
    if (null != entity.getPassword()) {
      userDetailsService.changePassword(entity.getOldPassword(), entity.getPassword());
    }
  }

  @PostMapping
  @ResponseStatus(HttpStatus.CREATED)
  Long create(@RequestBody User user) {
    userService.insert(user);
    return user.getId();
  }

  @PutMapping(CoreHttpPathStore.PARAM_ID)
  @ResponseStatus(HttpStatus.NO_CONTENT)
  void modify(@PathVariable("id") Long id, @RequestBody UserDTO user) {
    user.setId(id);
    preProcessEntity(user);
    userService.updateIgnore(user);
  }

  @DeleteMapping(CoreHttpPathStore.PARAM_ID)
  @ResponseStatus(HttpStatus.NO_CONTENT)
  void delete(@PathVariable("id") Long id) {
    userService.delete(id);
  }

  @DeleteMapping
  @ResponseStatus(HttpStatus.NO_CONTENT)
  void bulkDelete(@RequestBody Long[] ids) {
    userService.delete(ids);
  }
}

Voici comment j'écris mes contrôleurs:

XXX


1 commentaires

L'utilisation de ResponseEntity n'est pas du tout une nécessité.



1
votes

Pourriez-vous s'il vous plaît essayer de changer l'en-tête pour accepter dans le javascript

return dispatch => {
        dispatch(fetchMenuStart());
        const url = 'https://boot.ourcompany.com:8443/get/guy/1'; 
        const headers = {
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            }
        }
        axios.get(url, headers)
            .then(res => {
                console.log(res); 
                dispatch(fetchMenuSuccess(res.data.categories, res.data.restaurant));
            })
            .catch(error => {   
                console.log("error", error);
                const errorMsg = 'There was an error fetching the menu';
                dispatch(fetchMenuFail(errorMsg)); 
            });
    };


1 commentaires

Oui ... il manque l'en-tête 'Accept' dans le client ainsi que le type de retour sur le serveur (produit = MediaType.APPLICATION_JSON_VALUE)



0
votes

OK tout le monde, merci beaucoup pour vos efforts. Il s'avère que la solution proposée par @mpromonet (ajout de l'annotation CrossOrigin sur le contrôleur) résout ce problème. Je suis toujours très curieux de savoir pourquoi une méthode renvoyant List fonctionne et une méthode renvoyant un Guy ne fonctionne pas s'il s'agit d'un problème d'origine croisée. Cela ne semble pas logique et cela rend le problème beaucoup plus difficile à résoudre.


0 commentaires

1
votes

J'ai finalement résolu ce problème en désactivant CORS, avec la classe suivante:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Profile("devel")
@Configuration
public class WebConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**");
            }
        };
    }

}

J'ai également ajouté l'annotation @Profile pour désactiver CORS uniquement lors du développement heure.

Au fait, la raison du problème semble être expliquée dans:

https://chromium.googlesource.com/chromium/src/+/master/services/network/cross_origin_read_blocking_explainer.md -JSON

Lors du retour d'un objet, il est interprété comme un objet JSON non vide (tel que {"key": "value"} ). Lors du retour d'une liste, le même texte est placé entre crochets et il passe la protection .


1 commentaires

Merci pour votre contribution, Benjamin Valero