Validate Your Endpoints Using Spring

What Do We Want To Do? 🤔

Apply validation to the objects we receive at the endpoint controllers from clients.

What Is The First Solution That Comes To Mind? 🧠

it’s simple, right? write functions that validate those objects.

Say we have an endpoint to add new Shipments.

Let’s start with the DTO classes (Data Transfer Object)

<span>@RequiredArgsConstructor</span> <span>@Getter</span>
<span>public</span> <span>class</span> <span>ShipmentComponentDTO</span> <span>{</span>
<span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span>
<span>}</span>
<span>@RequiredArgsConstructor</span> <span>@Getter</span>  
<span>public</span> <span>class</span> <span>ShipmentComponentDTO</span> <span>{</span>  
    <span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span>  
<span>}</span>
@RequiredArgsConstructor @Getter public class ShipmentComponentDTO { private final String productCode; }

Enter fullscreen mode Exit fullscreen mode

<span>@RequiredArgsConstructor</span> <span>@Getter</span>
<span>public</span> <span>class</span> <span>ShipmentDTO</span> <span>{</span>
<span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span>
<span>private</span> <span>final</span> <span>int</span> <span>count</span><span>;</span>
<span>private</span> <span>final</span> <span>List</span><span><</span><span>ShipmentComponentDTO</span><span>></span> <span>shipmentComponentDTOs</span><span>;</span>
<span>}</span>
<span>@RequiredArgsConstructor</span> <span>@Getter</span>  
<span>public</span> <span>class</span> <span>ShipmentDTO</span> <span>{</span>  
    <span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span>  
    <span>private</span> <span>final</span> <span>int</span> <span>count</span><span>;</span>  
    <span>private</span> <span>final</span> <span>List</span><span><</span><span>ShipmentComponentDTO</span><span>></span> <span>shipmentComponentDTOs</span><span>;</span>  
<span>}</span>
@RequiredArgsConstructor @Getter public class ShipmentDTO { private final String productCode; private final int count; private final List<ShipmentComponentDTO> shipmentComponentDTOs; }

Enter fullscreen mode Exit fullscreen mode

Now, the controller

<span>@RestController</span>
<span>public</span> <span>class</span> <span>ShipmentController</span> <span>{</span>
<span>@PostMapping</span><span>(</span><span>"/shipment/add"</span><span>)</span>
<span>public</span> <span>ResponseEntity</span><span><</span><span>String</span><span>></span> <span>addShipment</span><span>(</span><span>@RequestBody</span> <span>ShipmentDTO</span> <span>shipmentDTO</span><span>){</span>
<span>if</span> <span>(</span><span>shipmentDTO</span><span>.</span><span>getProductCode</span><span>()</span> <span>==</span> <span>null</span><span>)</span>
<span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>"Product code can't be null"</span><span>);</span>
<span>if</span> <span>(</span><span>shipmentDTO</span><span>.</span><span>getCount</span><span>()</span> <span><=</span> <span>0</span><span>)</span>
<span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>"Count can't be negative or zero"</span><span>);</span>
<span>if</span> <span>(!</span><span>allComponentsAreValid</span><span>(</span><span>shipmentDTO</span><span>))</span>
<span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>"Component product code can't be null"</span><span>);</span>
<span>return</span> <span>ResponseEntity</span><span>.</span><span>ok</span><span>(</span><span>":)"</span><span>);</span>
<span>}</span>
<span>private</span> <span>boolean</span> <span>allComponentsAreValid</span><span>(</span><span>ShipmentDTO</span> <span>shipmentDTO</span><span>)</span> <span>{</span>
<span>List</span><span><</span><span>ShipmentComponentDTO</span><span>></span> <span>shipmentComponentDTOs</span> <span>=</span> <span>shipmentDTO</span><span>.</span><span>getShipmentComponentDTOs</span><span>();</span>
<span>return</span> <span>shipmentComponentDTOs</span> <span>!=</span> <span>null</span> <span>&&</span>
<span>shipmentComponentDTOs</span><span>.</span><span>size</span><span>()</span> <span>></span> <span>0</span> <span>&&</span>
<span>shipmentComponentDTOs</span><span>.</span>
<span>stream</span><span>().</span>
<span>allMatch</span><span>(</span><span>shipmentComponentDTO</span> <span>-></span> <span>shipmentComponentDTO</span><span>.</span><span>getProductCode</span><span>()</span> <span>!=</span> <span>null</span><span>);</span>
<span>}</span>
<span>}</span>
<span>@RestController</span>  
<span>public</span> <span>class</span> <span>ShipmentController</span> <span>{</span>  
    <span>@PostMapping</span><span>(</span><span>"/shipment/add"</span><span>)</span>  
    <span>public</span> <span>ResponseEntity</span><span><</span><span>String</span><span>></span> <span>addShipment</span><span>(</span><span>@RequestBody</span> <span>ShipmentDTO</span> <span>shipmentDTO</span><span>){</span>  
        <span>if</span> <span>(</span><span>shipmentDTO</span><span>.</span><span>getProductCode</span><span>()</span> <span>==</span> <span>null</span><span>)</span>  
            <span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>"Product code can't be null"</span><span>);</span>  

        <span>if</span> <span>(</span><span>shipmentDTO</span><span>.</span><span>getCount</span><span>()</span> <span><=</span> <span>0</span><span>)</span>  
            <span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>"Count can't be negative or zero"</span><span>);</span>  

        <span>if</span> <span>(!</span><span>allComponentsAreValid</span><span>(</span><span>shipmentDTO</span><span>))</span>  
            <span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>"Component product code can't be null"</span><span>);</span>  

        <span>return</span> <span>ResponseEntity</span><span>.</span><span>ok</span><span>(</span><span>":)"</span><span>);</span>  
    <span>}</span>  

    <span>private</span> <span>boolean</span> <span>allComponentsAreValid</span><span>(</span><span>ShipmentDTO</span> <span>shipmentDTO</span><span>)</span> <span>{</span>  
        <span>List</span><span><</span><span>ShipmentComponentDTO</span><span>></span> <span>shipmentComponentDTOs</span> <span>=</span> <span>shipmentDTO</span><span>.</span><span>getShipmentComponentDTOs</span><span>();</span>  

        <span>return</span> <span>shipmentComponentDTOs</span> <span>!=</span> <span>null</span> <span>&&</span>  
                <span>shipmentComponentDTOs</span><span>.</span><span>size</span><span>()</span> <span>></span> <span>0</span> <span>&&</span>  
                <span>shipmentComponentDTOs</span><span>.</span>  
                <span>stream</span><span>().</span>  
                <span>allMatch</span><span>(</span><span>shipmentComponentDTO</span> <span>-></span> <span>shipmentComponentDTO</span><span>.</span><span>getProductCode</span><span>()</span> <span>!=</span> <span>null</span><span>);</span>  
    <span>}</span>  
