Project-driven learning, practice to test knowledge
preface
In my last blog post, I wrote about how to build an elegant back-end interface architecture with parameter verification + unified response code + unified exception handling:
[Project practice] SpringBoot three moves combination, hand in hand to teach you to play elegant back-end interface. We did it:
- 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
This seems to be perfect, and many places are unified and standardized. But!!! Things tend to have two sides of one body. The benefits brought by unification and standardization are needless to say, but what about the disadvantages? The downside is not being flexible enough.
Unified data response
Where is the lack of flexibility? The unified response to data. The data that the back-end responds to the front-end is divided into three parts:
Code: indicates the response code. For example, 1000 indicates a successful response and 1001 indicates a failed response
MSG: Response information, used to describe the response
Data: indicates the specific data of the response
We unify code and MSG through the enumeration of response codes, and we will only respond to the code and MSG specified by the enumeration anyway. I naively thought this would work for all applications, until I met a question from an Internet user:
What if each parameter I check has a different error message, code, message? For example, the error code of the mobile phone number verification is V00001, and the id number error code is V00002.
Validation fails. You can manually capture the validation exception object, determine which field it is, and manually return an error code based on that field. Let me first demonstrate what I mean by this extremely cumbersome practice:
Manually capture exception objects
Because the BindingResult object encapsulates a lot of information, we can get the field name of the validation error, and then respond with the corresponding error code and error message. If BindingResult is processed in the Controller layer, it will not be caught by the global exception processing we wrote before, and will not respond to the unified error code, so that each field has its own response code and response information:
@PostMapping("/addUser")
public ResultVO<String> addUser(@RequestBody @Valid User user, BindingResult bindingResult) {
for (ObjectError error : bindingResult.getAllErrors()) {
// Get the checksum error parameter field
String field = bindingResult.getFieldError().getField();
// Determine which field is in error and return the data response body
switch (field) {
case "account":
return new ResultVO<>(100001."Account verification error", error.getDefaultMessage());
case "password":
return new ResultVO<>(100002."Password validation error", error.getDefaultMessage());
case "email":
return new ResultVO<>(100003."Mailbox verification error", error.getDefaultMessage()); }}// If there is no error, return the correct information directly
return new ResultVO<>(userService.addUser(user));
}
Copy the code
We mistyped the parameters on purpose.
Well, it worked. But the code is a pain in the ass once it’s out there. Tedious, poor maintenance, poor reuse, this is the judgment of three fields like this, to those special fields can not take off?
This way directly pass!
Instead of capturing the exception manually, we will simply discard validation.
Manual calibration
Let’s try:
@PostMapping("/addUser")
public ResultVO<String> addUser(@RequestBody User user) {
// Check parameters
if (user.getAccount().length() < 6 || user.getAccount().length() > 11) {
return new ResultVO<>(100001."Account verification error"."Account must be 6-11 characters long");
}
if (user.getPassword().length() < 6 || user.getPassword().length() > 16) {
return new ResultVO<>(100002."Password validation error"."Password must be 6-16 characters long");
}
if(! Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
return new ResultVO<>(100003."Mailbox verification error"."Email format is incorrect");
}
// If there is no error, return the correct information directly
return new ResultVO<>(userService.addUser(user));
}
Copy the code
Oh, shit. It’s better than that. At the very least, you can enjoy the convenience of validation rules, which is tedious and long.
Is there a way to validate validation without specifying the response code for each field? No, of course there are!
Remember that BindingResult gets the name of the field that checked the error? Now that we can get the Field name, let’s go one step further and of course we can get the Field object, if we can get the Field object we can also get the annotation of the Field. Yes, we are to use annotations to elegant implementation of the above function!
Custom annotations
If validation fails, we can retrieve the field object and get the annotation information for the field. If we annotate each field with our custom error code and MSG message, we can easily return the response body.
First we define a custom annotation:
/ * * *@author RC
* @descriptionCustom parameter verification error code and error message annotation */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD}) // Indicates that the annotation can only be placed on the field of the class
public @interface ExceptionCode {
// Response code code
int value(a) default 100000;
// Response message MSG
String message(a) default"Parameter verification error";
}
Copy the code
We then add our own custom annotation to the parameter fields:
@Data
public class User {
@notnull (message = "user ID cannot be empty ")
private Long id;
@notnull (message = "user account cannot be empty ")
@size (min = 6, Max = 11, message = "Account length must be 6-11 characters ")
ExceptionCode(value = 100001, message = "Account validation error ")
private String account;
@notnull (message = "user password cannot be empty ")
@size (min = 6, Max = 11, message = "Password must be 6-16 characters long ")
@ExceptionCode(value = 100002, message = "password validation error ")
private String password;
@notnull (message = "user mailbox cannot be empty ")
@email (message = "Email format is not correct ")
@ExceptionCode(value = 100003, message = "Email validation error ")
private String email;
}
Copy the code
Then we go to our global exception handling to do this, noting the code comment:
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) throws NoSuchFieldException {
// Get the error message from the exception object
String defaultMessage = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
// Get the Field object from the Field nameClass<? > parameterType = e.getParameter().getParameterType();// Get the wrong field name
String fieldName = e.getBindingResult().getFieldError().getField();
Field field = parameterType.getDeclaredField(fieldName);
// Get a custom annotation on the Field object
ExceptionCode annotation = field.getAnnotation(ExceptionCode.class);
// If there are annotations, return the response information of the annotations
if(annotation ! =null) {
return new ResultVO<>(annotation.value(),annotation.message(),defaultMessage);
}
// Retrieve the error message without annotations and return the unified error code
return newResultVO<>(ResultCode.VALIDATE_FAILED, defaultMessage); }}Copy the code
Global exception handling is done here, so the Controller layer can just focus on the business logic:
@apiOperation (" Add user ")
@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user) {
return userService.addUser(user);
}
Copy the code
Let’s take a look at the effect:
As you can see, whenever we add a custom annotation, if the parameter verification fails, the annotation’s error code and error message MSG will be returned. This approach brings the following benefits compared with the previous two approaches:
- Convenient. From a lot of manual judgment code, to a single annotation
- Strong reusability. It can have an effect on not just one object, but on all other checked objects without having to write extra code
- Can work with the unified response code. The first two methods are either a custom error code for all parameters of an object, or a uniform response code for all parameters. This way, if you don’t want to set a custom response code for a field, it automatically returns the uniform response code without annotations
It’s too convenient! This approach is like adding an extension to the data unified response, which is both standardized and flexible!
Of course, I’m just giving you an idea of what you can do with custom annotations. For example, we can apply annotations directly to the entire class, and have a class argument with an error code. You can also set the value of the annotation to an enumeration class to further unify the specification…
Bypass data unified response
As we demonstrated above how to make error codes flexible, we continue to extend them further.
Global unified processing of data response enables all data to be wrapped up in ResultVO and returned to the front end, so that all responses received by our front end are fixed format, which is very convenient. But! What if our interface is not for our own front-end use? We need to call other third-party interface and give the response data, others to receive the response may not be according to code, MSG, data oh! So, we also have to provide an extensibility that allows bypassing the data unified response!
As I guess you can guess, we’ll still use custom annotations to do this:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD}) // Indicates that the annotation can only be placed on methods
public @interface NotResponseBody {
}
Copy the code
As long as we add this annotated method, we don’t do a unified response to the data, and return whatever type is returned
@GetMapping("/getUser")
@NotResponseBody
public User getUser(a) {
User user = new User();
user.setId(1L);
user.setAccount("12345678");
user.setPassword("12345678");
user.setEmail("[email protected]");
return user;
}
Copy the code
We will then judge this annotation in the data Unified Response handling class:
@RestControllerAdvice(basePackages = {"com.rudecrab.demo.controller"})
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class
> aClass) {
// If the interface returns a type that is itself ResultVO there is no need for additional operations, return false
There is no need to do anything extra if we add our custom annotations to the method
return !(returnType.getParameterType().equals(ResultVO.class) || returnType.hasMethodAnnotation(NotResponseBody.class));
}
...
}
Copy the code
Ok, so let’s see what happens. Before annotation, the data is wrapped in the response body:
The annotated method returns the data itself:
Very good, adding another layer of extension to unified data response.
conclusion
After a wave of operations, we went from no specification to specification, and from specification to extended specification:
No specification (mess) –> Specification (inflexible) –> Extended specification (Nice)
The cause of writing this article is what I said earlier, a net friend suddenly asked me that question, I just suddenly found in the project development all kinds of things are likely to appear, no architecture can be perfect, we want to pursue the perfect, not so much as we should go to pursue, processing requirements change mixed ability!
Finally, put the github address of the project here, and it can be directly cloned to the local, and I have made the code submission for each optimization record, you can clearly see the improvement process of the project, if it is helpful to you, please click a star on Github, I will continue to update more [project practice] oh!
Blog, Github, wechat public account is: RudeCrab, welcome to follow! If it is helpful to you, you can collect, like, star, look at, share ~~ your support, is the biggest power of my writing
Wechat reprint please contact the public number to open the white list, other places reprint please indicate the original address, the original author!