• Spring 类级别多属性联合校验&国际化


    参考资料

    1. 👍自定义容器类型元素验证,类级别验证(多字段联合验证)
    2. 👍Springboot国际化i18n
    3. 👍Spring4 新特性 —— 集成 Bean Validation 1.1 (JSR-349) 到 SpringMVC

    前期准备

    ⏹配置文件,指定国际化文件存储的路径

    spring:
      messages:
      	# 指定国际化信息存储的路径(resources/i18n/messages开头的文件)
        basename: i18n/messages
        encoding: UTF-8
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    ⏹配置类,国际化相关配置

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.LocaleResolver;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
    import org.springframework.web.servlet.i18n.SessionLocaleResolver;
    import java.util.Locale;
    
    @Configuration
    public class InternationalConfig implements WebMvcConfigurer {
    
        // 默认解析器,用来设置当前会话默认的国际化语言
        @Bean
        public LocaleResolver localeResolver() {
            SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
            // 指定当前项目的默认语言是中文
            sessionLocaleResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
            return sessionLocaleResolver;
        }
    
        // 默认拦截器,用来指定切换国际化语言的参数名
        @Bean
        public LocaleChangeInterceptor localeChangeInterceptor() {
    
            LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
            /*
                设置国际化请求参数为language
                设置完成之后,URL中的 ?language=zh 表示读取国际化文件messages_zh.properties
             */
            localeChangeInterceptor.setParamName("language");
            return localeChangeInterceptor;
        }
    
        // 将我们自定义的国际化语言参数拦截器放入Spring MVC的默认配置中
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(localeChangeInterceptor());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    ⏹国际化信息

    messages_zh.properties

    # エラーE
    1001E=请输入{msgArgs}。
    1002E=请选择{msgArgs}。
    1003E=请输入{msgArgs}全角假名。
    1004E=输入的{msgArgs}日期格式不正确。
    1005E=请输入半角数字。
    1006E={msgArgs}最多不能超过{max}文字。
    1007E={0}と{1}的大小关系不正确。
    1008E=年龄fromと年龄to的大小关系不正确。
    
    # item
    1001Item=日期from
    1002Item=日期to
    1003Item=中文系统
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    messages_jp.properties

    # エラーE
    1001E={msgArgs}を入力してください。
    1002E={msgArgs}を選択してください。
    1003E={msgArgs}は全角カタカナまたは半角アルファベットで入力してください。
    1004E=入力された{msgArgs}日付は妥当ではありません。
    1005E=半角数字を入力してください。
    1006E={msgArgs}を{max}文字以内で入力してください。
    1007E={0}と{1}の大小関係が逆らいました。
    1008E=年齢fromと年齢toの大小関係が逆らいました。
    
    # item
    1001Item=日付from
    1002Item=日付to
    1003Item=日本語システム
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    ⏹国际化消息获取共通方法

    import org.springframework.context.MessageSource;
    import org.springframework.context.i18n.LocaleContextHolder;
    import org.springframework.context.support.ReloadableResourceBundleMessageSource;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    import java.nio.charset.StandardCharsets;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Locale;
    
    @Component
    public class LocaleMessageSourceService {
    
        @Resource
        private MessageSource messageSource;
    
        /**
         * 根据code获取对应的message
         * @param code 信息code
         * @return message
         */
        public String getMessage(String code) {
            return getMessage(code, null);
        }
    
        /**
         * 根据code和参数获取对应的message
         * @param code 信息code
         * @param args 参数
         * @return message
         */
        public String getMessage(String code, Object[] args) {
            return getMessage(code, args, "");
        }
    
        /**
         * 根据code和参数获取对应的message
         * @param code 信息code
         * @param args 参数
         * @param defaultMessage 默认信息
         * @return message
         */
        public String getMessage(String code, Object[] args, String defaultMessage) {
    
            // 这里使用比较方便的方法,不依赖request.
            Locale locale = LocaleContextHolder.getLocale();
            return messageSource.getMessage(code, args, defaultMessage, locale);
        }
    
        /**
         * 当不在配置文件中指定messages.basename,而在配置类中指定的时候
         * MessageSource会无法自动注入,此时使用此方法获取message
         * @param code 信息code
         * @param args 参数
         * @return message
         */
        public String getMsg(String code, Object[] args) {
    
            ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
            messageSource.setCacheSeconds(-1);
            messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
    
            // 设置国际化信息文件存在的路径,i18n文件夹下messages开头的文件
            messageSource.setBasenames("/i18n/messages");
    
            String message = "";
            try {
                message = messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
            } catch (Exception e) {
                e.printStackTrace();
                return "";
            }
    
            return message;
        }
    
        /**
         * 根据code获取页面的项目名称数组
         * @param args 信息code
         * @return 项目名称数组
         */
        public Object[] getItemName(String...args) {
    
            List<Object> objects = new ArrayList<>();
            for (String arg : args) {
                objects.add(getMessage(arg));
            }
            return objects.toArray();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    ⏹前台

    DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <script type="text/javascript" th:src="@{/js/public/jquery-3.6.0.min.js}">script>
        <script type="text/javascript" th:src="@{/js/common/common.js}">script>
        <title>国际化校验title>
    head>
    <body>
        <button id="btn1">发送请求,通过自定义类注解进行国际化校验button>
        <button id="btn2">发送请求,通过@ScriptAssert进行非国际化校验button>
        <hr>
    
        <label for="fromAge">年龄fromlabel><input id="fromAge" type="number" /><br>
        <label for="toAge">tolabel><input id="toAge" type="number" /><br>
        <hr>
    
        <label for="fromDate">开始日期label><input id="fromDate" type="number" /><br>
        <label for="toDate">结束日期label><input id="toDate" type="number" /><br>
        <hr>
        <hr>
    
        
        <div>[[#{1003Item}]]div>
    body>
    <script>
        let language1 = false;
        $("#btn1").click(() => {
    
            const param = {
                fromAge: $("#fromAge").val(),
                toAge: $("#toAge").val(),
                fromDate: $("#fromDate").val(),
                toDate: $("#toDate").val(),
            };
    		
    		// 切换语言状态,中日文语言切换
            language1 = !language1;
            const url = `http://localhost:8080/test6/validateCustomAnnotation?language=${language1 ? 'zh' : 'jp'}`;
            doAjax(url, param, function(data) {
                console.log(data);
            });
        });
    
        let language2 = false;
        $("#btn2").click(() => {
    
            const param = {
                fromAge: $("#fromAge").val(),
                toAge: $("#toAge").val(),
                fromDate: $("#fromDate").val(),
                toDate: $("#toDate").val(),
                newPassword: "110120",
                oldPassword: "110120"
            };
    
            language2 = !language2;
            const url = `http://localhost:8080/test6/validateScriptAssert?language=${language2 ? 'zh' : 'jp'}`;
            doAjax(url, param, function(data) {
                console.log(data);
            });
        });
    script>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    一. 自定义类注解&国际化校验

    ⏹待校验的form

    import com.example.jmw.common.validation.ValidateNotEmpty;
    import com.example.jmw.form.validation.ValidTest6Form1;
    import lombok.Data;
    
    @Data
    // 自定义的类注解,用于类中的多个属性联合校验
    @ValidTest6Form1
    public class Test6Form1 {
    	
    	// 属性校验注解
        @ValidateNotEmpty(msgArgs = "id")
        private String id;
    
        private Integer fromAge;
    
        private Integer toAge;
    
        private Integer fromDate;
    
        private Integer toDate;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    ⏹自定义一个注解,该注解作用于类上

    import com.example.jmw.common.utils.LocaleMessageSourceService;
    import com.example.jmw.form.Test6Form1;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.util.ObjectUtils;
    
    import javax.validation.Constraint;
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    import javax.validation.Payload;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    import java.util.ArrayList;
    import java.util.List;
    
    @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = {ValidTest6Form1.ValidTest6Form1Validator.class})
    public @interface ValidTest6Form1 {
    
        String message() default "";
    
        Class<?>[] groups() default {};
    	
        Class<? extends Payload>[] payload() default {};
    	
    	// 内部类
        class ValidTest6Form1Validator implements ConstraintValidator<ValidTest6Form1, Test6Form1> {
    		
    		// 注入自定义的国际化信息获取方法
            @Autowired
            private LocaleMessageSourceService localeMessageSourceService;
    
            @Override
            public void initialize(ValidTest6Form1 constraintAnnotation) {
            }
    
            @Override
            public boolean isValid(Test6Form1 value, ConstraintValidatorContext context) {
    
                if (ObjectUtils.isEmpty(value)) {
                    return true;
                }
    
                // 取消默认的校验提示消息
                context.disableDefaultConstraintViolation();
    
                List<Boolean> checkResultList = new ArrayList<>();
    
                // 校验from日期和to日期的大小关系
                Integer fromDate = value.getFromDate();
                Integer toDate = value.getToDate();
                if (!ObjectUtils.isEmpty(fromDate) && !ObjectUtils.isEmpty(toDate) && fromDate > toDate) {
    
                    // 获取from日期和to日期对应的国际化项目名称
                    Object[] args = localeMessageSourceService.getItemName("1001Item", "1002Item");
                    // 获取校验信息
                    String message = localeMessageSourceService.getMessage("1007E", args);
                    // 将校验信息添加到模板中
                    context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
                    
                    checkResultList.add(false);
                }
    
                // 校验from年龄和to年龄的大小关系
                Integer fromAge = value.getFromAge();
                Integer toAge = value.getToAge();
                if (!ObjectUtils.isEmpty(fromAge) && !ObjectUtils.isEmpty(toAge) && fromAge > toAge) {
    
                    // 如果校验信息不需要传参数的话,可直接写国际化文件中对应的code
                    context.buildConstraintViolationWithTemplate("{1008E}")
                            .addConstraintViolation();
    
                    checkResultList.add(false);
                }
    
                return !checkResultList.contains(false);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    ⏹controller层校验

    @Controller
    @RequestMapping("/test6")
    public class Test6Controller {
    	
    	// 注入校验类
        @Resource
        private LocalValidatorFactoryBean validator;
    
        @GetMapping("/init")
        public ModelAndView init() {
    
            ModelAndView modelAndView = new ModelAndView();
            modelAndView.setViewName("test6");
            return  modelAndView;
        }
    
        @PostMapping("/validateCustomAnnotation")
        @ResponseBody
        public void validateCustomAnnotation(@RequestBody Test6Form1 form) {
    
            Set<ConstraintViolation<Test6Form1>> validate = validator.validate(form);
    
            for (ConstraintViolation<Test6Form1> bean : validate) {
    
                // 获取当前的校验信息
                String message = bean.getMessage();
                System.out.println(message);
            }
            System.out.println(validate);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    ⏹效果
    在这里插入图片描述
    在这里插入图片描述
    💪点击发送请求,通过自定义类注解进行国际化校验,此时返回中文校验信息
    在这里插入图片描述
    💪再次点击发送请求,通过自定义类注解进行国际化校验,此时返回日文校验信息
    在这里插入图片描述
    💪上述两次点击的请求url
    在这里插入图片描述


    二. @ScriptAssert内置校验注解

    • Bean Validation没有内置任何类级别的注解,但Hibernate-Validator却对此提供了增强,弥补了其不足。
    • @ScriptAssert就是Hibernate-Validator内置的一个非常强大的、可以用于类级别验证注解。
    • 只能用于简单的校验

    ⏹待校验的form

    import lombok.Data;
    import org.hibernate.validator.constraints.ScriptAssert;
    
    @Data
    // 当有多个业务校验的时候使用 .List()
    @ScriptAssert.List({
            /*
             * 使用javascript脚本来完成校验
             * 默认_this相当于当前类对象
             * 可以通过alias取一个别名
             */
            @ScriptAssert(lang = "javascript"
                    , alias = "_"
                    , script = "_.fromAge <= _.toAge"
                    , message="From年龄值不能比to年龄大"),
            // 因为使用javascript脚本来完成校验,因此比较相等才可以使用js中的 !==
            @ScriptAssert(lang = "javascript"
                    , script = "_this.oldPassword !== _this.newPassword"
                    , message="新旧密码不能相同"),
            @ScriptAssert(lang = "javascript"
                    , script = "_this.fromDate <= _this.tomDate"
                    , message="From日期不能比to日期大"),
    })
    public class Test6Form2 {
    
        private Integer fromAge;
    
        private Integer toAge;
    
        private Integer fromDate;
    
        private Integer toDate;
    
        private String oldPassword;
    
        private String newPassword;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    ⏹controller层校验

    @Controller
    @RequestMapping("/test6")
    public class Test6Controller {
    
        @Resource
        private LocalValidatorFactoryBean validator;
    
        @GetMapping("/init")
        public ModelAndView init() {
    
            ModelAndView modelAndView = new ModelAndView();
            modelAndView.setViewName("test6");
            return  modelAndView;
        }
        
        @PostMapping("/validateScriptAssert")
        @ResponseBody
        public void validateScriptAssert(@RequestBody Test6Form2 form) {
    
            Set<ConstraintViolation<Test6Form2>> validate = validator.validate(form);
    
            for (ConstraintViolation<Test6Form2> bean : validate) {
    
                // 获取当前的校验信息
                String message = bean.getMessage();
                System.out.println(message);
            }
            System.out.println(validate);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    ⏹效果
    在这里插入图片描述

    三. 注意事项

    指定国际化文件存储的路径的方式一共有两种

    一种是在配置文件中指定

    spring:
      messages:
      	# 指定国际化信息存储的路径(resources/i18n/messages开头的文件)
        basename: i18n/messages
        encoding: UTF-8
    
    • 1
    • 2
    • 3
    • 4
    • 5

    另外一种是通过配置类的方式指定

    @Configuration
    public class InternationalConfig implements WebMvcConfigurer {
    
        // 自定义国际化环境下要显示的校验消息
        @Bean
        public LocalValidatorFactoryBean localValidatorFactoryBean() {
    
            LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
    
            // 使用Spring加载国际化资源文件
            ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
            messageSource.setBasename("messages");
            messageSource.setDefaultEncoding("UTF-8");
    
            localValidatorFactoryBean.setValidationMessageSource(messageSource);
            return localValidatorFactoryBean;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    ❗❗❗如果使用配置类的方式的话,MessageSource无法自动注入,如下图所示
    在这里插入图片描述

  • 相关阅读:
    java 常见算法
    mybatisPlus-sample
    ceph 数据恢复和回填速度 重建osd 加快数据恢复
    Spring5框架
    ubuntu20.04安装cv2
    Unity 2D 游戏学习笔记(5)
    #Microsoft Edge功能测评!# 关于Edge浏览器以及插件推荐
    【Linux】:Ubuntu 16.04安装samba服务器及配置
    【SpringCloud微服务项目实战-mall4cloud项目(2)】——mall4cloud-gateway
    考研数据结构大题整合_组一(ZYL组)_做题版
  • 原文地址:https://blog.csdn.net/feyehong/article/details/126258869