<span>}</span>
@RestController public class ShipmentController { @PostMapping("/shipment/add") public ResponseEntity<String> addShipment(@RequestBody ShipmentDTO shipmentDTO){ if (shipmentDTO.getProductCode() == null) return ResponseEntity.badRequest().body("Product code can't be null"); if (shipmentDTO.getCount() <= 0) return ResponseEntity.badRequest().body("Count can't be negative or zero"); if (!allComponentsAreValid(shipmentDTO)) return ResponseEntity.badRequest().body("Component product code can't be null"); return ResponseEntity.ok(":)"); } private boolean allComponentsAreValid(ShipmentDTO shipmentDTO) { List<ShipmentComponentDTO> shipmentComponentDTOs = shipmentDTO.getShipmentComponentDTOs(); return shipmentComponentDTOs != null && shipmentComponentDTOs.size() > 0 && shipmentComponentDTOs. stream(). allMatch(shipmentComponentDTO -> shipmentComponentDTO.getProductCode() != null); } }

Enter fullscreen mode Exit fullscreen mode

Notice all the manual tedious work we had to do.🤕

Can’t anyone help us make this task more fun?

Please meet the Javax validator.🥳

  • It provides a set of annotations that are used with class fields.
  • They act as constraints.
    • this field should not be null
    • this list should have a minimum size of 5
    • and many more…

How to do it?

For the object we want to validate

  • We annotate the fields we want to validate with annotations from javax.validation.constraints
  • Then, we annotate the parameter of the controller where we want to validate.

  • We then have to catch the error thrown by the validator, and return a proper response to the user.

Let’s code it 🦾

Here are the DTO objects

