Вернуть другой код состояния HTTP из пользовательских валидаторов весенней загрузки

Я использую spring-boot version:2.0.5

Грейдл:

buildscript {
    ext {
        springBootVersion = '2.0.5.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'io.reflectoring'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 11

repositories {
    mavenCentral()
}

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-data-jpa')
    implementation('org.springframework.boot:spring-boot-starter-validation')
    implementation('org.springframework.boot:spring-boot-starter-web')
    runtimeOnly('com.h2database:h2')
    testImplementation('org.springframework.boot:spring-boot-starter-test')
    testImplementation('org.junit.jupiter:junit-jupiter-engine:5.0.1')

    // these dependencies are needed when running with Java 11, since they
    // are no longer part of the JDK
    implementation('javax.xml.bind:jaxb-api:2.3.1')
    implementation('org.javassist:javassist:3.23.1-GA')
}

test{
    useJUnitPlatform()
}

Контроллер

@RestController
class ValidateRequestBodyController {

  @PostMapping("/validateBody")
  ResponseEntity<String> validateBody(@Valid @RequestBody Input input) {
    return ResponseEntity.ok("valid");
  }

}

Класс проверки

class InputWithCustomValidator {

  @IpAddress
  private String ipAddress;
  
  // ...

}


class IpAddressValidator implements ConstraintValidator<IpAddress, String> {

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    Pattern pattern = 
      Pattern.compile("^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$");
    Matcher matcher = pattern.matcher(value);
    //step 1
    if (!matcher.matches()) {
        return 400;
      }
    //Step 2
      if (ipAddress already in DB) {
        return 409; //conflict with other IP address
      }
      //Also I need to return different exception based on diff validations

  }
}

Совет по контроллеру

@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handle(ValidationException e) {
    return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(e.getMessage());
}

    @ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(e.getMessage());
}

Если я выбрасываю customException из моего валидатора, то я получаю сообщение об ошибке ниже, даже если у меня есть соответствующий controllerAdvice для него,

{
  "code": "invalid_request"
    "description": "HV000028: Unexpected exception during isValid call."
}

Всегда я получаю 400 Bad request, так как у меня есть controllerAdvice, который всегда возвращает 400.

Здесь я хотел бы добиться того, есть ли возможность вернуть customException с кодом состояния или в любом случае вернуть другой код состояния из валидатора? Я вижу похожие сообщения в StackOverflow, но ответа не было. Я также проверил другие сообщения, но не нашел их полезными.


person VelNaga    schedule 05.07.2020    source источник
comment
Вместо того, чтобы возвращать число в своем классе проверки, вы можете возвращать определенные исключения (существующие или созданные вами самостоятельно), управляя ими в своем ExceptionHandler и возвращая нужный вам Http-код.   -  person doctore    schedule 05.07.2020
comment
Я пробовал .... но бесполезно ... позвольте мне обновить вопрос к вам   -  person VelNaga    schedule 05.07.2020
comment
@doctore Я обновил вопрос. Если я возвращаю пользовательское исключение, то я получаю вышеуказанное сообщение об ошибке. Похоже, что spring-validator проглатывает пользовательское исключение и выдает ValidationException   -  person VelNaga    schedule 05.07.2020
comment
stackoverflow.com/questions/31780505/   -  person Gundamaiah    schedule 05.07.2020


Ответы (3)


Текущее поведение

  • Когда исключение (которое не расширяет ConstraintDeclarationException) генерируется кодом валидатора вместо возврата false, javax.validation заключает исключение в ValidationException. Это поведение фреймворка валидатора, а не проблема фреймворка Spring.

  • Когда исключение, которое расширяет ConstraintDeclarationException, выдается кодом валидатора вместо возврата false, javax.validation фреймворк распространяет его.

  • Если валидатор возвращает false вместо исключения, Spring преобразует все ошибки проверки в global errors или field errors, завернет их в MethodArgumentNotValidException и выдаст его.

Проблема

  • Второй вариант имеет ошибки поля и глобальные ошибки, пользовательский код состояния можно вернуть, только проверив имя поля и код ошибки. Так что это невозможно, так как с этой аннотацией можно добавить много полей.
  • В первом варианте пользовательские исключения, которые были сгенерированы в валидаторе, завернуты в ValidationException, поэтому обработчик конкретного исключения невозможен.

