preface

A back-end interface consists of four parts: interface URL, interface request mode (get, POST), request data, and response data. There is no “always the best” standard, but there is a big difference between a good back-end interface and a bad one. One of the most important things to look for is specification!

This article demonstrates step by step how to build a good back-end interface system, the system built naturally have a specification, and then build a new back-end interface will be very easy.

At the end of the article, I pasted the Github address of the project demonstration, which can be run after clone. And I submitted the code for each optimization record, so you can clearly see the improvement process of the project!

Required dependency packages

The SpringBoot configuration project is used here, and this article focuses on the back-end interface, so just import a spring-boot-starter-web package:

<! -- Web dependency package, > <dependency> <groupId>org.springframework.boot</groupId> <artifactId> Spring-boot-starter -web</artifactId> </dependency>Copy the code

This article also uses Swagger to generate API documentation and Lombok to simplify classes, but both are optional and not required.

Parameter calibration

An interface on the parameters (request data) will carry out security check, the importance of parameter check naturally needless to say, so how to check the parameters have to pay attention to.

Business layer check

First let’s take a look at the most common practice, which is to validate parameters at the business layer:

public String addUser(User user) { if (user == null || user.getId() == null || user.getAccount() == null || User. GetPassword () = = null | | user. GetEmail () = = null) {return "object or object field cannot be empty"; } if (StringUtils.isEmpty(user.getAccount()) || StringUtils.isEmpty(user.getPassword()) || Stringutils.isempty (user.getemail ())) {return "cannot input an empty string "; } the if (user getAccount (). The length () < 6 | | user. GetAccount (). The length () > 11) {return "account length must be 6 to 11 characters"; } the if (user. GetPassword (.) length () < 6 | | user. GetPassword (). The length () > 16) {return "password length must be 6-16 characters"; } if (! The Pattern matches (" ^ [a zA - Z0 - _ - 9] + @ [a zA - Z0 - _ - 9] + (\ \ [a zA - Z0 - _ - 9] +) + $", the user, getEmail ())) {return "E-mail format is not correct"; } return "success"; }Copy the code

There is nothing wrong with doing this, of course, and the format is neat and clear, but it is too cumbersome, this has not carried out business operations, just a parameter verification has so many lines of code, it is not elegant.

Use Spring Validator and Hibernate Validator to validate parameters. Both sets of Validator dependencies are already included with the Web dependencies described above, so you can use them directly.

Validator + BindResult validates

Validators make it easy to write validation rules and automate validation for you. First, annotate the fields to be checked in the input parameter, and each annotation corresponds to a different check rule, and can formulate the information after the check failure:

@data public class User {@notnull (message = "User id cannot be null ") private Long ID; @data public class User {@notnull (message =" User ID cannot be null ") private Long ID; @notnull (message = "user account cannot be empty ") @size (min = 6, Max = 11, message =" User account length must be 6-11 characters ") private String Account; @notnull (message = "user password cannot be empty ") @size (min = 6, Max = 11, message =" password must be 6-16 characters long ") private String password; @notnull (message = "user Email cannot be empty ") @email (message =" Email format is incorrect ") private String Email; }Copy the code

After the verification rules and error messages are configured, add @valid to the interface parameters to be verified and add the BindResult parameter to facilitate verification:

@RestController @RequestMapping("user") public class UserController { @Autowired private UserService userService; @PostMapping("/addUser") public String addUser(@RequestBody @Valid User user, BindingResult BindingResult) {for (ObjectError error: bindingResult.getAllErrors()) { return error.getDefaultMessage(); } return userService.addUser(user); }}Copy the code

When the request data is passed to the interface, the Validator completes the validation automatically, and the validation results are encapsulated in BindingResult. If there is an error message, we return it directly to the front end, and the business logic code does not execute at all.

At this point, the validation code in the business layer is no longer needed:

Public String addUser(User User) {return "success"; }Copy the code

Now you can see the parameter verification effect. We deliberately passed a parameter to the interface that did not meet the verification rules. We first passed an error data to the interface, and deliberately failed to meet the verification conditions of the password field:

{
    "account": "12345678",
    "email": "[email protected]",
    "id": 0,
    "password": "123"
}
Copy the code

Let’s look at the interface’s response data:

Isn’t that much more convenient? There are several benefits to using Validator:

