Spring Boot не может вернуть JSON для объекта, но не для списка объектов

Я разрабатываю свое первое приложение Spring Boot и столкнулся со странной проблемой. Конфигурация очень проста:

    <?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>

У нас есть код Javascript на странице, вызывающей эти две службы. Когда контроллер возвращает объект Guy в первом методе, мы получаем пустой ответ:

    {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

Однако когда мы возвращаем объекты List of Guy из второго метода, мы получаем полную структуру Json.

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

Контроллер выглядит так:

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;
    }

}

Как ни странно, если я вызываю эти две службы из браузера, я получаю правильную структуру Json для обоих вызовов.

Когда я запускаю дерево зависимостей mvn, появляются ожидаемые зависимости Джексона, которые поставляются с базовым загрузочным проектом.

Вот как выглядит код JavaScript:

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.info(res); 
                dispatch(fetchMenuSuccess(res.data.categories, res.data.restaurant));
            })
            .catch(error => {   
                console.info("error", error);
                const errorMsg = 'There was an error fetching the menu';
                dispatch(fetchMenuFail(errorMsg)); 
            });
    };

Может ли кто-нибудь предложить, что может быть причиной этого, или шаги для проверки, чтобы выяснить проблему?

Новый пример кода JavaScript:

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.info(res); 
    })
    .catch(error => {   
        console.info("error", error);
        const errorMsg = 'There was an error fetching the menu';
    });

Вы установили тип содержимого как application/json в вызове ajax?

Fullstack Guy 18.02.2019 17:13

Если вы получаете правильный ответ от браузера, но неправильный от кода js, очевидно, что есть проблема с кодом js. Не могли бы вы добавить фрагмент кода js, который вы используете?

Madhu Bhat 18.02.2019 17:15

С каким URL/путем вы пытаетесь получить доступ к «/get/guy»? ... Я ожидаю, что /get/guy/1, /get/guy/2 и /get/guy/3 будут работать.. (Вы заметили «переменную пути» {guyId}?)

xerx593 18.02.2019 17:18

пожалуйста, предоставьте код JavaScript, откуда вы звоните в службу

kamlesh pandey 18.02.2019 17:37

@ xerx593 - это просто упрощенный пример кода, иллюстрирующий проблему

Mats Andersson 18.02.2019 19:24

..может показать нам "некоторый код Javascript".

xerx593 18.02.2019 19:36

Я добавил код JavaScript

Mats Andersson 18.02.2019 20:29

@MatsAndersson, вы исправили эту ошибку?

Michał Ziober 20.02.2019 22:50

@MichałZiober нет, нет. Мы ждали помощи здесь

Mats Andersson 22.02.2019 09:37

@MatsAndersson, так как вы получаете правильный ответ при вызове через браузер. Проблема в вашем внешнем коде. Можете ли вы добавить полный код интерфейса? Кроме того, добавьте код и для другого запроса.

Maruthi Adithya 25.02.2019 11:32

@MatsAndersson, я настроил приложение Spring Boot, как в вашем примере, и оно у меня работает. Должно быть проблема на стороне клиента. Вы используете библиотеку аксиомы для загрузки данных с сервера. Почему вы используете URL с доменом? Нельзя просто использовать const url = '/get/guy/1'; Вы загружаете данные с другого домена? Есть ли у вас какая-либо пользовательская глобальная конфигурация для axios? Кроме того, серверный ответ с 200 означает, что серверная сторона вернула пустую строку в качестве успешного результата.

Michał Ziober 25.02.2019 23:46

@MichałZiober. Да, клиент находится в домене, отличном от служб, которые находятся в загрузочном приложении Spring, которое предоставляет наши микросервисы. Одна и та же конфигурация axios используется для всех сервисных вызовов. Все работают, кроме этого конкретного. Если я вызову эту службу из веб-браузера, я ясно увижу, что ответ не пустой. Кроме того, если я вызову этот URL-адрес из Postman, я получу успех и ответ, содержащий правильную структуру данных.

