1 overview

Based on Spring Boot, this article describes how to design an excellent back-end interface system from the following three directions:

  • Parameter verification: YesHibernate ValidatorA variety of annotations, quick failure modes, grouping, group sequences and custom annotations /Validator
  • Exception handling: InvolvedControllerAdvice/@RestControllerAdviceAs well as@ExceptionHandler
  • Data response: Involves how to design a response body and how to package it

With a good back-end interface architecture, not only do you have a specification, but it’s also easy to extend new interfaces. This article demonstrates how to build a good back-end interface architecture from scratch.

2 New Construction project

Open familiar IDEA and select dependency:

Create the following files first:

TestController. Java:

@RestController
@RequestMapping("/")
@CrossOrigin(value = "http://localhost:3000")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TestController {
    private final TestService service;
    @PostMapping("test")
    public String test(@RequestBody User user)
    {
        returnservice.test(user); }}Copy the code

@requiredargsconstructor is used instead of @autowired, and since I’m using Postwoman tests, I need to add the cross-domain annotation @crossorigin, which defaults to port 3000 (Postwoman port).

TestService. Java:

@Service
public class TestService {
    public String test(User user)
    {
        if(StringUtils.isEmpty(user.getEmail()))
            return "Mailbox cannot be empty.";
        if(StringUtils.isEmpty(user.getPassword()))
            return "Password cannot be empty.";
        if(StringUtils.isEmpty(user.getPhone()))
            return "The phone cannot be empty.";
// Persist
        return "success"; }}Copy the code

The business layer validates the parameters first, where persistence is omitted.

User. Java:

@Data
public class User {
    private String phone;
    private String password;
    private String email;
}
Copy the code

3 Parameter Verification

Let’s start with validation. In the above example, validation is done at the business level, which is fine. However, it’s not a good idea to do so much validation before the business operation.

3.1 Hibernate Validator

3.1.1 introduction

JSR stands for Java Specification Requests, a formal request to add a standardized technical Specification to the JCP(Java Community Process). Jsr-303 is a Java EE6 subspecification called Bean Validator. Hibernate Validator is a reference implementation of Bean Validator. In addition to implementing the built-in constraint implementation in all JSR-303 specifications, There are additional constraints, detailed below:

  • @NullThe annotated element must benull(To save space, “element” means “annotated element must be”.)
  • @NotNull: The element does notnull
  • @AssertTrue: the element istrue
  • @AssertFalse: the element isfalse
  • @Min(value): The element is greater than or equal to the specified value
  • @Max(value): The element is less than or equal to the specified value
  • @DecimalMin(value): The element is greater than the specified value
  • @DecimalMax(value): The element is smaller than the specified value
  • @Size(max,min): Element size within a given range
  • @Digits(integer,fraction): Specifies the maximum number of integer digits in the element stringintegerThe decimal number specifies the maximum number of decimal placesfractionposition
  • @PastThe: element is a past date
  • @FutureThe: element is a future date
  • @PatternThe: element must conform to the regular expression

Hibernate Validator has the following additional constraints:

  • @EamilThe: element is mailbox
  • @Length: The string size is within the specified range
  • @NotEmpty: The string must be non-empty (current latest)6.1.5Version deprecated, standard is recommended@NotEmpty)
  • @Range: The number is in the specified range

In Spring, Hibernate Validation is rewrapped, automatic Validation is added, and Validation information is encapsulated in a specific BindingResult. Here’s how to use it.

3.1.2 use

Add @notempty to each field, add @email to each mailbox, add an 11-bit limit to each phone, and add message to each annotation to indicate the corresponding prompt:

@Data
public class User {
    @notempty (message = "Phone cannot be empty ")
    @length (min = 11, Max = 11,message = "Phone number must be 11 digits ")
    private String phone;
    @notempty (message = "password cannot be empty ")
    @length (min = 6, Max = 20,message = "password must be 6-20 digits ")
    private String password;
    @notempty (message = "mailbox cannot be empty ")
    @email (message = "Email format is not correct ")
    private String email;
}
Copy the code

@notnull or @notblank are sometimes used for strings. The difference is as follows:

  • @NotEmpty: not fornullAnd the length has to be greater than 0, exceptStringOutside, theCollection/MapThe/array also works
  • @NotBlank: only forString, not fornullAnd calltrim()After, the length must be greater than 0, that is, must have actual characters other than Spaces
  • @NotNull: not fornull

