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: Yes
Hibernate Validator
A variety of annotations, quick failure modes, grouping, group sequences and custom annotations /Validator
- Exception handling: Involved
ControllerAdvice
/@RestControllerAdvice
As 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:
@Null
The 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 stringinteger
The decimal number specifies the maximum number of decimal placesfraction
position@Past
The: element is a past date@Future
The: element is a future date@Pattern
The: element must conform to the regular expression
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 (current latest)6.1.5
Version 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 fornull
And the length has to be greater than 0, exceptString
Outside, theCollection
/Map
The/array also works@NotBlank
: only forString
, not fornull
And 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 @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 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 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(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 classvalidate
: whensupports
returntrue
After, validate the given objecto
, when an error occurs, toerrors
Registration 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 interfaceBindingResult
However, 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 beforeBindingResult
In 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
/@RestControllerAdvice
Annotations (depending on which control layer is used@Controller
/@RestController
.@Controller
You can jump to the corresponding page and return toJSON
Such as add@ResponseBody
Can, but@RestController
The equivalent of@Controller
+@ResponseBody
To return toJSON
Don’t need to add@ResponseBody
, but the view parser cannot parsejsp
As well ashtml
Page) - Create exception handling methods: add
@ExceptionHandler
Specify 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
supports
Method: Check whether controller return method type is supportedsupports
Determine which types need to be wrapped and which are returned withoutbeforeBodyWrite
Methods: whensupports
returntrue
After, the data is wrapped so that it is not needed when the data is returnedResult<User>
Manually wrap, but return directlyUser
Can 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