Mats Andersson 26.02.2019 15:19

добавлен пример кода javascript

Mats Andersson 26.02.2019 15:20

Вы сказали Spring преобразовать тело строки ответа в JSON? Вам либо нужно добавить produces = "application/json" в качестве параметра аннотации @RequestMapping ИЛИ вы можете использовать аннотацию @ResponseBody на уровне контроллера.

Agam 26.02.2019 20:11

Используйте @JsonSerialize для класса Guy и добавьте @Produces(MediaType.APPLICATION_JSON) в свой метод.

sankar 28.02.2019 02:57

Можете ли вы попробовать использовать ObjectMapper для сериализации модели, прежде чем возвращать ответ в свой класс контроллера?

Ramachandra A Pai 28.02.2019 09:34

Вы не указываете тип возврата.. @RequestMapping(value = "/get/guy/{guyId}", method=RequestMethod.GET, produces=MediaType.APPLICATION_JSON_VALUE )

Edward J Beckett 01.03.2019 00:30

Спасибо @agam. Я попробовал то, что вы предложили. Я пробовал эти, и не было никаких изменений. Пожалуйста, поймите, что я могу вызывать обе эти службы из браузера, и они обе возвращают Json.

Mats Andersson 01.03.2019 11:01

Спасибо @ЭддиБ. Я попробовал это, и мы получили точно такие же результаты. Пожалуйста, поймите, что я могу вызывать обе эти службы из браузера, и они обе возвращают Json.

Mats Andersson 01.03.2019 11:02

Спасибо @RamachandraAPai. У нас есть несколько проектов Spring, которые автоматически создают Json, поэтому переход к решению, в котором каждый метод контроллера должен использовать ObjectMapper для ручного создания Json, не кажется привлекательным изменением. Надеюсь, я не ошибаюсь в вашем ответе. Если я, пожалуйста, дайте мне знать, что вы имеете в виду.

Mats Andersson 01.03.2019 11:06

Спасибо @sankar. Я совершенно уверен, что это не-Spring-решение. У нас есть множество сервисов, которые возвращают Json-представления классов без аннотации (at)JsonSerialize. Вызывая эти службы из браузера, я вижу, что они оба возвращают Json.

Mats Andersson 01.03.2019 11:13

Мы провели еще несколько исследований по этому поводу. Похоже, что когда мы возвращаем собственные классы (например, Guy), мы получаем ошибку. Однако если мы вернем, например, ArrayList или String, мы не получим эту ошибку. Кроме того, ArrayList<Guy> отлично работает.

Mats Andersson 01.03.2019 15:13

Также немного дополнительной информации о самой ошибке: вызывающий код javascript получает пустую строку в качестве возвращаемых данных и это сообщение об ошибке: «xhr.js: 173 Блокировка чтения из разных источников (CORB) заблокировала ответ из разных источников boot.exampledomain.com:8443/get/guy/1 с приложением типа MIME / json. Дополнительные сведения см. в статье chromestatus.com/feature/5629709824032768».

Mats Andersson 01.03.2019 15:14

Согласованный. Единственный намек, который я мог придумать, заключается в том, что Guy - это пользовательский компонент, а все остальное - встроенные типы данных или коллекции. Похоже, ajax не может интерпретировать модель как json. Используя аннотации ObjectMapper или Json, предложенные @sankar, вы четко указываете, что вам нужно преобразование. По возможности буду искать более простые способы. Может быть, я что-то упускаю.

Ramachandra A Pai 01.03.2019 15:21

@MatsAndersson - Если это работает в браузере, ваша проблема не в бэкэнде. В тот момент, когда JSON передается по сети, нет типа Guy, нет «пользовательских компонентов» и так далее. JSON — это обычный текст, и ваш JS-интерфейс должен знать, как с ним работать. Можете ли вы добавить перехватчики для запроса и ответов и вставить вывод в оба? github.com/axios/axios#interceptors

hovanessyan 03.03.2019 08:39