  • Simplified code, before the business layer so a large section of verification code has been omitted.
  • Easy to use, so many validation rules can be easily implemented, such as mailbox format validation, before writing a long list of regular expressions, also prone to error, with the Validator directly a annotation. (There are more validation rule annotations, you can learn about them.)
  • Reducing the coupling and using validators allows the business layer to focus only on the business logic, away from the basic parameter validation logic.

Using Validator+ BindingResult is already a convenient way to validate parameters, and there are many projects that do this in real life, but it’s still not very convenient because you need to add a BindingResult parameter every time you write an interface, and then extract the error information and return it to the front end.

This is a bit cumbersome, and there is a lot of duplicate code (although you can wrap this duplicate code as a method). Can we get rid of the BindingResult step? Of course it can!

Validator + automatically throws an exception

We can remove the BindingResult step entirely:

@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user) {
    return userService.addUser(user);
}
Copy the code

What happens when you remove it? Try it out, or pass a parameter to the interface that does not conform to the validation rules as before. We observed the console interface can be found at this time has caused MethodArgumentNotValidException abnormal:

In fact, we have achieved the desired effect, if the parameter verification is not passed, the following business logic will not be executed. After removing BindingResult, an exception will be automatically raised, and the business logic will not be executed automatically when the exception occurs. In other words, there is no need to add related BindingResult related operations.

This is not the end of the story. The exception is raised, but we did not write the code to return an error message. What data will be responded to the front end if the parameter verification fails?

Let’s take a look at the interface response data after the exception occurred:

Yes, the entire error object information is directly returned to the front end! This is uncomfortable, but the solution to this problem is also very simple, which we will talk about global exception handling!

Global exception handling

If the parameter verification fails, it will automatically raise an exception. Of course, it is not possible to catch the exception manually. Otherwise, it would be better to use the previous BindingResult method. Do not want to manually capture this exception, and to handle this exception, it is just use SpringBoot global exception handling to achieve the effect of once and for all!

The basic use

First, we need to create a new class, annotate it with @ControllerAdvice or @RestControllerAdvice, and the class is configured as a global handler. (This depends on whether your Controller layer uses @Controller or @RestController.)

Create a new method in the class, annotate the method with @ExceptionHandler and specify the type of exception you want to handle. Then write the operation logic for the exception in the method, and complete the global handling of the exception!

We’re here to demonstrate for calibration failure thrown MethodArgumentNotValidException global processing parameters:

@RestControllerAdvice public class ExceptionControllerAdvice { @ExceptionHandler(MethodArgumentNotValidException.class) Public String MethodArgumentNotValidExceptionHandler (MethodArgumentNotValidException e) {/ / get ObjectError object from the exception object ObjectError objectError = e.getBindingResult().getAllErrors().get(0); / / and then extract the error information returned return objectError. GetDefaultMessage (); }}Copy the code

Let’s look at the response data after this verification failure:

Yes, this time back is the error message we made! We have elegantly implemented the desired functionality with global exception handling! If we want to write an interface parameter verification, we need to add the Validator annotation to the member variable of the input parameter, and add the @valid annotation to the parameter to complete the verification.

Custom exception

