REST控制器

时间:2016-01-11 18:03:55

标签: spring validation spring-mvc spring-boot spring-restcontroller

当我使用JSR-303(验证框架)注释时,我有以下模型:

public enum Gender {
    MALE, FEMALE
}

public class Profile {
    private Gender gender;

    @NotNull
    private String name;

    ...
}

以及以下JSON数据:

{ "gender":"INVALID_INPUT" }

在我的REST控制器中,我想处理绑定错误(gender属性的枚举值无效)和验证错误(name属性不能为空)。

以下控制器方法不起作用:

@RequestMapping(method = RequestMethod.POST)
public Profile insert(@Validated @RequestBody Profile profile, BindingResult result) {
    ...
}

在绑定或验证发生之前,这会导致com.fasterxml.jackson.databind.exc.InvalidFormatException序列化错误。

经过一番摆弄后,我想出了这个自定义代码,它可以满足我的需求:

@RequestMapping(method = RequestMethod.POST)
public Profile insert(@RequestBody Map values) throws BindException {

    Profile profile = new Profile();

    DataBinder binder = new DataBinder(profile);
    binder.bind(new MutablePropertyValues(values));

    // validator is instance of LocalValidatorFactoryBean class
    binder.setValidator(validator);
    binder.validate();

    // throws BindException if there are binding/validation
    // errors, exception is handled using @ControllerAdvice.
    binder.close(); 

    // No binding/validation errors, profile is populated 
    // with request values.

    ...
}

基本上这个代码的作用是序列化为通用映射而不是模型,然后使用自定义代码绑定到模型并检查错误。

我有以下问题:

  1. 自定义代码是这样的,还是在Spring Boot中有更标准的方法?
  2. @Validated注释如何工作?如何创建自己的自定义注释,其工作方式类似于@Validated来封装我的自定义绑定代码?

8 个答案:

答案 0 :(得分:5)

这是我在我的一个项目中使用的代码,用于在spring boot中验证REST api,这与你要求的不一样,但是相同..检查这是否有帮助

