【问题标题】:Return different HTTP status code from spring boot custom validators从 Spring Boot 自定义验证器返回不同的 HTTP 状态码
【发布时间】:2020-10-25 15:52:37
【问题描述】:

我正在使用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,因为我有一个总是返回 400 的 controllerAdvice。

我想在这里实现的是,是否有可能返回带有状态代码的 customException,或者是否有可能从验证器返回不同的状态代码?我在 StackOverflow 中看到了类似的帖子,但没有答案。我还查看了其他帖子,但我发现它没有用。

【问题讨论】:

  • 我试过了......但没有用......让我更新问题给你
  • @dotore 我已经更新了这个问题。如果我返回自定义异常,那么我曾经收到上述错误消息。似乎 spring-validator 吞下了自定义异常并抛出 ValidationException

标签: java spring spring-boot spring-validator


【解决方案1】:

当前行为

  • 当验证器代码抛出异常(不扩展 ConstraintDeclarationException)而不是返回 false 时,javax.validation 将异常包装在 ValidationException 中。这是验证器框架的行为,而不是 Spring 框架的问题。

  • 当验证器代码抛出扩展 ConstraintDeclarationException 的异常而不是返回 false 时,javax.validation 框架会传播它。

  • 如果验证器返回 false 而不是抛出异常,Spring 会将所有验证错误转换为 global errorsfield 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) {
     ...
    }

参考代码

【讨论】:

    【解决方案2】:

    在模型包下为Error创建一个类

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

    在异常包下创建自定义验证异常类

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

    【讨论】:

      【解决方案3】:

      在完全理解问题后,以下内容将为您提供所需的内容。您可以根据需要在层次结构中添加任意数量的异常作为 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());
      }
      

      希望这会有所帮助!

      【讨论】:

      • 我知道如何从 Controller 返回自定义状态代码或异常,但 spring-validator 不提供您抛出自定义异常。他们过去常常捕获我们的自定义异常并默认抛出validationException。在客户验证器中,您可以返回 false 或抛出 ValidationException,没有其他方法可以返回带有正确消息和状态代码的自定义异常。
      • 我的错,第一次尝试时错过了验证器部分。我已经用图片中的验证器更新了答案。基本上,现在您必须根据您的业务场景使用以 ValidationException 作为根的异常层次结构和自定义错误代码。让我知道这是否有帮助。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-08-09
      • 2019-01-22
      • 1970-01-01
      • 1970-01-01
      • 2017-03-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多