У меня есть контроллер 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?
В сущности с большим количеством полей это может занять много времени. Вы можете быть более конкретным? Весь объект не является недопустимым, но ему не удается установить поле с неправильным именем.
Если JSON правильно сформирован, но в нем отсутствуют данные, необходимо убедиться, что присутствуют правильные данные. В противном случае вы получите исключение везде, где вы ссылаетесь на отсутствующие данные, которые могут быть где угодно в вашем коде. Вы даже не знаете заранее, какие возможные исключения могут быть выброшены. Вы должны либо проверять достоверность данных во время загрузки, либо везде, где вы используете данные (фактически, в обоих местах для надежности кода). Нет простого ярлыка. Кодить легко — писать настоящие программы сложно.




Вместо того, чтобы рассматривать проблему как поле с неправильным названием, подумайте о ней как о двух проблемах: отсутствующем свойстве (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-компоненте в контроллере.
Нужен ли ответу контроллера блок if для изменения между HttpStatus.OK и 400?
Я сделал несколько тестов. Таким образом, действительный bean-компонент обеспечивает соответствие bean-компоненту NotNull, но не останавливает выполнение, если другое поле имеет неправильное имя, например, владельцев вместо владельца, если только я не сделал владельца также ненулевым, но это кажется хакерским способом убедиться, что имя поля правильное.
@RadikaMoonesinghe В случае недопустимого ввода Spring Framework выдаст исключение MethodArgumentNotValidException с кодом состояния 400, и вы можете обработать его, создав @ExceptionHandler с помощью @ControllerAdvice.
@RadikaMoonesinghe да, @Valid будет проверять только те поля, которые аннотированы, например, @NotNull или @NotBlank и так далее..
Понятно. И помимо действительного bean-компонента, нормально ли просто позволить Джексону игнорировать неправильные имена полей и отображать то, что он может (по сравнению с попыткой строго соблюдать 100% правильно отформатированный json)
@RadikaMoonesinghe Да, верно, кроме того, я также включил код для обработчика исключений, чтобы получить ошибки проверки в правильно отформатированном ответе.
Сначала проверьте возвращаемое значение на null, прежде чем пытаться его использовать?