Возможные решения

  • Разверните конкретное исключение, которое не расширяет ConstraintDeclarationException, и сопоставьте его.
    @ExceptionHandler(ValidationException.class)
    public ResponseEntity handle(ValidationException e) {
        Throwable cause = e.getCause();
        if (cause instanceof InvalidIpException) {
            return ResponseEntity
                    .status(HttpStatus.BAD_REQUEST)//may be different code
                    .body(cause.getMessage());
        }
        if (cause instanceof InuseIpException) {
            return ResponseEntity
                    .status(HttpStatus.BAD_REQUEST)//may be different code
                    .body(cause.getMessage());
        }
        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(e.getMessage());
    }
  • Сделайте свое конкретное исключение, чтобы расширить ConstraintDeclarationException, а затем создайте для него специальный обработчик.
    public class InvalidIpException extends 
                                    ConstraintDeclarationException {
    @ExceptionHandler(InvalidIpException.class)
    public ResponseEntity handle(InvalidIpException e) {
     ...
    }

Ссылочный код

person Kavithakaran Kanapathippillai    schedule 05.07.2020

создать класс для ошибки в пакете модели

@NoArgsConstructor
@Geeter
@Setter
public class ErrorMessage
{
    private int errorCode;
    private String errorMsg;
    private String documentation;

     public ErrorMessage(int errorCode,String errorMsg,String documentation)
       {
          this.errorCode=errorCode;
          this.errorMsg=errorMsg;
          this.documentation=documentation;
       }
}

создать класс Custom Validation Exception в пакете исключений

public class CustomValidationException extends RuntimeException
{
          public CustomValidationException(String msg)
         {
              super(msg);
         }
}

создать класс ExceptionHandler в том же пакете (исключение)

@RestControllerAdvice
public class CustomValidationExceptionHandler
{
   @ExceptionHandler
   public ResponseEntity toResponse(CustomValidationException ex)
   {
        ErrorMessage errorMessage=new 
       ErrorMessage(400,ex.getMessage,"www.stackoverflow.com");
      return new ResponseEntity<ErrorMessage>(errorMessage,HttpStatus.BAD_REQUES);
   }

}
person NafazBenzema    schedule 05.07.2020

После полного понимания вопроса, вот что даст вам то, что вам нужно. Вы можете добавить в иерархию столько исключений, сколько вам нужно, в качестве дочерних классов IpValidationException и сохранить собственный код состояния HTTP в соответствии с вашими деловыми/техническими вариантами использования.

public abstract class IpValidationException extends ValidationException {
    private HttpStatus status;
    private String message;

    public IpValidationException(HttpStatus status, String message) {
        this.status = status;
        this.message = message;
    }

    public HttpStatus getStatus() {
        return status;
    }

    public String getMessage() {
        return message;
    }
}

public class InvalidIpException extends IpValidationException {
    public InvalidIpException(HttpStatus status, String message) {
        super(HttpStatus.BAD_REQUEST, "Invalid IP address");
    }
}

public class InuseIpException extends IpValidationException {
    public InuseIpException(HttpStatus status, String message) {
        super(HttpStatus.CONFLICT, "IP address already in use");
    }
}

public class IpAddressValidator implements ConstraintValidator<IpAddress, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        Pattern pattern =
                Pattern.compile("^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$");
        Matcher matcher = pattern.matcher(value);
        //step 1
        if (!matcher.matches()) {
            throw new InvalidIpException();
        }
        //Step 2
        if (ipAddress already in DB) {
            throw new InuseIpException(); //conflict with other IP address
        }
        //Add more checks with more exception as you need.

    }
}

// This is how your exception handler should look like
@ExceptionHandler(IpValidationException.class)
public ResponseEntity<ErrorResponse> handle(IpValidationException e) {
    return ResponseEntity
            .status(e.getStatus())
            .body(e.getMessage());
}

Надеюсь это поможет!!

person Avnish    schedule 05.07.2020
comment
Я знаю, как вернуть собственный код состояния или исключение из контроллера, но spring-validator не предлагает вам создавать собственное исключение. Раньше они перехватывали наше пользовательское исключение и выдавали validationException по умолчанию. В средстве проверки клиента вы можете либо вернуть false, либо выдать ValidationException, нет другого способа вернуть пользовательское исключение с правильным сообщением и кодом состояния. - person VelNaga; 05.07.2020
comment
Плохо, пропустил часть валидатора с первой попытки. Я обновил ответ с помощью валидатора на картинке. По сути, теперь вам придется использовать иерархию исключений с ValidationException как root с пользовательскими кодами ошибок в соответствии с вашим бизнес-сценарием. Позвольте мне знать, если это помогает. - person Avnish; 05.07.2020