На первый взгляд кажется, что это проблема JavaScript, но вот некоторые вещи, которые вы можете попробовать на стороне Spring: 1. Протестируйте свои конечные точки с помощью Postman, чтобы получить более подробную информацию о заголовках и сопутствующей информации (и отправьте нам результаты), 2. Удалить заголовки = {"Принять=приложение/json"} из вашей конечной точки (Spring обрабатывает и создает JSON по умолчанию), 3. Попробуйте принять id как строку вместо int, как @PathVariable("guyId") StringguyId

LazR 04.03.2019 10:26
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
6
26
3 961
6

Ответы 6

Вы должны добавить аннотацию @ResponseBody перед вашим методом.

@ResponseBody
public Guy ....
@RestController не нужно @ResponseBody - см. этот ответ
kukkuz 02.03.2019 07:33

Поскольку ваш javascript находится в домене, отличном от службы весенней загрузки, вам необходимо настроить КОРС.

Это можно сделать глобально, добавив @CrossOrigin вот так:

@RestController
@CrossOrigin
public class GuyController {

Не думаю, что это будет проблемой, так как запрос, который возвращает список пользовательских объектов, работает нормально.

Madhu Bhat 03.03.2019 15:21

@MadhuBhat: я провел тест с spring-boot и axiom, и он без проблем работает с 1 объектом и списком объектов, обслуживающих JS из spring-boot. Используя разные веб-серверы, я воспроизвел исключение CORB с 1 объектом, которого нет в списке объектов, отключив CORS, он работает в обоих случаях.

mpromonet 03.03.2019 17:23

Извините все, я был болен несколько дней. Я могу добавить, что у нас работает несколько систем на основе Spring. У них есть сотни методов контроллера, возвращающих как наши собственные классы, так и списки и другие классы. Мы вызываем эти методы, используя тот же самый способ javascript, и у нас никогда не было этой проблемы раньше. Однако это наш первый раз, когда мы используем загрузочное приложение Spring со встроенной поддержкой Json. Это также первый раз, когда мы делаем вызовы из клиента React, который не интегрирован с серверной частью. Другие наши системы используют JSP для графического интерфейса.

Mats Andersson 05.03.2019 10:07

Если вы используете spring, вы должны использовать ResponseEntity вместо прямого возврата объекта:

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

Вот как я пишу свои контроллеры:

@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);
  }
}

Использование ResponseEntity вовсе не обязательно.

Madhu Bhat 03.03.2019 15:21

Не могли бы вы попробовать изменить заголовок, чтобы принять в 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.info(res); 
                dispatch(fetchMenuSuccess(res.data.categories, res.data.restaurant));
            })
            .catch(error => {   
                console.info("error", error);
                const errorMsg = 'There was an error fetching the menu';
                dispatch(fetchMenuFail(errorMsg)); 
            });
    };

Да... у него отсутствует заголовок "Accept" в клиенте, а также тип возврата на сервере (производит = MediaType.APPLICATION_JSON_VALUE)

Edward J Beckett 30.04.2019 17:04

Хорошо всем, большое спасибо за ваши усилия. Оказывается, решение, предложенное @mpromonet (добавление аннотации CrossOrigin на контроллер), решает эту проблему. Мне все еще очень любопытно узнать, почему метод, возвращающий List, работает, а метод, возвращающий Guy, не работает, если это проблема с перекрестным происхождением. Это не кажется логичным и значительно усложняет решение проблемы.

Наконец-то я решил эту проблему, отключив CORS со следующим классом:

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("/**");
            }
        };
    }

}

Я также добавил аннотацию @Profile, чтобы отключить CORS только во время разработки.

Кстати, причина проблемы, кажется, объясняется в:

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

При возврате объекта он интерпретируется как непустой объект JSON (например, {"key": "value"}). При возврате списка тот же текст заключен в квадратные скобки и имеет вид проходит защиту.

Спасибо за ваш вклад, Бенджамин Валеро

Mats Andersson 22.03.2019 09:48

Другие вопросы по теме