Then delete the parameter verification operation of the business layer and modify the control layer as follows:

@PostMapping("test")
public String test(@RequestBody @Valid User user, BindingResult bindingResult)
{
    if(bindingResult.hasErrors())
    {
        for(ObjectError error:bindingResult.getAllErrors())
            return error.getDefaultMessage();
    }
    return service.test(user);
}
Copy the code

Add @valid and BindingResult to the object you want to validate to get an error message and return it.

3.1.3 test

All use wrong parameter Settings, return “mailbox format is not correct” :

The second test uses the correct parameters except for the password, and returns “password must be 6-20 characters” :

The third test returns “success” with all correct parameters:

3.2 Setting the verification mode

Hibernate Validator has two validation modes:

  • Common mode: the default mode that verifies all attributes and returns all authentication failures
  • Fast failure mode: Returns as long as there is one validation failure

Using rapid failure mode need to create the Validator through HibernateValidateConfiguration and ValidateFactory, and use the Validator. The validate () manual verification.

Start by adding a class that generates a Validator:

@Configuration
public class FailFastValidator<T> {
    private final Validator validator;
    public FailFastValidator(a)
    {
        validator = Validation
        .byProvider(HibernateValidator.class).configure()
		.failFast(true).buildValidatorFactory()
		.getValidator();
    }

    public Set<ConstraintViolation<T>> validate(T user)
    {
        returnvalidator.validate(user); }}Copy the code

[root@requiredargsconstructor] [root@requiredargsconstructor] [root@requiredargsconstructor] [root@requiredargsconstructor] [root@requiredargsconstructor]

@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TestController {
    private final TestService service;
    private final FailFastValidator<User> validator;
    @PostMapping("test")
    public String test(@RequestBody User user, BindingResult bindingResult)
    {
        Set<ConstraintViolation<User>> message = validator.validate(user);
        message.forEach(t-> System.out.println(t.getMessage()));
// if(bindingResult.hasErrors())
/ / {
// bindingResult.getAllErrors().forEach(t->System.out.println(t.getDefaultMessage()));
// for(ObjectError error:bindingResult.getAllErrors())
// return error.getDefaultMessage();
/ /}
        returnservice.test(user); }}Copy the code

Test (result of three consecutive checks) :

In common mode (failFast(false)), three information is output consecutively at one time:

3.3 @Validwith@Validated

@ is Valid javax.mail. Validation the inside of the bag, and @ is Validated org. Springframework. Validation. The annotation, inside is an encapsulation of the @ Valid, rather then @ the enhanced version of Valid, The verification mechanism provided by Spring provides grouping and group sequence functions compared with @Valid and @Validated. The following are introduced respectively.

3.4 grouping

Group verification can be used when different verification modes need to be used in different situations. For example, the verification ID is not required during registration and is required when information is modified. However, the default verification mode verifies both cases. In this case, group verification is required.

The following uses different groups to verify the different lengths of phone numbers.

@Data
public class User {
    @notempty (message = "Phone cannot be empty ")
    @length (min = 11, Max = 11,message = "phone number must be 11 digits ",groups = {groupa.class})
    @length (min = 12, Max = 12,message = "phone number must be 12 digits ",groups = {groupb.class})
    private String phone;
    @notempty (message = "password cannot be empty ")
    @length (min = 6, Max = 20,message = "password must be 6-20 digits ")
    private String password;
    @notempty (message = "mailbox cannot be empty ")
    @email (message = "Email format is not correct ")
    private String email;

    public interface GroupA{}
    public interface GroupB{}}Copy the code

GroupA/GroupB is the two empty interfaces of User. Then change the control layer:

public String test(@RequestBody @Validated({User.GroupB.class}) User user, BindingResult bindingResult)
{
    if(bindingResult.hasErrors())
    {
        bindingResult.getAllErrors().forEach(t->System.out.println(t.getDefaultMessage()));
        for(ObjectError error:bindingResult.getAllErrors())
            return error.getDefaultMessage();
    }
    return service.test(user);
}
Copy the code

At @, GroupB is used and the phone number is 12 digits. The test is as follows:

3.5 set sequence

