1 overview
Based on Spring Boot, this article describes how to design an excellent back-end interface system from the following three directions:
- Validation: Hibernate Validator annotations, quick failure modes, grouping, group sequences, and custom annotations/Validators
- Exception handling: Involves ControllerAdvice/@RestControllerAdvice and @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)
{
return service.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 "; 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 the 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:
- @null: The annotated element must be Null.
- @notnull: elements are NotNull
- AssertTrue: The element is true
- @assertFalse: Element is false
- @min (value) : The value of 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 larger 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) : The integer number in the element string specifies the maximum integer number, and the decimal number specifies the maximum fraction
- @past: The element is a Past date
- @Future: Element is a Future date
- @pattern: Elements must conform to regular expressions
Hibernate Validator has the following additional constraints:
- @eamil: The element is mailbox
- @length: The string size is within the specified range
- @notempty: The string must be non-empty (deprecated in 6.1.5, the standard @notempty is recommended)
- @range: indicates that 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 number 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 characters ") private String password; @notempty (message = "mailbox cannot be empty ") @email (message =" mailbox format is incorrect ") private String Email; }Copy the code
@notnull or @notblank are sometimes used for strings. The difference is as follows:
- @notempty: cannot be null and the length must be greater than 0. This applies to Collection/Map/ array in addition to String
- @notblank: String only, cannot be null, and must be greater than 0 after trim(), that is, must have actual characters other than Spaces
- @notnull: The value cannot be null
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()
{
validator = Validation
.byProvider(HibernateValidator.class).configure()
.failFast(true).buildValidatorFactory()
.getValidator();
}
public Set<ConstraintViolation<T>> validate(T user)
{
return validator.validate(user);
}
Copy the code
[root@requiredargsconstructor] [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(); // } return service.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 @ Valid with @ 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 ",groups = {groupb.class}) private String phone; @notempty (message = "password cannot be empty ") @length (min = 6, Max = 20,message =" password must be 6-20 characters ") private String password; @notempty (message = "mailbox cannot be empty ") @email (message =" mailbox format is incorrect ") 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 number 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 characters ") private String password; @notempty (message = "mailbox cannot be empty ") @email (message =" mailbox format is incorrect ") 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 = Group = {Second. Class}) private String phone; @notempty (message = "password must not 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 = "mailboxes cannot be empty ",groups = {first.class}) @email (message =" mailboxes cannot be empty ",groups = {Second 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” :
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 custom Validator
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() default "Use stronger password "; @retention (retentionPolicy.runtime) public @interface WeakPassword{String message() default" Use stronger password "; Class<? >[] groups() default {}; Class<? extends Payload>[] payload() default {}; }Copy the code
ConstraintValidator<A,T> WeakPasswordValidator, ConstraintValidator<A,T> WeakPasswordValidator, ConstraintValidator<A,T> WeakPasswordValidator, ConstraintValidator<A,T> WeakPasswordValidator, ConstraintValidator<A,T> WeakPasswordValidator.
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 Customizing the Validator
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 whether the class is an instance of a class
- Validate: When supports returns true, validates the given object O and registers errors with errors when errors occur
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) Replicates the codeCopy the code
This is a convenient way to use BindingResult on each interface, but the front-end is not easy to handle. The whole exception is returned, so the backend needs to catch these exceptions. However, you can’t manually catch every exception, so it is better to use BindingResult before. In this case, global exception handling is needed.
4.1 Basic Usage
The steps for handling global exceptions are as follows:
- Create a global exception handling class: @ControllerAdvice/@RestControllerAdvice annotation (depending on the Controller layer using @Controller/@RestController, @Controller can jump to the corresponding page, Add @ ResponseBody can such as return JSON, and @ RestController equivalent to @ Controller + @ ResponseBody, no return JSON @ ResponseBody, but view the parser cannot resolve JSP and HTML pages)
- Create ExceptionHandler: specify the type of exception you want to handle with @ExceptionHandler
- 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); return error.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() {this(111," TestException "); } 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(" exception "); // 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 message: 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 the < > (um participant etCode (), "failure", um participant etMsg ()); }Copy the code
Using Result to encapsulate the return value, the test is 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"," successful "),FAILED("222"," FAILED "); 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 new Result<>(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<? extends HttpMessageConverter<? >> aClass) { return ! methodParameter.getParameterType().equals(Result.class); } @Override public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<? >> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { return new Result<>(o); }}Copy the code
Implement ResponseBodyAdvice:
- Supports method: Determines whether controller return method types are supported. Supports determines which types are wrapped and which are returned without wrapping
- BeforeBodyWrite method: When supports returns true, the data is wrapped so that when the data is returned, it does not need to be wrapped manually with Result, but returns directly to the User
Then modify 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<? extends HttpMessageConverter<? >> 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 directly down using IDEA to open, each optimization has done a submission, you can see the optimization process, if you like, remember to click a like and follow oh