One article teaches you to implement custom Validator and error message internationalization configuration in SpringBoot

One article teaches you to implement custom Validator and error message internationalization configuration in SpringBoot

Code Ape Stone
One article teaches you to implement custom Validator and error message internationalization configuration in SpringBoot

This article uses examples to illustrate how to customize the Validator in Springboot and how to return internationalized error messages. Note that the code in this article must not be copied directly, it may cause major problems. Leave a suspense first, can readers and friends see what is wrong?

Project initialization


Download the template directly from the springboot official website, and directly add the implementation logic through the GreetingController in the example.


@RestController
public class GreetingController {

  private static final String template = "Hello, %s!";
  private final AtomicLong counter = new AtomicLong();

  @RequestMapping("/greeting")
  public Response<Greeting> greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
    if (!"tangleithu".equals(name)) {
      throw new BadRequestException("user.notFound");
    }
    return Response.ok(new Greeting(counter.incrementAndGet(), String.format(template, name)));
  }
}

The above code is directly derived from the official spring-guides demo, let me change it slightly. Under normal circumstances, the correct result can be returned:

# curl "localhost:8080/greeting?name=tangleithu&lang=en" 

{
    "code": 0,
    "data": {
        "content": "Hello, tangleithu!",
        "id": 9
    },
    "message": "success"
}

International demand


As a tall project, we definitely have overseas users, so we need an international configuration. Now let's simulate the business logic. Assuming that the input parameters have some verification functions, such as the above name parameter, if it is not equal to "tangleithu", an error will be returned directly. At the same time, the error message that you want to return needs to be internationalized, that is, the results returned are different in different language environments. For example, Chinese: "User does not exist." Corresponding English: "User does not exist.", and the corresponding German is..., forget it, I won't.
One article teaches you to implement custom Validator and error message internationalization configuration in SpringBoot
Use a picture to express that the desired effect is that users in different countries and regions (different languages) encounter the same error cause in the same business scenario, and have different translations. For example, if the parameter verification fails, the Http Status Code should return 400 and inform the reason of the error; in the specific service implementation, there may be other cases that need to return a specific error message. In this way, it can be easily managed in a unified manner.

Note: In actual business scenarios, the backend may only return error codes, and the specific display is translated by the frontend according to the key. However, in some more flexible scenarios (such as some app implementation schemes), error messages are likely to be returned directly by the backend interface. This article just uses a simple case to illustrate the entire process.

Unified error handling


We use AOP in Spring and use a ControllerAdvice to uniformly intercept this BadRequestException. The same is true for other Exceptions. The unified processing of exception information is not easy to cause security risks (I have encountered a large website before because of an exception in the background, which directly exposed specific SQL errors, and there are many sensitive table structures information). E.g:


@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BadRequestException.class)
    @ResponseBody
    public ResponseEntity handle(HttpServletRequest request, BadRequestException e){
        String i18message = getI18nMessage(e.getKey(), request);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(e.getCode(), i18message));
    }
}

International configuration


The specific error message translation can be directly configured in the resource file of the corresponding language. We can use a key to mark the error code in this specific exception information, and use different languages ​​in the resource file to define the specific error message that should be returned. For example, in the example in this article, Chinese and English are added. The corresponding directory structure is as follows:
One article teaches you to implement custom Validator and error message internationalization configuration in SpringBoot
At this time, we only need to return the corresponding error message in GlobalExceptionHandler according to whether the request source is Chinese or English.


private String getI18nMessage(String key, HttpServletRequest request) {
   try {
       return messageSource.getMessage(key, null, LanguaggeUtils.currentLocale(request));
   } catch (Exception e) {
       // log
       return key;
   }
}

There are many ways to obtain language information from the request source. For example, we can obtain Accept-Lanuage from the request header. Generally, the browser will bring this request header according to the user's settings, as shown in the following figure.

One article teaches you to implement custom Validator and error message internationalization configuration in SpringBoot
Or we can explicitly define some parameters such as lang. This article will not elaborate, let's simply use the lang parameter to define, as follows:


public class LanguaggeUtils {
    public static Locale currentLocale(HttpServletRequest request) {
        // 从 RequestHeader 等等获取相应的语言信息
        // 简单起见,直接从 queryParams 中取, 只模拟中英文
        String locale = request.getParameter("lang");
        if ("zh".equalsIgnoreCase(locale)) {
            return Locale.CHINA;
        } else {
            return Locale.ENGLISH;
        }
    }
}

In this way, the "internationalized" parameter return on the tall can be achieved with a few simple lines of code. Try the effect as follows:


#curl "localhost:8080/greeting?name=tanglei&lang=en" 
{
    "code": 400,
    "data": null,
    "message": "User does not exist."
}

#curl "localhost:8080/greeting?name=tanglei&lang=zh" 
{
    "code": 400,
    "data": null,
    "message": "没找到用户呢。"
}

Bean Validator


In fact, we have a simpler method for parameter verification like Form. That is with the help of the Validation framework that comes with SpringBoot, the corresponding implementation of this version used in this article is jakarta.validation-api. In fact, Bean Validation has corresponding standards and may have different specific implementations. Those interested in the standard can click here JSR #380 Bean Validation 2.0.

