Traditional parameter verification

I’m sure you have a lot of headaches in the process of development, because sometimes the interface needs a lot of parameters, in ancient times we should check the way:

@PostMapping("/order/submit") public void submit(@RequestBody OrderRequest order){ if(order.getParam1() == null){ throw New XXXException(" XXX cannot be empty "); } else if(order.getparam2 () == null){throw new XXXException(" XXX cannot be null "); }... }Copy the code

Dude, if an interface has dozens of parameters, I think it can kill people… If the request parameters are not verified, then the problem is more serious, and in the front end of the joint adjustment may appear a variety of parameters do not match, illegal problems, so I think the front and back end will crash… You might say, isn’t there a swagger document? “*” means “must pass”. But you know, this just tells the front end that this is required, it doesn’t actually limit the interface. What if the front end just forgot to pass it? So background parameter verification is necessary

Try to optimize the solution

If you’re smart enough to think about it, you don’t need to write these judgments on every interface. I’ll define my own annotation, intercept the request in an interceptor, and determine whether the fields in the request object have the annotation I defined. Suppose we define our own annotation

/** * Documented @target ({elementType.field}) @Retention(RetentionPolicy.runtime) public @interface NotNull {}Copy the code

Use the annotation we defined in the request parameter entity class

@data public class OrderRequest {@notnull private String merchantCode; @NotNull private String memberCode; }Copy the code

Do uniform verification in the interceptor

@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { HandlerMethod handlerMethod = (HandlerMethod)handler; Parameter[] parameters = handlerMethod.getMethod().getParameters(); For (Parameter Parameter: parameters) {Field[] declaredFields = parameter.getType().getdeclAredFields (); for (Field declaredField : declaredFields) { NotNull annotation = declaredField.getAnnotation(NotNull.class); // If the field has a NotNull annotation if(annotation! = null){ declaredField.setAccessible(true); String s = declaredField.get(getTargetObject(parameter,request)).toString(); If (s == null){throw new RuntimeException(declaredfield.getName ()+" : cannot be null "); } } } } }Copy the code

So that’s a simple NotNull check, which looks pretty simple, but I didn’t actually implement the getTargetObject method, because it’s a little tricky to get the target object, which is the order object of the controller method above, Because we can only get types, fields, and so on through reflection. If you want to get the target object, you have to get the byte stream from HttpServletRequest to do the conversion, which is what SpringMVC’s parameter resolution and data binding do.

Powerful hibernate validator

As you may have noticed, the custom validation we just implemented is pretty rudimentary. We just did a non-null validation, and we implemented it inside the interceptor. By the time we got to the interceptor, the data binding stuff was actually over. There is a framework, Hibernate-Validator, that solves this problem for us.

Before we get to that, we need to look at the Java Specification Requests (JSR 303). JSR 303 is the official Java Specification for Bean Validation, which is currently in version 2.0. Corresponds to JSR 380. Just like servlets, only one specification is mentioned, and we implement it. Hibernate-validator follows this specification with an implementation.

This article takes SpringBoot as an example. It is very convenient to use SpringBoot. You can directly introduce the Starter and SpringBoot will help us automatically configure it

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
Copy the code

Once introduced, we can use the set of validation annotations it provides. Now we replace the custom annotations with those provided by the framework, removing the previous interceptors

@ Data public class OrderRequest {/ / hibernate validator annotations provide @ javax.mail. Validation. Constraints. NotNull private String merchantCode; @javax.validation.constraints.NotNull private String memberCode; }Copy the code

The Controller code

    @PostMapping("/order/submit")
    public void submit(@RequestBody @Validated OrderRequest request){}
Copy the code

Using Postman to send a Post request and intentionally pass an empty JSON string to test, you can see that Postman receives the following return:

{"timestamp": "2021-06-09T11:25:49.136+00:00", "status": 400, "error": "Bad Request", "path": "/order/submit"}Copy the code

This suggests that the annotation works, but this is not the results we want, we must hope is to be able to tell the front-end, which failed to pass the check, or which field and want to know what is the check does not pass, is will pass parameters I didn’t pass, for example, or the number is greater than the maximum limit, and so on.

So here we need to do to error message processing, before that we need to know first, the parameter calibration of action is for SpringMVC, concrete is hibernate validator – provide validation rules, can be read for SpringMVC source code:

At the point where I break, SpringMVC determines whether or not it needs to be checked. In this case, it calls the implementation rule of Hibernate-Validator, and then it returns the result. Throws a MethodArgumentNotValidException type of exception. This way, we just need to catch the exception and the developer can respond to it. Catching this exception, I’m sure you all know SpringMVC uniform exception handling

SpringMVC unified exception handling

Unified exception handling is easy, just write a class with @RestControllerAdvice annotation and @ExceptionHandler annotation. However, the SpringMVC ResponseEntityExceptionHandler provides us a class, inheritance rewriting it handleExceptionInternal method

@RestControllerAdvice
public class GlobalExceptionController extends ResponseEntityExceptionHandler {
    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        if(ex instanceof MethodArgumentNotValidException){
            String message = ((MethodArgumentNotValidException) ex).getFieldErrors().stream().map(v -> v.getField()+":"+v.getDefaultMessage()).collect(Collectors.joining(";"));
            JSONObject obj = new JSONObject();
            obj.put("status",status.value());
            obj.put("error",status.getReasonPhrase());
            obj.put("message",message);
            obj.put("path",((NativeWebRequest) request).getNativeRequest(HttpServletRequest.class).getRequestURI());
            body = obj;
        }
        return super.handleExceptionInternal(ex, body, headers, status, request);
    }
}
Copy the code

Since we have seen above, for illegal parameter calibration throws MethodArgumentNotValidException anomalies, so the above we for judging the ex type, if it is the type, put the information we need in the body back to the front end, Using the Postman test again, the following results are obtained

{"path": "/order/submit", "error": "Bad Request", "message": "memberCode: cannot be null; MerchantCode: cannot be NULL ", "status": 400}Copy the code

The result is what we usually need. So why should we override this handleExceptionInternal method to intercept exceptions? May have a look our inheritance ResponseEntityExceptionHandler class source code, it defines a method using the @ ExceptionHandler annotation, and it stopped almost any exceptions that may occur

These returns do different things for different exception types, but they all end up internally calling the handleExceptionInternal method that we overrode.

As you may have noticed, it can only handle the exception types defined in @ExceptionHandler. In development, we will define some exception classes of our own, so how do we catch our own exception classes?

SpringMVC handles custom exceptions

To simulate ResponseEntityExceptionHandler its writing, we also write a method to use @ ExceptionHandler annotation can, first of all, a custom exception class ClientException

public class ClientException extends RuntimeException {}
Copy the code

Then in our unified handling class, we will define a custom exception handling method, handleClientException, and add @ExceptionHandler

@RestControllerAdvice @Slf4j public class GlobalExceptionController extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { if(ex instanceof MethodArgumentNotValidException){ String message = ((MethodArgumentNotValidException) ex).getFieldErrors().stream().map(v -> v.getField()+":"+v.getDefaultMessage()).collect(Collectors.joining(";" )); JSONObject obj = new JSONObject(); obj.put("status",status.value()); obj.put("error",status.getReasonPhrase()); obj.put("message",message); obj.put("path",((NativeWebRequest) request).getNativeRequest(HttpServletRequest.class).getRequestURI()); body = obj; } else if (ex instanceof ClientException){// Catch custom exception body = "1111"; } return super.handleExceptionInternal(ex, body, headers, status, request); } @ExceptionHandler public ResponseEntity<Object> handleClientException(ClientException ex, NativeWebRequest request) { HttpHeaders headers = new HttpHeaders(); HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED; return handleExceptionInternal(ex, null, headers, status, request); }}Copy the code

We then test it in our code, throw a ClientException, and the server will respond to the information we define.

Hibernate-validator annotates function queries

If you don’t use it very often, you may not be familiar with what the checkmarks are and what the checkmarks are, so as a warm guy I will list all the checkmarks here

annotations meaning annotations meaning annotations meaning
@NotNull Verify that the field is not null @NotEmpty The validation field is not empty. It is used to verify that a collection element is not empty @NotBlank The validation field is not empty. It is used to verify that a string is not empty
@Max Verify the maximum value of the field @Min Verify the minimum value of the field @Digits (Integer = integer number, Fraction = decimal number) Verify the integer number and the decimal number upper limit of the field
@DecimalMax Similar to @max, except that it can limit values to decimals, typically for double and Bigdecimal types @DecimalMin Similar to @min,…… @Range Verify that the numeric type field value is between the minimum and maximum value
@Size Validates that the value of a field is within the min and Max (inclusive) specified range, such as character length, set size @Length Verify that the length of the string value is within min and Max
@AssertFalse Verify that the Boolean type value is false @AssertTrue Verify that the Boolean type value is true @Future Verify that the date type field value is later than the current time
@Email Verify that the field value is a mailbox @Pattern (regex= regular expression) Verifies that the annotated element value does not match the specified regular expression @Past Verify that the date type field value is earlier than the current time
@Negative The check must be negative @Positive The check must be positive @PastOrPresent Verify that the date type field value is earlier than the current time or the current date
@NegativeOrZero Check must be negative or 0 @PositiveOrZero The check must be positive or 0 @FutureOrPresent Verify that the date type field value is later than the current time or the current date

Check the grouping

In actual development, we would write a class that accepts request parameters, possibly for multiple business validations. For example, I have a class, which is used for saving and updating, that is generally not to pass the ID, but update must pass the ID field. What if we want one class to satisfy both checks? Hibernate-validator provides us with a grouping scheme. We can divide the save into a group and identify which fields are in the save action group and which are in the update action group, so that we can meet the validation requirements of both businesses.

In fact, all annotations provided by hibernate-Validator have groups properties

@data public class ReqPremiumLevelRights {@notnull (groups = {updateGroup.class}) @Length(max = 8, groups = {SaveGroup.class, UpdateGroup.class}) @NotNull(groups = {SaveGroup.class, UpdateGroup.class}) private String name; @notnull (groups = {savegroup.class}) @size (min = 1, message = "must select at least one member benefit ", groups = {savegroup.class, UpdateGroup.class}) private List<Long> rightsIds; public interface SaveGroup {} public interface UpdateGroup {} }Copy the code

And then we just specify the group when we use the @Validated in Controller

@postmapping @operation (summary = "New member level ") public void save(@requestBody) @Validated(ReqPremiumLevelRights.SaveGroup.class) ReqPremiumLevelRights levelRights) { premiumMemberLevelService.save(levelRights); } @putMapping @operation (summary = "edit member level ") public void edit(@requestBody) @Validated(ReqPremiumLevelRights.UpdateGroup.class) ReqPremiumLevelRights levelRights) { premiumMemberLevelService.update(levelRights); }Copy the code

This solves the problem of one class being used for multiple business validations at the same time.

Does not rely on SpringMVC to trigger verification actions

With such a scenario, we need according to the values of a field to determine which group to use check, the above we are using a different interface separately, so you can choose to use which group of rules to check, if we’re going to use an interface to implement according to different situations of different packet check?

For example, for example, we now have a billing requirement, a set of verification for individual billing, a set of verification rules for enterprise billing, we generally do not use two interfaces to achieve this billing function. So it’s up to us in the code to decide which set of validation rules to use, depending on the user type.

@Autowired private Validator validator; @PostMapping public void apply(@RequestBody @Validated AppInvoiceApplyRequest request) { Set<ConstraintViolation<FundApplyVerifyRequest>> constraintViolations = null; ConstraintViolations = if (isCorporation) {constraintViolations = validator.validate(request,AppInvoiceApplyRequest.CompanyInvoiceGroup.class); } else {constraintViolations = validator.validate(request,AppInvoiceApplyRequest.PersonalCompanyInvoiceGroup.class); } the if (CollectionUtils isNotEmpty (constraintViolations)) {/ / if not through the check, An exception is thrown throw new ConstraintViolationException (constraintViolations); } appInvoiceService.save(request); }Copy the code

SpringMVC source code does the same thing when calling the validator. Now, we don’t specify any @validated validator in the argument, and we check whether the type of user passed from the front end is Validated. Use the validator to verify the corresponding validator group for different services according to the user type

Extend the annotations provided by Hibernate-Validator

As you may have noticed, Hibernate-Validator provides only 20 or so validation annotations, which can be used in most scenarios. However, due to the diversity of business during development, we may encounter validation requirements that it does not provide. For example, if we want to verify that a field must be a valid ID number, we have no choice. Hibernate-validator allows us to define our own annotations

First define a validation annotation

/** * Id check */ @target ({elementType.field}) @retention (retentionPolicy.runtime) @constraint (validatedBy = IdentityConstraintValidator. Class) / / in which validators check public @ interface IdentityNo {String message (default) "id number is not in conformity with the rules"; Class<? >[] groups() default { }; Class<? extends Payload>[] payload() default { }; }Copy the code

To verify the logic, simply implement ConstraintValidator and override the isValid method

/ * * * id constraint logic * / public class IdentityConstraintValidator implements ConstraintValidator < IdentityNo, String> { @Override public boolean isValid(String value, ConstraintValidatorContext context) { return check(value); // Return whether the id number conforms to the rules (implement the verification logic by yourself)}}Copy the code

In this way, we can define our own annotation and use it on the fields that need validation.

@data public class OrderRequest {@identityNo private String merchantCode; private String memberCode; }Copy the code

Postman test results

{"path": "/order/submit", "error": "Bad Request", "message": "merchantCode: ID number not correct ", "status": 400}Copy the code

conclusion

Hibernate-validator works perfectly with unified exception handling, so most problems are exposed when passing parameters in the front end

If this post helped you, be sure to like and follow. Your support is what keeps me going