By default, constraints for different groups are validated unordered, that is, for the following User class:

@Data
public class User {
    @notempty (message = "Phone cannot be empty ")
    @length (min = 11, Max = 11,message = "Phone number must be 11 digits ")
    private String phone;
    @notempty (message = "password cannot be empty ")
    @length (min = 6, Max = 20,message = "password must be 6-20 digits ")
    private String password;
    @notempty (message = "mailbox cannot be empty ")
    @email (message = "Email format is not correct ")
    private String email;
}
Copy the code

The verification sequence is different each time, and the results of the three tests are as follows:

There are times when order doesn’t matter, and times when order matters, such as:

  • The constraint validation in the second group runs on a steady state that is validated by the first group
  • The validation of a certain group is time-consuming and consumes a large amount of CPU and memory. Therefore, the best choice is to validate the group last

Therefore, it is necessary to provide an orderly verification mode during group verification. One group can be defined as the sequence of other groups, so that the sequence of each verification can be fixed instead of random. In addition, if the previous group fails in the verification sequence, the subsequent group will not be verified.

As an example, first modify the User class and define the group sequence:

@Data
public class User {
    @notempty (message = "phone cannot be empty ",groups = {first.class})
    @length (min = 11, Max = 11,message = "phone number must be 11 digits ",groups = {Second. Class})
    private String phone;
    @notempty (message = "password cannot be empty ",groups = {first.class})
    @length (min = 6, Max = 20,message = "password must be 6-20 bits ",groups = {Second. Class})
    private String password;
    @notempty (message = "mailbox cannot be empty ",groups = {first.class})
    @email (message = "Email format incorrect ",groups = {Second. Class})
    private String email;

    public interface First{}
    public interface Second{}
    @GroupSequence({First.class,Second.class})
    public interface Group{}}Copy the code

Defines two empty interfaces, First and Second, to indicate the order, and uses @groupsequence in the Group to specify the order.

Then modify the control layer and define the groups in @ “Validated” :

public String test(@RequestBody @Validated({User.Group.class}) User user, BindingResult bindingResult)
Copy the code

In this way, parameters can be checked in a fixed order.

3.6 Custom Verification

Although annotations in Hibernate Validator can be used in a wide range of situations, sometimes specific validation rules are needed, such as password strength, to artificially determine whether a weak password is strong or not. In other words, you need to add a custom verification method, which can be handled in two ways:

  • Custom annotations
  • The customValidator

First, let’s look at ways to customize annotations.

3.6.1 Customizing Annotations

Here add a judgment WeakPassword annotation WeakPassword:

@Documented
@Constraint(validatedBy = WeakPasswordValidator.class)
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface WeakPassword{
    String message(a) default"Please use a stronger password."; Class<? >[] groups()default {};
    Class<? extends Payload>[] payload() default {};
}
Copy the code

ConstraintValidator

WeakPasswordValidator, ConstraintValidator

WeakPasswordValidator, ConstraintValidator

WeakPasswordValidator, ConstraintValidator

WeakPasswordValidator, ConstraintValidator

WeakPasswordValidator.
,t>
,t>
,t>
,t>
,t>

public class WeakPasswordValidator implements ConstraintValidator<WeakPassword.String> {
    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        return s.length() > 10;
    }
    @Override
    public void initialize(WeakPassword constraintAnnotation) {}}Copy the code

Then you can modify User as follows, add custom annotation @WeakPassword in the corresponding field:

@Data
public class User {
	/ /...
    @WeakPassword(groups = {Second.class})
    private String password;
    / /...
}
Copy the code

The tests are as follows:

3.6.2 customValidator

In addition to custom annotations, you can also customize the Validator to implement custom parameter validation. You need to implement the Validator interface:

@Component
public class WeakPasswordValidator implements Validator{
    @Override
    public boolean supports(Class
        aClass) {
        return User.class.equals(aClass);
    }

    @Override
    public void validate(Object o, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors,"password"."password.empty");
        User user = (User)o;
        if(user.getPassword().length() <= 10)
            errors.rejectValue("password"."Password is not strong enough!"); }}Copy the code

Supports and validate are implemented:

  • support: verifies that the class is an instance of a class
  • validate: whensupportsreturntrueAfter, validate the given objecto, when an error occurs, toerrorsRegistration error