@RequestMapping(value = "/person/{id}",method = RequestMethod.PUT)
@ResponseBody
public Object updatePerson(@PathVariable Long id,@Valid Person p,BindingResult bindingResult){
    if (bindingResult.hasErrors()) {
        List<FieldError> errors = bindingResult.getFieldErrors();
        List<String> message = new ArrayList<>();
        error.setCode(-2);
        for (FieldError e : errors){
            message.add("@" + e.getField().toUpperCase() + ":" + e.getDefaultMessage());
        }
        error.setMessage("Update Failed");
        error.setCause(message.toString());
        return error;
    }
    else
    {
        Person person = personRepository.findOne(id);
        person = p;
        personRepository.save(person);
        success.setMessage("Updated Successfully");
        success.setCode(2);
        return success;
    }

<强> Success.java

public class Success {
int code;
String message;

public int getCode() {
    return code;
}

public void setCode(int code) {
    this.code = code;
}

public String getMessage() {
    return message;
}

public void setMessage(String message) {
    this.message = message;
}
}

<强> Error.java

public class Error {
int code;
String message;
String cause;

public int getCode() {
    return code;
}

public void setCode(int code) {
    this.code = code;
}

public String getMessage() {
    return message;
}

public void setMessage(String message) {
    this.message = message;
}

public String getCause() {
    return cause;
}

public void setCause(String cause) {
    this.cause = cause;
}

}

您还可以在此处查看:Spring REST Validation

答案 1 :(得分:2)

通常当Spring MVC无法读取http消息(例如请求体)时,它将抛出HttpMessageNotReadableException异常的实例。因此,如果spring无法绑定到您的模型,它应该抛出该异常。此外,如果您在方法参数中的每个待验证模型之后 NOT 定义BindingResult,则在验证错误的情况下,spring将抛出MethodArgumentNotValidException异常。有了这一切,您可以创建捕获这两个异常的ControllerAdvice并以理想的方式处理它们。

@ControllerAdvice(annotations = {RestController.class})
public class UncaughtExceptionsControllerAdvice {
    @ExceptionHandler({MethodArgumentNotValidException.class, HttpMessageNotReadableException.class})
    public ResponseEntity handleBindingErrors(Exception ex) {
        // do whatever you want with the exceptions
    }
}

答案 2 :(得分:2)

我放弃了这个;如果没有大量自定义代码,则无法使用@RequestBody来获取绑定错误。这与绑定到普通JavaBeans参数的控制器不同,因为@RequestBody使用Jackson绑定而不是Spring数据绑定器。

请参阅https://jira.spring.io/browse/SPR-6740?jql=text%20~%20%22RequestBody%20binding%22

答案 3 :(得分:2)

您无法通过@RequestBody获取BindException。不在控制器中使用此处记录的Errors方法参数:

  

错误,绑定结果,用于访问验证和数据中的错误   绑定命令对象(即@ModelAttribute参数)或   @RequestBody或@RequestPart的验证错误   论点。您必须声明一个错误或BindingResult参数   在经过验证的方法参数之后。

它指出,对于@ModelAttribute,您将获得绑定和验证错误,对于@RequestBody,您将仅获得验证错误

https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods

在这里进行了讨论:

https://github.com/spring-projects/spring-framework/issues/11406?jql=text%2520~%2520%2522RequestBody%2520binding%2522

对我来说,从用户角度来看仍然没有意义。通常,获取BindExceptions以向用户显示正确的错误消息非常重要。参数是,无论如何都应该进行客户端验证。但是,如果开发人员直接使用API​​,则情况并非如此。

并想象您的客户端验证是基于API请求的。您要根据保存的日历检查给定日期是否有效。您将日期和时间发送到后端,它只会失败。

您可以使用ExceptionHAndler响应HttpMessageNotReadableException来修改获取的异常,但是与BindException一样,我无法正常访问哪个字段引发了错误。我需要解析异常消息才能访问它。

所以我看不到任何解决方案,这很不好,因为使用@ModelAttribute可以很容易地获得绑定和验证错误。

答案 4 :(得分:0)

根据此帖子https://blog.codecentric.de/en/2017/11/dynamic-validation-spring-boot-validation/-您可以在控制器方法中添加额外的参数“错误”-例如

@RequestMapping(method = RequestMethod.POST)
public Profile insert(@Validated @RequestBody Profile profile, Errors errors) {
   ...
}

然后获得验证错误(如果有)。

答案 5 :(得分:0)

解决此问题的主要障碍之一是杰克逊数据绑定器的默认急切失败性质;一个人必须以某种方式说服继续解析,而不只是偶然遇到第一个错误。为了最终将它们转换为BindingResult条目,还必须收集这些解析错误。基本上,人们必须 catch suppress collect 解析异常,将它们转换BindingResult个条目然后将这些条目 add 添加到正确的@Controller方法BindingResult参数中。

捕获和抑制部分可以通过以下方式完成:

  • 自定义杰克逊解串器,它们将简单地委派给默认的相关解析器,但还会捕获,抑制和收集其解析异常
  • 使用AOP(aspectj版本),可以仅拦截解析异常的默认解串器,抑制并收集它们
  • 使用其他方式,例如适当的BeanDeserializerModifier,还可以捕获,抑制和收集解析异常;这可能是最简单的方法,但是需要有关此杰克逊特定的自定义支持的一些知识

收集部分可以使用ThreadLocal变量来存储所有必要的异常相关详细信息。通过{上的BindingResult拦截器可以很容易地完成对BindingResult项的转换和对AOP参数的附加 {1}}方法(任何类型的@Controller,包括Spring变体)。

有什么收获

通过这种方法,可以将数据 binding 错误(除了 validation 错误)获取到AOP参数中,方法与预期的相同他们在使用例如BindingResult。它也可以与多个级别的嵌入式对象一起使用-问题中提出的解决方案不能很好地解决这个问题。

解决方案详细信息自定义杰克逊解串器方法)

我创建了一个small project proving the solution(运行测试类),而在这里我仅突出显示主要部分:

@ModelAttribute

