This is the 29th day of my participation in the More Text Challenge. For more details, see more Text Challenge

Hibernate – Validator the most complete internationalization solution

In order to implement Hibernate-Validator internationalization almost to death, recently in the study of Hibernate-Validator and internationalization, in the wall and the wall to find a lot of time, may be because of the version of the update iteration, found the basic data can not be used. Oneself toss about for half a day, finally figure out, hereby record.

How to check parameters elegantly

Hibernate-validator is used in the following ways:

1. Native SpringBoot environment

Environmental information

  • SpringBoot : 2.3.8.RELEASE
  • Hibernate validator: 6.1.7. The Final

validation

Hibernate-validator is used to verify that the parameters requested in the previous paragraph meet the criteria

The controller layer:

@RestController
@RequestMapping("valid")
public class TestController {

    @PostMapping("/test")
    public Person test(@Valid @RequestBody Person person) {
        returnperson; }}Copy the code

Entity class

public class Person {
    @NotBlank()
    private String name;
    @Range(min = 2, max = 100)
    private Integer age;

    public String getName(a) {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge(a) {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString(a) {
        return "Person{" +
                "name='" + name + '\' ' +
                ", age=" + age +
                '} '; }}Copy the code

You can see from the entity class that there are two main validation conditions

  • Person. name cannot be empty
  • Person. age ranges from 2 to 100

If you call the interface directly at this point with data that does not meet the criteria, you will return a 404, and you will not see the validation error message, because Spring intercepted the validation exception and returned a 404

Not returning 404 requires a custom exception interceptor that handles the validation exception itself

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler({BindException.class})
    public String bindExceptionHandler(final BindException e) { String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.j oining("; "));
        return "{\"errors\":\"" + message + "\"}";
    }


    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String handler(final MethodArgumentNotValidException e) { String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.j oining("; "));
        return "{\"errors\":\"" + message + "\"}";
    }

    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(ConstraintViolationException.class)
    public String handler(final ConstraintViolationException e) {
        String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining("; "));
        return "{\"errors\":\"" + message + "\"}"; }}Copy the code

Validation exceptions are these three cases:

  • BindException
  • MethodArgumentNotValidException
  • ConstraintViolationException

After adding global exception handling, call the interface again:

There are two problems with this post-processing error message

  • How to internationalize and return to English and Chinese
  • The prompt message is not clear. Can you customize it

Internationalization problem

Spring provides i18N solution, with breakpoints can also find check failure is also go after this process, core classes for AcceptHeaderLocaleResolver

If you want to change it to another format or value of the header, you can override the preHandle method by inheriting the LocaleChangeInterceptor. If you want to change it to another format or value of the header, you can override the preHandle method by inheriting the LocaleChangeInterceptor. Equip custom bean usage

User-defined verification information

1. If internationalization is not considered

This is the easiest case to modify directly in the validation Bean

@notblank (message = "name must not be blank ")
private String name;
@Range(min = 2, max = 100, message = "年龄需要在2~100岁之间")
private Integer age;
Copy the code

2. Consider internationalization

In this case, you can’t simply write dead messages, you need to use hibernate- Valid internationalization

As you can see from the Hibernate source code, internationalization information is stored in a series of Properties files, which gives me the opportunity to override this by defining an internationalization file with the same name under resource

First, modify the bean’s message to bean el expression

@NotBlank(message = "{person.name}")
private String name;
@Range(min = 2, max = 100, message = "{person.age}")
private Integer age;
Copy the code

Then, define the key in your own internationalization file :(make sure to pay attention to the encoding)

After calling the interface:

If you need to customize the international file name and location, add the following configuration classes

@Configuration
public class MessageConfig {

    public ResourceBundleMessageSource getMessageSource(a) throws Exception {
        ResourceBundleMessageSource resourceBundle = new ResourceBundleMessageSource();
        resourceBundle.setDefaultEncoding(StandardCharsets.UTF_8.name());
        // Specify the internationalization file path
        resourceBundle.setBasenames("i18n/valid");
        return resourceBundle;
    }