Going back to the demo of this article, suppose we need to pass a UserForm in our business logic and receive three parameters: age, name, and param. And check the input, among them, param has no specific meaning, just to illustrate the problem.


public class UserForm {
    @Min(value = 0, message = "validate.userform.age")
    @Max(value = 120, message = "validate.userform.age")
    private int age;

    @NotNull(message = "validate.userform.name.notEmpty")
    private String name;

    @CustomParam(message = "validate.userform.param.custom")
    private String param;
    ...
}

@RequestMapping("/user")
public Response<Greeting> createUser(@Valid @RequestBody UserForm userForm) {
    return Response.ok(new Greeting(counter.incrementAndGet(), String.format(template, userForm.getName())));
}

The code is as above, the above example only uses very simple constraints such as @Min, @Max, @NotNull, etc. The meaning can be seen by the name. For more constraint rules, you can directly see the corresponding source code javax.validation.constraints.xxx, for example, there are common email and other format verifications.

By default, after violating the corresponding constraints, the default output is more verbose, such as using this request curl -H "Content-Type: application/json" -d "{}" "localhost:8080/user", the corresponding output as follows:


{
    "error": "Bad Request",
    "errors": [
        {
            "arguments": [
                {
                    "arguments": null,
                    "code": "name",
                    "codes": [
                        "userForm.name",
                        "name"
                    ],
                    "defaultMessage": "name"
                }
            ],
            "bindingFailure": false,
            "code": "NotBlank",
            "codes": [
                "NotBlank.userForm.name",
                "NotBlank.name",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "defaultMessage": "must not be blank",
            "field": "name",
            "objectName": "userForm",
            "rejectedValue": null
        }
    ],
    "message": "Validation failed for object='userForm'. Error count: 1",
    "path": "/user",
    "status": 400,
    "timestamp": "2020-05-10T08:44:12.952+0000"
}

Let's draw a gourd according to the gourd, when debugging, add the specific exception thrown to the previous GlobalExceptionHandler, and then modify the default behavior.


@ExceptionHandler(BindException.class)
@ResponseBody
public ResponseEntity handle(HttpServletRequest request, BindException e){
   String key = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
   String i18message = getI18nMessage(key, request);
   return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(400, i18message));
}

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ResponseEntity handle(HttpServletRequest request, MethodArgumentNotValidException e){
   String key = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
   String i18message = getI18nMessage(key, request);
   return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(400, i18message));
}

@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public ResponseEntity handle(HttpServletRequest request, ConstraintViolationException e){
   String key = e.getConstraintViolations().iterator().next().getMessage();
   String i18message = getI18nMessage(key, request);
   return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(400, i18message));
}

After the improvement, after adding a custom handler, the returned information structure is consistent to facilitate unified front-end processing, and it is also quite concise:


{
    "code": 400,
    "data": null,
    "message": "validate.userform.name.notEmpty"
}

Combined with the parameter configuration through i18n explained earlier, it can be realized that when the verification fails, the error information is uniformly configured by the corresponding internationalized resource file.

Custom Validator

When the built-in conditions cannot be met, we hope to implement a custom Validator, such as the CustomParam mentioned above. How to do it We need an Annotation to facilitate the reference verification when corresponding to the Form. The specific implementation is as follows:


/**
 * @author tanglei
 * @date 2020/5/10
 */
@Documented
@Constraint(validatedBy = CustomValidator.class)
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomParam {
    String message() default "name.tanglei.www.validator.CustomArray.defaultMessage";

    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default { };

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
    @interface List {
        CustomParam[] value();
    }
}

A specific validator implementation class is also needed, which is associated with @Constraint(validatedBy = CustomValidator.class) above. This article is just a demo, so the specific parameter verification has no actual logical meaning. The following assumes that the input parameters are the same as "tanglei" and the verification passes, otherwise the user will be prompted to input an error.


public class CustomValidator implements ConstraintValidator<CustomParam, String> {
    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if (null == s || s.isEmpty()) {
            return true;
        }
        if (s.equals("tanglei")) {
            return true;
        } else {
            error(constraintValidatorContext, "Invalid params: " + s);
            return false;
        }
    }

    @Override
    public void initialize(CustomParam constraintAnnotation) {
    }

    private static void error(ConstraintValidatorContext context, String message) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
    }
}

Look at the effect, the input failed to pass the verification, and you are also prompted that the input parameter "xx" does not meet the conditions.
One article teaches you to implement custom Validator and error message internationalization configuration in SpringBoot

Does it feel perfect?

Note: There is a relatively hidden security hole above, please pay attention to it.

Note: There is a relatively hidden security hole above, please pay attention to it.

Note: There is a relatively hidden security hole above, please pay attention to it.

The important thing is said three times. Generally speaking, the ideas in this article are worth learning (see github for the corresponding code), but you must be careful not to copy it completely. The security vulnerability mentioned above is quite serious. Give a hint, that is, in the specific implementation of CustomValidator, do any friends know about it? Welcome to leave a message to discuss.

Guess you like

Origin blog.51cto.com/15072927/2607570
Recommended