Я разрабатываю свое первое приложение 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';
});
Если вы получаете правильный ответ от браузера, но неправильный от кода js, очевидно, что есть проблема с кодом js. Не могли бы вы добавить фрагмент кода js, который вы используете?
С каким URL/путем вы пытаетесь получить доступ к «/get/guy»? ... Я ожидаю, что /get/guy/1, /get/guy/2 и /get/guy/3 будут работать.. (Вы заметили «переменную пути» {guyId}?)
пожалуйста, предоставьте код JavaScript, откуда вы звоните в службу
@ xerx593 - это просто упрощенный пример кода, иллюстрирующий проблему
..может показать нам "некоторый код Javascript".
Я добавил код JavaScript
@MatsAndersson, вы исправили эту ошибку?
@MichałZiober нет, нет. Мы ждали помощи здесь
@MatsAndersson, так как вы получаете правильный ответ при вызове через браузер. Проблема в вашем внешнем коде. Можете ли вы добавить полный код интерфейса? Кроме того, добавьте код и для другого запроса.
@MatsAndersson, я настроил приложение Spring Boot, как в вашем примере, и оно у меня работает. Должно быть проблема на стороне клиента. Вы используете библиотеку аксиомы для загрузки данных с сервера. Почему вы используете URL с доменом? Нельзя просто использовать const url = '/get/guy/1'; Вы загружаете данные с другого домена? Есть ли у вас какая-либо пользовательская глобальная конфигурация для axios? Кроме того, серверный ответ с 200 означает, что серверная сторона вернула пустую строку в качестве успешного результата.
@MichałZiober. Да, клиент находится в домене, отличном от служб, которые находятся в загрузочном приложении Spring, которое предоставляет наши микросервисы. Одна и та же конфигурация axios используется для всех сервисных вызовов. Все работают, кроме этого конкретного. Если я вызову эту службу из веб-браузера, я ясно увижу, что ответ не пустой. Кроме того, если я вызову этот URL-адрес из Postman, я получу успех и ответ, содержащий правильную структуру данных.
добавлен пример кода javascript
Вы сказали Spring преобразовать тело строки ответа в JSON? Вам либо нужно добавить produces = "application/json" в качестве параметра аннотации @RequestMapping ИЛИ вы можете использовать аннотацию @ResponseBody на уровне контроллера.
Используйте @JsonSerialize для класса Guy и добавьте @Produces(MediaType.APPLICATION_JSON) в свой метод.
Можете ли вы попробовать использовать ObjectMapper для сериализации модели, прежде чем возвращать ответ в свой класс контроллера?
Вы не указываете тип возврата.. @RequestMapping(value = "/get/guy/{guyId}", method=RequestMethod.GET, produces=MediaType.APPLICATION_JSON_VALUE )
Спасибо @agam. Я попробовал то, что вы предложили. Я пробовал эти, и не было никаких изменений. Пожалуйста, поймите, что я могу вызывать обе эти службы из браузера, и они обе возвращают Json.
Спасибо @ЭддиБ. Я попробовал это, и мы получили точно такие же результаты. Пожалуйста, поймите, что я могу вызывать обе эти службы из браузера, и они обе возвращают Json.
Спасибо @RamachandraAPai. У нас есть несколько проектов Spring, которые автоматически создают Json, поэтому переход к решению, в котором каждый метод контроллера должен использовать ObjectMapper для ручного создания Json, не кажется привлекательным изменением. Надеюсь, я не ошибаюсь в вашем ответе. Если я, пожалуйста, дайте мне знать, что вы имеете в виду.
Спасибо @sankar. Я совершенно уверен, что это не-Spring-решение. У нас есть множество сервисов, которые возвращают Json-представления классов без аннотации (at)JsonSerialize. Вызывая эти службы из браузера, я вижу, что они оба возвращают Json.
Мы провели еще несколько исследований по этому поводу. Похоже, что когда мы возвращаем собственные классы (например, Guy), мы получаем ошибку. Однако если мы вернем, например, ArrayList или String, мы не получим эту ошибку. Кроме того, ArrayList<Guy> отлично работает.
Также немного дополнительной информации о самой ошибке: вызывающий код javascript получает пустую строку в качестве возвращаемых данных и это сообщение об ошибке: «xhr.js: 173 Блокировка чтения из разных источников (CORB) заблокировала ответ из разных источников boot.exampledomain.com:8443/get/guy/1 с приложением типа MIME / json. Дополнительные сведения см. в статье chromestatus.com/feature/5629709824032768».
Согласованный. Единственный намек, который я мог придумать, заключается в том, что Guy - это пользовательский компонент, а все остальное - встроенные типы данных или коллекции. Похоже, ajax не может интерпретировать модель как json. Используя аннотации ObjectMapper или Json, предложенные @sankar, вы четко указываете, что вам нужно преобразование. По возможности буду искать более простые способы. Может быть, я что-то упускаю.
@MatsAndersson - Если это работает в браузере, ваша проблема не в бэкэнде. В тот момент, когда JSON передается по сети, нет типа Guy, нет «пользовательских компонентов» и так далее. JSON — это обычный текст, и ваш JS-интерфейс должен знать, как с ним работать. Можете ли вы добавить перехватчики для запроса и ответов и вставить вывод в оба? github.com/axios/axios#interceptors
На первый взгляд кажется, что это проблема JavaScript, но вот некоторые вещи, которые вы можете попробовать на стороне Spring: 1. Протестируйте свои конечные точки с помощью Postman, чтобы получить более подробную информацию о заголовках и сопутствующей информации (и отправьте нам результаты), 2. Удалить заголовки = {"Принять=приложение/json"} из вашей конечной точки (Spring обрабатывает и создает JSON по умолчанию), 3. Попробуйте принять id как строку вместо int, как @PathVariable("guyId") StringguyId



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Вы должны добавить аннотацию @ResponseBody перед вашим методом.
@ResponseBody
public Guy ....
Поскольку ваш javascript находится в домене, отличном от службы весенней загрузки, вам необходимо настроить КОРС.
Это можно сделать глобально, добавив @CrossOrigin вот так:
@RestController
@CrossOrigin
public class GuyController {
Не думаю, что это будет проблемой, так как запрос, который возвращает список пользовательских объектов, работает нормально.
@MadhuBhat: я провел тест с spring-boot и axiom, и он без проблем работает с 1 объектом и списком объектов, обслуживающих JS из spring-boot. Используя разные веб-серверы, я воспроизвел исключение CORB с 1 объектом, которого нет в списке объектов, отключив CORS, он работает в обоих случаях.
Извините все, я был болен несколько дней. Я могу добавить, что у нас работает несколько систем на основе Spring. У них есть сотни методов контроллера, возвращающих как наши собственные классы, так и списки и другие классы. Мы вызываем эти методы, используя тот же самый способ javascript, и у нас никогда не было этой проблемы раньше. Однако это наш первый раз, когда мы используем загрузочное приложение Spring со встроенной поддержкой Json. Это также первый раз, когда мы делаем вызовы из клиента React, который не интегрирован с серверной частью. Другие наши системы используют JSP для графического интерфейса.
Если вы используете 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 вовсе не обязательно.
Не могли бы вы попробовать изменить заголовок, чтобы принять в 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)
Хорошо всем, большое спасибо за ваши усилия. Оказывается, решение, предложенное @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 только во время разработки.
Кстати, причина проблемы, кажется, объясняется в:
При возврате объекта он интерпретируется как непустой объект JSON (например, {"key": "value"}). При возврате списка тот же текст заключен в квадратные скобки и имеет вид проходит защиту.
Спасибо за ваш вклад, Бенджамин Валеро
Вы установили тип содержимого как application/json в вызове ajax?