    @Bean
    public Validator getValidator(a) throws Exception {
        LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
        validator.setValidationMessageSource(getMessageSource());
        return validator;
    }

    @Bean
    public MethodValidationPostProcessor validationPostProcessor(a) throws Exception {
        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
        // Specify the request validator
        processor.setValidator(getValidator());
        returnprocessor; }}Copy the code

The above configuration allows you to implement the validation message customization using the specified internationalization file

2. DropWizard project

DropWizard is a lightweight microservices development framework that encapsulates Hibernate-Valid itself, but does not have an internationalization solution

You need to use the hibernate-valid primitive internationalization method by setting the default language.

If you do not want the default, skip this section

Entity class:

public class Person {
    @NotBlank()
    private String name;
    @Range(min = 2, max = 100)
    private Integer age;

    public String getName(a) {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge(a) {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString(a) {
        return "Person{" +
                "name='" + name + '\' ' +
                ", age=" + age +
                '} '; }}Copy the code

The resource class

@Service
@Path("/test")
public class TestResource {

    @POST
    @Path("/test")
    @Consumes({APPLICATION_JSON})
    @Produces({APPLICATION_JSON})
    public String test(Person person, @Context final HttpServletRequest request) {
        Set<ConstraintViolation<Person>> validate = getValidator().validate(person);
        if(! validate.isEmpty()) { StringJoiner message =new StringJoiner(";");
            for (Object aValidate : validate) {
                message.add(((ConstraintViolation) aValidate).getMessage());
            }
            return "{\"result\":\"" + message.toString() + "\"}";
        }
        return "{\"result\":\"ok\"}"; }}Copy the code

1. You only need to customize the message body

This scenario simply requires modifying Message in the entity class

public class Person {
    @notblank (message = "name must not be empty ")
    private String name;
    @range (min = 2, Max = 100, message = "age = 2~100 ")
    private Integer age;
}
Copy the code

The calling interface will return a fixed message:

2. Only internationalize

This scenario uses hibernate-Valid’s built-in internationalization message, which controls the Language type with accept-Language in the header. For example, en-us, zh-cn

And then after you get the header on the back end, you set the language

Modifying an entity class

public class Person {
    @NotBlank()
    private String name;
    @Range(min = 2, max = 100)
    private Integer age;
}
Copy the code

Modifying a Resource Class

@POST
@Path("/test")
@Consumes({APPLICATION_JSON})
@Produces({APPLICATION_JSON})
public String test(Person person, @Context final HttpServletRequest request) {
    // Get the language in the header
    String language = request.getHeader("Accept-Language");
    Set<ConstraintViolation<Person>> validate = getValidator(language).validate(person);
    if(! validate.isEmpty()) { StringJoiner message =new StringJoiner(";");
        for (Object aValidate : validate) {
            message.add(((ConstraintViolation) aValidate).getMessage());
        }
        return "{\"result\":\"" + message.toString() + "\"}";
    }

    return "{\"result\":\"ok\"}";
}

private static Validator getValidator(String language) {
    // Set the language
    Locale.setDefault(language.contains("zh")? Locale.CHINA : Locale.ENGLISH);return Validation.byDefaultProvider().configure()
        .buildValidatorFactory()
        .getValidator();
}
Copy the code

Call interface effect:

Need to internationalize and customize the message body

This scenario requires adding configuration classes to specify internationalization files

private static Validator getValidator(String language) {
    Locale.setDefault(new Locale(language.contains("zh")?"zh" : "en"));
    return Validation.byDefaultProvider().configure()
        // Internationalize the file configuration
        .messageInterpolator(new MyResourceBundleMessageInterpolator(new PlatformResourceBundleLocator("i18n/valid")))
        .buildValidatorFactory()
        .getValidator();
}
Copy the code

In Java, the default encoding of the properties configuration file is ISO-8859-1, which does not support Chinese characters. Therefore, garbled characters are required to be transcoded.

public class MyResourceBundleMessageInterpolator extends ResourceBundleMessageInterpolator {
    public MyResourceBundleMessageInterpolator(ResourceBundleLocator userResourceBundleLocator) {
        super(userResourceBundleLocator);
    }

    @Override
    public String interpolate(String message, Context context) {
        String result = super.interpolate(message, context);
        try {
            return new String(result.getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8);
        } catch (Exception e) {
            returnresult; }}}Copy the code

Change the entity class message to the EL expression type:

public class Person {
    @NotBlank(message = "{person.name}")
    private String name;
    @Range(min = 2, max = 100, message = "{person.age}")
    private Integer age;
}
Copy the code

Add internationalization file under resouce

Use accept-language to control the Language type and invoke the interface effect:

3. SpringBoot project

Since Jersey is not the same as normal SpringMVC, it cannot be internationalized in the traditional way, so if you do not know or do not use Jersey in your project, you can skip this section

Entity class:

public class Person {
    @NotBlank()
    private String name;
    @Range(min = 2, max = 100)
    private Integer age;

    public String getName(a) {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge(a) {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString(a) {
        return "Person{" +
                "name='" + name + '\' ' +
                ", age=" + age +
                '} '; }}Copy the code

The resource class

@Component
@Path("/test")
public class TestResource {

    @Autowired
    private Validator validate;

    @POST
    @Path("/test")
    @Consumes({APPLICATION_JSON})
    @Produces({APPLICATION_JSON})
    public String test(Person person, @Context final HttpServletRequest request) {
        Set<ConstraintViolation<Person>> errorResults = validate.validate(person);
        if(! errorResults.isEmpty()) { StringJoiner message =new StringJoiner(";");
            for (Object result : errorResults) {
                message.add(((ConstraintViolation) result).getMessage());
            }
            return "{\"result\":\"" + message.toString() + "\"}";
        }
        return "{\"result\":\"ok\"}";
    }
Copy the code

1. You only need to customize the message body

This scenario simply requires modifying Message in the entity class

public class Person {
    @notblank (message = "name must not be empty ")
    private String name;
    @range (min = 2, Max = 100, message = "age = 2~100 ")
    private Integer age;
}
Copy the code

The calling interface will return a fixed message:

2. Only internationalize

This scenario uses hibernate-Valid’s own internationalization message and Spring’s I18N internationalization rules

The Language type is controlled by accept-language in the header. For example, en-us, zh-cn

Modifying an entity class

public class Person {
    @NotBlank()
    private String name;
    @Range(min = 2, max = 100)
    private Integer age;
}
Copy the code

Call interface effect:

Need to internationalize and customize the message body

This scenario requires adding configuration classes to specify internationalization files

@Configuration
public class MessageConfig {

    public MessageSource getMessageSource(a) throws Exception {
        ResourceBundleMessageSource rbms = new ResourceBundleMessageSource();
        rbms.setDefaultEncoding(StandardCharsets.UTF_8.toString());
        // Specify the internationalization file path
        rbms.setBasenames("i18n/valid");
        return rbms;
    }

    @Bean
    public LocalValidatorFactoryBean getValidator(a) throws Exception {
        LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
        validator.setValidationMessageSource(getMessageSource());
        returnvalidator; }}Copy the code

Change the entity class message to the EL expression type:

public class Person {
    @NotBlank(message = "{person.name}")
    private String name;
    @Range(min = 2, max = 100, message = "{person.age}")
    private Integer age;
}
Copy the code

Add internationalization files under resouce, making sure that the file encoding format is the same as in the configuration

The accept-language is used to control the Language type and invoke the interface effect:

summary

Hibernate-valid internationalization is relatively difficult. You need to go deep into the source code to understand how to configure it. There is no documentation even on the official website. Ha ha

Learn more skills now, in the future, less to say a word, you and I are refueling