Global processing certainly does not handle just one exception, nor is it used to optimize just one parameter validation method. In actual development, how to handle exceptions is actually a very troublesome matter. Traditional handling of exceptions has the following concerns:

  • Catch exception (try… Throws exception (throws exception)
  • Do you do it at the Controller layer or do it at the Service layer or do it at the DAO layer
  • Do you handle exceptions by doing nothing, or by returning specific data, and if so what data
  • Not all exceptions can be caught in advance. What if an exception occurs that is not caught?

All of these problems can be solved by global exception handling, also known as unified exception handling. What does global and unified handling stand for? Stand for specification! With norms, many problems will be solved!

The basic usage of global exception handling is already known, so let’s take it a step further to standardize the exception handling in projects: custom exceptions.

In many cases, we need to manually throw exceptions, such as in the business layer when some conditions do not conform to the business logic, I can manually throw exceptions to trigger transaction rollback. The easiest way to manually throw an exception is to throw new RuntimeException(” exception information “), but it is better to use custom:

  • Custom exceptions can carry more information than just a string like this.
  • In project development, there are often many people responsible for different modules. Using custom exceptions can unify the way to display external exceptions.
  • The semantics of custom exceptions are clearer, and you can see that they are manually thrown in your project.

Let’s start writing a custom exception:

Public class APIException extends RuntimeException {private int code; private String msg; Public APIException() {this(1001, "API error "); } public APIException(String msg) { this(1001, msg); } public APIException(int code, String msg) { super(msg); this.code = code; this.msg = msg; }}Copy the code

Remember to add handling for our custom exception in the global exception handling class:

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

Thus to regulate the Exception handling is, of course you can also add to the Exception handling, abnormal that no matter what happens we can block out and then the response data to the front, but suggested that finally when doing this project launch, block out false information to expose to the front, in development in order to facilitate debugging or don’t do it.

Now the global exception handling and the custom exception have been done, I wonder if you have found a problem, that is, when we throw the custom exception, the global exception handling only responds to the error message MSG in the exception to the front end, and does not return the error code. This leads us to what we’re going to talk about next: unified data response

Unified data response

Now we have a specification for parameter validation and exception handling, but we have no specification for response data! For example, if I want to fetch paging data, I will get a list of data if I succeed, and if I fail, I will get an exception message, namely a string, which means that the front-end developer has no idea what the backend data will look like! So, unifying response data is a must in the front and back end specifications!

Custom unified response body

Unified data response the first step is certainly to do is our own custom response body class, whether the background is normal or abnormal, response to the front end of the data format is unchanged! So how do you define the response body?

You can refer to our custom exception class, and also a response message code and response message description MSG:

@getter public class ResultVO<T> {/** * status code, such as 1000 indicates a successful response */ private int code; /** * private String MSG; /** * private T data; public ResultVO(T data) { this(1000, "success", data); } public ResultVO(int code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; }}Copy the code

Then we modify the return value of the global exception handler:

@ExceptionHandler(apiException.class) public ResultVO<String> APIExceptionHandler(APIException e) { Return new ResultVO<>(LLDB etCode(), "response failed ", LLDB etMsg())); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { ObjectError objectError = e.getBindingResult().getAllErrors().get(0); / / note: oh, here the return type is a custom response body return new ResultVO < > (1001, "parameter validation fails," objectError. GetDefaultMessage ()); }Copy the code

Let’s take a look at what data will be returned to the front-end if an exception occurs:

OK, this exception message response is very good, the status code and response description and error message data are returned to the front end, and all exceptions will return the same format! Now that we’re done with the exception, don’t forget to change the return type when we go to the interface. Let’s add a new interface to see what happens:

@GetMapping("/getUser")
public ResultVO<User> getUser() {
    User user = new User();
    user.setId(1L);
    user.setAccount("12345678");
    user.setPassword("12345678");
    user.setEmail("[email protected]");

    return new ResultVO<>(user);
}
Copy the code

Let’s see what happens if the response is correct:

In this way, whether the response is correct or an exception occurs, the format of the response data is uniform, very standardized!

The data format is standardized, but the response code and response message MSG are not standardized. If 10 developers write 10 different response codes for the same type of response, then the unified response body format specification is meaningless! Therefore, the response code and response information must be standardized.

Response code enumeration

Enumerations are perfect for regulating response codes and response information in the response body. Let’s create a response code enumeration class:

@getter Public enum ResultCode {SUCCESS(1000, "Operation successful "), FAILED(1001," response FAILED "), VALIDATE_FAILED(1002, "Parameter verification FAILED "), ERROR(5000, "unknown ERROR "); private int code; private String msg; ResultCode(int code, String msg) { this.code = code; this.msg = msg; }}Copy the code

Then modify the constructor of the response body so that it only accepts the enumeration of response codes to set the response code and response information:

public ResultVO(T data) {
    this(ResultCode.SUCCESS, data);
}

public ResultVO(ResultCode resultCode, T data) {
    this.code = resultCode.getCode();
    this.msg = resultCode.getMsg();
    this.data = data;
}
Copy the code

Then modify the response code setting mode of global exception handling simultaneously:

@ExceptionHandler(apiException.class) public ResultVO<String> APIExceptionHandler(APIException e) { Return new ResultVO<>(resultcode. FAILED, LLDB ()); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { ObjectError objectError = e.getBindingResult().getAllErrors().get(0); / / note: oh, here to pass the response code enumeration return new ResultVO < > (the ResultCode. VALIDATE_FAILED, objectError getDefaultMessage ()); }Copy the code

In this way, the response code and response information can only be enumerated that several, truly achieve the response data format, response code and response information standardization, unification!

Global processing of response data

The interface returns a uniform response body, plus the exception returns a uniform response body, which is good enough, but there are some areas that can be optimized. It is quite normal to have hundreds of interfaces defined in a project. If each interface returns data and needs to be wrapped in the response body, it seems a bit troublesome. Is there a way to eliminate the wrapping process? There are drops, of course, but we’re going to use global processing.

First, create a class with annotations to make it a global processing class. Then override the method from the ResponseBodyAdvice interface to enhance our controller.

@ RestControllerAdvice (basePackages = {" com. Rudecrab. Demo. Controller "}) / / attention oh, Public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {@override public Boolean supports(MethodParameter returnType, Class<? Extends HttpMessageConverter<?>> aClass) {// If the interface returns a type that is itself ResultVO, there is no need to perform additional operations. Return false! returnType.getGenericParameterType().equals(ResultVO.class); } @Override public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType, Class<? extends HttpMessageConverter<? >> aClass, ServerHttpRequest Request, ServerHttpResponse Response) {// String type cannot be wrapped directly, So for some special processing if (returnType. GetGenericParameterType (.) the equals (String. Class)) {ObjectMapper ObjectMapper = new ObjectMapper(); Try {/ / the data package in ResultVO, then converted to a json string response to the front return objectMapper. WriteValueAsString (new ResultVO < > (data)); } catch (JsonProcessingException e) {throw new APIException(" Mandatory String Type error "); Return new ResultVO<>(data); return ResultVO<>(data); }}Copy the code

These two methods are overridden to enhance data before the Controller returns it. The SUPPORTS method must return true before executing the beforeBodyWrite method, so there are situations in which enhancements are not needed in the SUPPORTS method. The real manipulation of the returned data is still in the beforeBodyWrite method, we can wrap the data directly in this method so that we do not need to wrap the data for every interface, saving a lot of trouble.

We can now remove the interface data wrapper to see the effect:

@GetMapping("/getUser") public User getUser() { User user = new User(); user.setId(1L); user.setAccount("12345678"); user.setPassword("12345678"); user.setEmail("[email protected]"); Return User; // Return User; // Return User; }Copy the code

Then let’s look at the response data:

Successfully wrapped the data!

Note: beforeBodyWrite the wrapper data cannot be strong-cast directly on String data, so it needs special processing. I don’t go into too much detail here, you can learn more if you are interested.

conclusion

From there, the entire back-end interface infrastructure is built

  • Easy parameter validation is achieved with Validator + automatic exception throwing
  • Through global exception handling + custom exception to complete the exception operation specification
  • The specification of response data is completed through data unified response
  • The multi-aspect assembly elegantly completes the back-end interface coordination, allowing developers to have more experience focusing on business logic code and easily build back-end interfaces

Again, how to build the project system, how to write the back-end interface is not an absolute unified standard, not to say that must follow the article is the best, you can, this article every link you can according to their own ideas to carry out the code, I just provide an idea!