有了这些,您将得到/** * The logic for copying the gathered binding errors * into the @Controller method BindingResult argument. * * This is the most "complicated" part of the project. */ @Aspect @Component public class BindingErrorsHandler { @Before("@within(org.springframework.web.bind.annotation.RestController)") public void logBefore(JoinPoint joinPoint) { // copy the binding errors gathered by the custom // jackson deserializers or by other means Arrays.stream(joinPoint.getArgs()) .filter(o -> o instanceof BindingResult) .map(o -> (BindingResult) o) .forEach(errors -> { JsonParsingFeedBack.ERRORS.get().forEach((k, v) -> { errors.addError(new FieldError(errors.getObjectName(), k, v, true, null, null, null)); }); }); // errors copied, clean the ThreadLocal JsonParsingFeedBack.ERRORS.remove(); } } /** * The deserialization logic is in fact the one provided by jackson, * I only added the logic for gathering the binding errors. */ public class CustomIntegerDeserializer extends StdDeserializer<Integer> { /** * Jackson based deserialization logic. */ @Override public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { try { return wrapperInstance.deserialize(p, ctxt); } catch (InvalidFormatException ex) { gatherBindingErrors(p, ctxt); } return null; } // ... gatherBindingErrors(p, ctxt), mandatory constructors ... } /** * A simple classic @Controller used for testing the solution. */ @RestController @RequestMapping("/errormixtest") @Slf4j public class MixBindingAndValidationErrorsController { @PostMapping(consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) public Level1 post(@Valid @RequestBody Level1 level1, BindingResult errors) { // at the end I show some BindingResult logging for a @RequestBody e.g.: // {"nr11":"x","nr12":1,"level2":{"nr21":"xx","nr22":1,"level3":{"nr31":"xxx","nr32":1}}} // ... your whatever logic here ... 这样的内容:

BindingResult

其中第一行由 validation 错误(将Field error in object 'level1' on field 'nr12': rejected value [1]; codes [Min.level1.nr12,Min.nr12,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [level1.nr12,nr12]; arguments []; default message [nr12],5]; default message [must be greater than or equal to 5] Field error in object 'level1' on field 'nr11': rejected value [x]; codes []; arguments []; default message [null] Field error in object 'level1' on field 'level2.level3.nr31': rejected value [xxx]; codes []; arguments []; default message [null] Field error in object 'level1' on field 'level2.nr22': rejected value [1]; codes [Min.level1.level2.nr22,Min.level2.nr22,Min.nr22,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [level1.level2.nr22,level2.nr22]; arguments []; default message [level2.nr22],5]; default message [must be greater than or equal to 5] 设置为1的值)确定,而第二行由 binding < / em>一个(将@Min(5) private Integer nr12;设置为"x"的值)。第三行测试嵌入对象的绑定错误@JsonDeserialize(using = CustomIntegerDeserializer.class) private Integer nr11;包含level1,其中包含level2对象属性。

请注意,其他方法如何可以简单地替换自定义杰克逊解串器的用法,同时保留其余解决方案(level3AOP

答案 6 :(得分:0)

我想我应该以相反的顺序回答您的问题。

对于第二个问题, 如果字段验证期间发生错误,则@Validate批注将引发MethodArgumentNotValidException。此批注的对象包含两个方法,getBindingResult(),getAllErrors(),提供验证错误的详细信息。您可以使用AspectJ(AOP)创建自定义注释。但是这里不需要。因为您的情况可以使用SpringBoot的ExceptionHandler来解决。

现在您的第一个问题,

请浏览此链接Link 5 部分。实际上,它涵盖了Spring Boot中的整个bean验证。您的问题可以通过 5 部分解决。更好地理解Spring Boot中一般异常处理的基础知识可能会有所帮助。为此,我可以在Google上共享该主题ExceptionHandling的查询链接。请仔细阅读该链接的前几个结果。

答案 7 :(得分:0)

enter code here
 public class User {

@NotNull
@Size(min=3,max=50,message="min 2 and max 20 characters are alllowed !!")
private String name;

@Email
private String email;

@Pattern(regexp="[7-9][0-9]{9}",message="invalid mobile number")
@Size(max=10,message="digits should be 10")
private String phone;

@Override
public String toString() {
    return "User [name=" + name + ", email=" + email + ", phone=" + phone + "]";
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getEmail() {
    return email;
}

public void setEmail(String email) {
    this.email = email;
}

public String getPhone() {
    return phone;
}

public void setPhone(String phone) {
    this.phone = phone;
}


}

   Controller.java

    @Controller
    public class User_Controller {

    @RequestMapping("/")
    public String showForm(User u,Model m)
    {
    m.addAttribute("user",new User());
    m.addAttribute("title","Validation Form");
    return "register";
    }

    @PostMapping("/")
    public String register(@Valid User user,BindingResult bindingResult ,Model m)
    {
    if(bindingResult.hasErrors())
    {
        return "register";
    }
    else {
        m.addAttribute("message", "Registration successfully... ");
    return "register";
    }
    }
    }
 

   register.html
   <div class="container">
   <div class="alert alert-success" role="alert" th:text="${message}">
   </div>
   <h1 class="text-center">Validation Form </h1>
   <form action="/" th:action="@{/}" th:object="${user}" method="post">
   <div class="mb-3">
   <label for="exampleInputEmail1" class="form-label">Name</label>
   <input type="text" class="form-control" id="exampleInputEmail1" aria- 
    describedby="emailHelp" th:field="*{name}">
    <br>
    <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="alert alert- 
    danger"></p>
    </div>
    <div class="mb-3">
    <label for="exampleInputPassword1" class="form-label">Email</label>
     <input type="email" class="form-control" id="exampleInputPassword1" th:field="* 
    {email}">
    <br>
   <p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="alert alert- 
   danger"></p>
   </div>

   <div class="mb-3">
   <label for="exampleInputPassword1" class="form-label">Phone</label>
   <input type="text" class="form-control" id="exampleInputPassword1" th:field="* 
   {phone}">
    <p th:if="${#fields.hasErrors('phone')}" th:errors="*{phone}" class="alert alert- 
    danger"></p>
    <br>
    </div>

    <button type="submit" class="btn btn-primary">Submit</button>
     </form>
     </div>