ValidationUtils. RejectIfEmpty check when the object o a field attribute is null, the errors of the registration error, attention will not interrupt the operation of the statement, even if the password is empty, the user, getPassword (), or would you run, A null-pointer exception is thrown. Errors. rejectValue does not interrupt the execution of the statement, but registers the error message.

Change the return value in the control layer to getCode() :

if(bindingResult.hasErrors())
{
    bindingResult.getAllErrors().forEach(t-> System.out.println(t.getCode()));
    for(ObjectError error:bindingResult.getAllErrors())
        return error.getCode();
}
return service.test(user);
Copy the code

Testing:

4 Exception Handling

At this point, parameter validation is complete, and the next step is to handle the exception.

If BindingResult is removed from parameter validation, the entire backend exception is returned to the front end:

//public String test(@RequestBody @Validated({User.Group.class}) User user, BindingResult bindingResult)
public String test(@RequestBody @Validated({User.Group.class}) User user)
Copy the code

This way, although the back end is convenient, you don’t need to add every interfaceBindingResultHowever, the front end is not easy to handle, the whole exception is returned, so the back end needs to catch these exceptions, but, cannot manually catch each one, so it is not as good as beforeBindingResultIn this case, global exception handling is needed.

4.1 Basic Usage

The steps for handling global exceptions are as follows:

  • Create classes for global exception handling: plus@ControllerAdvice/@RestControllerAdviceAnnotations (depending on which control layer is used@Controller/@RestController.@ControllerYou can jump to the corresponding page and return toJSONSuch as add@ResponseBodyCan, but@RestControllerThe equivalent of@Controller+@ResponseBodyTo return toJSONDon’t need to add@ResponseBody, but the view parser cannot parsejspAs well ashtmlPage)
  • Create exception handling methods: add@ExceptionHandlerSpecify the type of exception you want to handle
  • Exception handling: Handle exceptions in the corresponding exception handling method

Add a GlobalExceptionHandler class GlobalExceptionHandler:

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e)
    {
        ObjectError error = e.getBindingResult().getAllErrors().get(0);
        returnerror.getDefaultMessage(); }}Copy the code

Start with @RestControllerAdvice and add @ExceptionHandler to the exception handling method.

Then modify the control layer to remove the BindingResult:

@PostMapping("test")
public String test(@RequestBody @Validated({User.Group.class}) User user)
{
    return service.test(user);
}
Copy the code

Then you can test:

Global exception handling is much more convenient than adding BindingResult to each interface and handling all exceptions centrally.

4.2 Custom Exceptions

Here’s a new TestException TestException:

@Data
public class TestException extends RuntimeException{
    private int code;
    private String msg;

    public TestException(int code,String msg)
    {
        super(msg);
        this.code = code;
        this.msg = msg;
    }

    public TestException(a)
    {
        this(111."Test exception");
    }

    public TestException(String msg)
    {
        this(111,msg); }}Copy the code

Add a method to handle the exception in the global exception handler class:

@ExceptionHandler(TestException.class)
public String testExceptionHandler(TestException e)
{
    return e.getMsg();
}
Copy the code

Test at the control layer:

@PostMapping("test")
public String test(@RequestBody @Validated({User.Group.class}) User user)
{
    throw new TestException("Abnormal");
// return service.test(user);
}
Copy the code

The results are as follows:

5 Data Response

After deal with the parameter calibration and exception handling, the next step is to set up a unified standardized response data, both in general response to the success or failure will have a status code, response success will take response data, response information carries corresponding failure, therefore, the first step is to design a unified response body.

5.1 Unified Response Body

The unified response body needs to create the response body class. In general, the response body needs to contain:

  • Status code:String/int
  • Response information:String
  • Response data:Object/T(generic)

Here we simply define a unified response body Result:

@Data
@AllArgsConstructor
public class Result<T> {
    private String code;
    private String message;
    private T data;
}
Copy the code

Next modify the global exception handling class:

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<String> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e)
    {
        ObjectError error = e.getBindingResult().getAllErrors().get(0);
        return new Result<>(error.getCode(),"Parameter verification failed",error.getDefaultMessage());
    }

    @ExceptionHandler(TestException.class)
    public Result<String> testExceptionHandler(TestException e)
    {
        return new Result<>(e.getCode(),"Failure",e.getMsg()); }}Copy the code