<span>@RequiredArgsConstructor</span> <span>@Getter</span>
<span>public</span> <span>class</span> <span>ShipmentComponentDTO</span> <span>{</span>
<span>@NotBlank</span><span>(</span><span>message</span> <span>=</span> <span>"Component product code can't be null"</span><span>)</span>
<span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span>
<span>// required by the javax validation code </span>
<span>public</span> <span>ShipmentComponentDTO</span><span>()</span> <span>{</span>
<span>this</span><span>.</span><span>productCode</span> <span>=</span> <span>""</span><span>;</span>
<span>}</span>
<span>}</span>
<span>@RequiredArgsConstructor</span> <span>@Getter</span>  
<span>public</span> <span>class</span> <span>ShipmentComponentDTO</span> <span>{</span>  
    <span>@NotBlank</span><span>(</span><span>message</span> <span>=</span> <span>"Component product code can't be null"</span><span>)</span>  
    <span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span>  
    <span>// required by the javax validation code </span>
  <span>public</span> <span>ShipmentComponentDTO</span><span>()</span> <span>{</span>  
        <span>this</span><span>.</span><span>productCode</span> <span>=</span> <span>""</span><span>;</span>  
    <span>}</span>  
<span>}</span>
@RequiredArgsConstructor @Getter public class ShipmentComponentDTO { @NotBlank(message = "Component product code can't be null") private final String productCode; // required by the javax validation code public ShipmentComponentDTO() { this.productCode = ""; } }

Enter fullscreen mode Exit fullscreen mode

<span>@RequiredArgsConstructor</span> <span>@Getter</span>
<span>public</span> <span>class</span> <span>ShipmentDTO</span> <span>{</span>
<span>@NotBlank</span><span>(</span><span>message</span> <span>=</span> <span>"Product code can't be null"</span><span>)</span>
<span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span>
<span>@Min</span><span>(</span><span>1</span><span>)</span>
<span>private</span> <span>final</span> <span>int</span> <span>count</span><span>;</span>
<span>@Size</span><span>(</span><span>min</span> <span>=</span> <span>1</span><span>,</span> <span>message</span> <span>=</span> <span>"Component product code can't be empty"</span><span>)</span>
<span>@NotNull</span><span>(</span><span>message</span> <span>=</span> <span>"Component product code can't be null"</span><span>)</span>
<span>@Valid</span>
<span>private</span> <span>final</span> <span>List</span><span><</span><span>ShipmentComponentDTO</span><span>></span> <span>shipmentComponentDTOs</span><span>;</span>
<span>}</span>
<span>@RequiredArgsConstructor</span> <span>@Getter</span>  
<span>public</span> <span>class</span> <span>ShipmentDTO</span> <span>{</span>  
    <span>@NotBlank</span><span>(</span><span>message</span> <span>=</span> <span>"Product code can't be null"</span><span>)</span>  
    <span>private</span> <span>final</span> <span>String</span> <span>productCode</span><span>;</span>  
    <span>@Min</span><span>(</span><span>1</span><span>)</span>  
    <span>private</span> <span>final</span> <span>int</span> <span>count</span><span>;</span>  

    <span>@Size</span><span>(</span><span>min</span> <span>=</span> <span>1</span><span>,</span> <span>message</span> <span>=</span> <span>"Component product code can't be empty"</span><span>)</span>  
    <span>@NotNull</span><span>(</span><span>message</span> <span>=</span> <span>"Component product code can't be null"</span><span>)</span>  
    <span>@Valid</span>  
  <span>private</span> <span>final</span> <span>List</span><span><</span><span>ShipmentComponentDTO</span><span>></span> <span>shipmentComponentDTOs</span><span>;</span>  
<span>}</span>
@RequiredArgsConstructor @Getter public class ShipmentDTO { @NotBlank(message = "Product code can't be null") private final String productCode; @Min(1) private final int count; @Size(min = 1, message = "Component product code can't be empty") @NotNull(message = "Component product code can't be null") @Valid private final List<ShipmentComponentDTO> shipmentComponentDTOs; }

Enter fullscreen mode Exit fullscreen mode

Ok, what were those annotations?

  • NotBlank
    • This field can’t be null or empty string
  • Min
    • this integer should have a min value we specify
  • Size
    • this array’s size should be within the boundaries we specify
  • NotNull
    • This field must not be null
  • Valid
    • Validate the fields in this object

Now, here is the controller

<span>@RestController</span>
<span>public</span> <span>class</span> <span>ShipmentController_Validation</span> <span>{</span>
<span>@PostMapping</span><span>(</span><span>"validation/shipment/add"</span><span>)</span>
<span>public</span> <span>ResponseEntity</span><span><</span><span>String</span><span>></span> <span>addShipment</span><span>(</span><span>@RequestBody</span> <span>@Validated</span> <span>ShipmentDTO</span> <span>shipmentDTO</span><span>){</span>
<span>return</span> <span>ResponseEntity</span><span>.</span><span>ok</span><span>(</span><span>":)"</span><span>);</span>
<span>}</span>
<span>}</span>
<span>@RestController</span>  
<span>public</span> <span>class</span> <span>ShipmentController_Validation</span> <span>{</span>  
    <span>@PostMapping</span><span>(</span><span>"validation/shipment/add"</span><span>)</span>  
    <span>public</span> <span>ResponseEntity</span><span><</span><span>String</span><span>></span> <span>addShipment</span><span>(</span><span>@RequestBody</span> <span>@Validated</span> <span>ShipmentDTO</span> <span>shipmentDTO</span><span>){</span>  
        <span>return</span> <span>ResponseEntity</span><span>.</span><span>ok</span><span>(</span><span>":)"</span><span>);</span>  
    <span>}</span>  
<span>}</span>
@RestController public class ShipmentController_Validation { @PostMapping("validation/shipment/add") public ResponseEntity<String> addShipment(@RequestBody @Validated ShipmentDTO shipmentDTO){ return ResponseEntity.ok(":)"); } }

Enter fullscreen mode Exit fullscreen mode

What was that @Validated annotation?🤨

