SpringBoot 类级别自定义注解实现bean多属性联合校验&国际化

参考资料

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

前期准备

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

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

在这里插入图片描述

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

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());
    }
}

⏹国际化信息

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=中文系统

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=日本語システム

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

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();
    }
}

⏹前台

<!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">年龄from</label><input id="fromAge" type="number" /><br>
    <label for="toAge">to</label><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>

    <!-- 
		✅国际化语言展示区域
		✅#{}中放入国际化资源文件中的key
	 -->
    <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>

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

⏹待校验的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;
}

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

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);
        }
    }
}

⏹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);
    }
}

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

⏹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);
    }
}

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

三. 注意事项

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

一种是在配置文件中指定

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

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

@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;
    }
}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值