Контроллер Spring Boot для обработки недопустимого JSON

У меня есть контроллер Spring Boot, который использует JSON:

    @PostMapping(
    value = "/shipdetails")
    public ResponseEntity acceptShip(@RequestBody Ship ship, HttpServletRequest request) {
        shipService.checkShip(ship);
        return new ResponseEntity<>(HttpStatus.OK);
    }

Существует соответствующий Ship Entity:

public class Ship implements Serializable {

@JsonProperty("Name")
private String name;
@JsonProperty("Owner")
private String owner;

public Ship(String name, String owner) {
    this.name = name;
    this.owner = owner;    }

public Ship() {
}

// Getters and Setters removed for brevity

И, наконец, сервис:

@Service
public class ShipService {

Boolean checkShip(Ship ship) {
    if (ship.getName().equals("Queen Mary")) {
         // do something
    }
//edited for brevity

Пример недопустимого JSON:

{
    "name_wrong":"test name",
    "owner":"Lloyds Shipping"
}

В настоящее время ошибка, которую я получаю в трассировке стека, если я отправляю неверный JSON (сервисный уровень): Cannot invoke "String.equals(Object)" because the return value of "com.ships.Ship.getName()" is null.

Джексону нужен конструктор без аргументов для десериализации.

При проверке в отладчике объект Ship имеет установленного владельца, но не установленного имени, поэтому не весь объект имеет значение null - только одно поле.

Я попробовал свой объект Ship без конструктора по умолчанию без аргументов, поэтому вы не могли даже передать объект с нулевым полем в службу, но затем он терпит неудачу даже с допустимым JSON.

Как и где должно обрабатываться исключение для недопустимого JSON?

Сначала проверьте возвращаемое значение на null, прежде чем пытаться его использовать?

Jim Garrison 19.08.2023 00:42

В сущности с большим количеством полей это может занять много времени. Вы можете быть более конкретным? Весь объект не является недопустимым, но ему не удается установить поле с неправильным именем.

Radika Moonesinghe 19.08.2023 00:43

Если JSON правильно сформирован, но в нем отсутствуют данные, необходимо убедиться, что присутствуют правильные данные. В противном случае вы получите исключение везде, где вы ссылаетесь на отсутствующие данные, которые могут быть где угодно в вашем коде. Вы даже не знаете заранее, какие возможные исключения могут быть выброшены. Вы должны либо проверять достоверность данных во время загрузки, либо везде, где вы используете данные (фактически, в обоих местах для надежности кода). Нет простого ярлыка. Кодить легко — писать настоящие программы сложно.

Jim Garrison 19.08.2023 00:48
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
3
73
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вместо того, чтобы рассматривать проблему как поле с неправильным названием, подумайте о ней как о двух проблемах: отсутствующем свойстве (name) и дополнительном свойстве (name_wrong).

Spring Boot по умолчанию настроен на игнорирование любых дополнительных свойств. И если вы не сообщите контроллеру, что свойство name является обязательным (например, с помощью аннотации проверки bean-компонента @NotNull в поле name), он с радостью примет нулевое значение для этого свойства.

Отсутствует необходимое свойство

NullPointerException происходит, так как вы пытаетесь вызвать equals в поле nullname.

ship.getName().equals("Queen Mary")
^^^^^^^^^^^^^^

Для этого есть несколько различных исправлений. Во-первых, вы можете добавить проверку bean-компонента в обязательные поля:

public class Ship implements Serializable {
  @NotNull
  @JsonProperty("Name")
  private String name;

  @JsonProperty("Owner")
  private String owner;

  // ...
}

В качестве альтернативы вы можете изменить метод equals, чтобы он был нулевым:

Boolean checkShip(Ship ship) {
    if (Objects.equals(ship.getName(), "Queen Mary"))) {
         // do something
    }
}

Дополнительное имущество

Есть несколько способов заставить Spring отклонить дополнительное свойство name_wrong.

Если вы хотите, чтобы приложение всегда отклоняло неизвестные свойства (если конкретный bean-компонент не отказывается от него), вы можете настроить его глобально:

application.yml

spring.jackson.deserialization.fail-on-unknown-properties: false
Ответ принят как подходящий

Необходимо добавить несколько аннотаций и проверок для проверки тела запроса json.

1. Добавьте зависимость в pom.xml для следующих аннотаций:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>3.1.1</version>
</dependency>

2. @Valid с телом запроса