  • it tells Spring to run the validation mechanism on this object according to its validation annotations(NotBlank, etc..)

Finally, let’s catch the exception thrown by the validator and generate a proper response

<span>@Order</span><span>(</span><span>Ordered</span><span>.</span><span>HIGHEST_PRECEDENCE</span><span>)</span>
<span>@ControllerAdvice</span>
<span>public</span> <span>class</span> <span>ValidationAdvice</span><span>{</span>
<span>@ResponseStatus</span><span>(</span><span>BAD_REQUEST</span><span>)</span>
<span>@ResponseBody</span>
<span>@ExceptionHandler</span><span>(</span><span>MethodArgumentNotValidException</span><span>.</span><span>class</span><span>)</span>
<span>public</span> <span>ResponseEntity</span><span><?></span> <span>methodArgumentNotValidException</span><span>(</span><span>MethodArgumentNotValidException</span> <span>ex</span><span>)</span> <span>{</span>
<span>BindingResult</span> <span>result</span> <span>=</span> <span>ex</span><span>.</span><span>getBindingResult</span><span>();</span>
<span>List</span><span><</span><span>FieldError</span><span>></span> <span>fieldErrors</span> <span>=</span> <span>result</span><span>.</span><span>getFieldErrors</span><span>();</span>
<span>String</span> <span>errorMessage</span> <span>=</span> <span>fieldErrors</span><span>.</span><span>get</span><span>(</span><span>0</span><span>).</span><span>getDefaultMessage</span><span>();</span>
<span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>errorMessage</span><span>);</span>
<span>}</span>
<span>}</span>
<span>@Order</span><span>(</span><span>Ordered</span><span>.</span><span>HIGHEST_PRECEDENCE</span><span>)</span>  
<span>@ControllerAdvice</span>  
<span>public</span> <span>class</span> <span>ValidationAdvice</span><span>{</span>  
    <span>@ResponseStatus</span><span>(</span><span>BAD_REQUEST</span><span>)</span>  
    <span>@ResponseBody</span>  
 <span>@ExceptionHandler</span><span>(</span><span>MethodArgumentNotValidException</span><span>.</span><span>class</span><span>)</span>  
    <span>public</span> <span>ResponseEntity</span><span><?></span> <span>methodArgumentNotValidException</span><span>(</span><span>MethodArgumentNotValidException</span> <span>ex</span><span>)</span> <span>{</span>  
        <span>BindingResult</span> <span>result</span> <span>=</span> <span>ex</span><span>.</span><span>getBindingResult</span><span>();</span>  
        <span>List</span><span><</span><span>FieldError</span><span>></span> <span>fieldErrors</span> <span>=</span> <span>result</span><span>.</span><span>getFieldErrors</span><span>();</span>  
        <span>String</span> <span>errorMessage</span> <span>=</span> <span>fieldErrors</span><span>.</span><span>get</span><span>(</span><span>0</span><span>).</span><span>getDefaultMessage</span><span>();</span>  
        <span>return</span> <span>ResponseEntity</span><span>.</span><span>badRequest</span><span>().</span><span>body</span><span>(</span><span>errorMessage</span><span>);</span>  
    <span>}</span>  
<span>}</span>
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class ValidationAdvice{ @ResponseStatus(BAD_REQUEST) @ResponseBody @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<?> methodArgumentNotValidException(MethodArgumentNotValidException ex) { BindingResult result = ex.getBindingResult(); List<FieldError> fieldErrors = result.getFieldErrors(); String errorMessage = fieldErrors.get(0).getDefaultMessage(); return ResponseEntity.badRequest().body(errorMessage); } }

Enter fullscreen mode Exit fullscreen mode

In conclusion️

  • Imagine having an API with many endpoints that we need to validate. Which method would be better?
  • The Validator.

Code on GitHub

原文链接:Validate Your Endpoints Using Spring

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
Don’t let your dreams be dreams.
不要让你的梦想只是想想而已
评论 抢沙发

请登录后发表评论

    暂无评论内容