Use Result

to encapsulate the return value as follows:

If you need to return specific User data, you can modify the interface of the control layer to directly return Result

:

@PostMapping("test")
public Result<User> test(@RequestBody @Validated({User.Group.class}) User user)
{
    return service.test(user);
}
Copy the code

Testing:

5.2 Enumeration of response codes

In general, we can make the response code an enumeration class:

@Getter
public enum ResultCode {
    SUCCESS("111"."Success"),FAILED("222"."Failure");

    private final String code;
    private final String message;
    ResultCode(String code,String message)
    {
        this.code = code;
        this.message = message; }}Copy the code

The enumeration class encapsulates the status code and information so that when the result is returned, only the corresponding enumeration value and data need to be passed in:

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<String> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e)
    {
        ObjectError error = e.getBindingResult().getAllErrors().get(0);
        return new Result<>(ResultCode.FAILED,error.getDefaultMessage());
    }

    @ExceptionHandler(TestException.class)
    public Result<String> testExceptionHandler(TestException e)
    {
        return newResult<>(ResultCode.FAILED,e.getMsg()); }}Copy the code

5.3 Wrapping the response body globally

Unifying the response body is a good idea, but you can take it a step further, because you need to wrap the response body every time you return. It’s only one line of code, but each interface needs to be wrapped. This is a very cumbersome operation. You can optionally implement ResponseBodyAdvice

to wrap the response body globally.

Modify the original global exception handling class as follows:

@RestControllerAdvice
public class GlobalExceptionHandler implements ResponseBodyAdvice<Object> {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<String> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e)
    {
        ObjectError error = e.getBindingResult().getAllErrors().get(0);
        return new Result<>(ResultCode.FAILED,error.getDefaultMessage());
    }

    @ExceptionHandler(TestException.class)
    public Result<String> testExceptionHandler(TestException e)
    {
        return new Result<>(ResultCode.FAILED,e.getMsg());
    }

    @Override
    public boolean supports(MethodParameter methodParameter, Class
       > aClass) {
        return! methodParameter.getParameterType().equals(Result.class); }@Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class
       > aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        return newResult<>(o); }}Copy the code

ResponseBodyAdvice

  • supportsMethod: Check whether controller return method type is supportedsupportsDetermine which types need to be wrapped and which are returned without
  • beforeBodyWriteMethods: whensupportsreturntrueAfter, the data is wrapped so that it is not needed when the data is returnedResult<User>Manually wrap, but return directlyUserCan be

Then change the control layer to return the entity class User directly instead of the response body wrapper class Result

:

@PostMapping("test")
public User test(@RequestBody @Validated({User.Group.class}) User user)
{
    return service.test(user);
}
Copy the code

The test output is as follows:

5.4 Bypass global wrapping

Although according to the above ways can make all the backend data in accordance with the unified form is returned to the front end, but sometimes is not returned to the front end but returned to other third parties, then don’t need to code as well as the information such as MSG, just need the data, in this way, can provide an annotation on the method to get around the global response body packaging.

For example, add a @notresponsebody annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface NotResponseBody {
}
Copy the code

You then need to judge in supports in the classes that handle global wrapping:

@Override
public boolean supports(MethodParameter methodParameter, Class
       > aClass) {
    return! ( methodParameter.getParameterType().equals(Result.class) || methodParameter.hasMethodAnnotation(NotResponseBody.class) ); }Copy the code

Finally, modify the control layer and add custom annotation @notresponseBody to the method that needs to be bypassed:

@PostMapping("test")
@NotResponseBody
public User test(@RequestBody @Validated({User.Group.class}) User user)
Copy the code

6 summarizes

7 source

Clone it directly and open it using IDEA. Each optimization has been submitted once. You can see the optimization process.

  • Github
  • Yards cloud

8 reference

1, UncleChen blog -SpringBoot custom request parameter verification

2. Summary and distinction between -@Valid and @ “Validated”

3. The difference between -@Controller and @restController

4, Jane book – [project practice] -SpringBoot three moves combination, hand in hand to teach you to play elegant back-end interface

5. Brief Description – [Project Practice] How to elegantly extend the specification while unifying the back-end interface specification