    @PostMapping(value = "/shipdetails")
    public ResponseEntity acceptShip(@Valid @RequestBody Ship ship, 
                                     HttpServletRequest request)

3. @NotNull с обязательными атрибутами

    public class Ship implements Serializable {

      @NotNull(message = "The name is mandatory")
      @JsonProperty("Name")
      private String name;
      @JsonProperty("Owner")
      private String owner;

4. Дополнительная проверка на null перед методом equals

Boolean checkShip(Ship ship) {
        if (StringUtils.isNotBlank(ship.getName()) 
            && ship.getName().equals("Queen Mary")) {
            // do something
        }

Вывод с недопустимым json:

Вход

    {
     "name_wrong":"test name",
     "owner":"Lloyds Shipping"
    }

Выход: Вы получите статус запроса 400 Bad с длинной трассировкой стека ошибок, включающей что-то вроде этого

    "message": "Validation failed for object='ship'. Error count: 1",
"errors": [
    {
        "codes": [
            "NotNull.ship.name",
            "NotNull.name",
            "NotNull.java.lang.String",
            "NotNull"
        ],
        "arguments": [
            {
                "codes": [
                    "ship.name",
                    "name"
                ],
                "arguments": null,
                "defaultMessage": "name",
                "code": "name"
            }
        ],
        "defaultMessage": "The name is mandatory",
        "objectName": "ship",
        "field": "name",
        "rejectedValue": null,
        "bindingFailure": false,
        "code": "NotNull"
    }
]

Примечание: Некоторый дополнительный код для обработки исключения MethodArgumentNotValidException с использованием нашего пользовательского обработчика исключений, чтобы проверка была в правильно отформатированном ответе вместо длинной трассировки стека ошибок и управляла кодом состояния, как показано ниже:

@ControllerAdvice
public class ValidationExceptionHandler {
    /**
     * For an invalid input, spring framework will throw an 
            MethodArgumentNotValidException exception
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> notValidInput(MethodArgumentNotValidException e) {
        Map<String,String> errorMap = e.getAllErrors()
                .stream()
                .collect(Collectors.toMap(x -> ((FieldError)x).getField(), 
                 b -> b.getDefaultMessage(),(p,q) -> p, LinkedHashMap::new));
        return new ResponseEntity<>(errorMap, HttpStatus.BAD_REQUEST);
    }
}

Отформатированный вывод для недопустимого json:

{
    "name": "The name is mandatory"
}

этот ответ кажется довольно разумным, и я отмечу его как принятый. Было бы золотым стандартом иметь некоторую информацию о действительном bean-компоненте в контроллере.

Radika Moonesinghe 19.08.2023 05:14

Нужен ли ответу контроллера блок if для изменения между HttpStatus.OK и 400?

Radika Moonesinghe 19.08.2023 05:29

Я сделал несколько тестов. Таким образом, действительный bean-компонент обеспечивает соответствие bean-компоненту NotNull, но не останавливает выполнение, если другое поле имеет неправильное имя, например, владельцев вместо владельца, если только я не сделал владельца также ненулевым, но это кажется хакерским способом убедиться, что имя поля правильное.

Radika Moonesinghe 19.08.2023 05:56

@RadikaMoonesinghe В случае недопустимого ввода Spring Framework выдаст исключение MethodArgumentNotValidException с кодом состояния 400, и вы можете обработать его, создав @ExceptionHandler с помощью @ControllerAdvice.

GD07 19.08.2023 05:57

@RadikaMoonesinghe да, @Valid будет проверять только те поля, которые аннотированы, например, @NotNull или @NotBlank и так далее..

GD07 19.08.2023 06:06

Понятно. И помимо действительного bean-компонента, нормально ли просто позволить Джексону игнорировать неправильные имена полей и отображать то, что он может (по сравнению с попыткой строго соблюдать 100% правильно отформатированный json)

Radika Moonesinghe 19.08.2023 06:37

@RadikaMoonesinghe Да, верно, кроме того, я также включил код для обработчика исключений, чтобы получить ошибки проверки в правильно отформатированном ответе.

GD07 19.08.2023 06:44

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