SpringBoot @InitBinder注解实现Bean国际化校验

参考资料

  1. 参考: 妥当性チェックのエラーメッセージ出力方法 (需翻墙)
  2. springMVC之@InitBinder的用法1
  3. springMVC之@InitBinder的用法2
  4. springMVC之@InitBinder 和 Validator
  5. Spring MVCにおけるフォームバリデーションの適用事例【後編】


一. 前期准备

1.1 自定义校验注解

import javax.validation.Constraint;
import javax.validation.OverridesAttribute;
import javax.validation.Payload;
import javax.validation.constraints.Size;
import javax.validation.ReportAsSingleViolation;
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
@Documented
@Constraint(validatedBy = {})
@ReportAsSingleViolation
@Size
public @interface ValidateSize {

    String msgArgs() default "";

    String message() default "{1006E}";

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

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

    // 覆盖重写@Size注解中的属性
    @OverridesAttribute(constraint = Size.class, name = "min")
    int min() default 0;

    @OverridesAttribute(constraint = Size.class, name = "max")
    int max() default Integer.MAX_VALUE;
}
import javax.validation.Constraint;
import javax.validation.constraints.NotEmpty;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import java.lang.annotation.*;

@Documented
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
@NotEmpty
@ReportAsSingleViolation
public @interface ValidateNotEmpty {

    String msgArgs() default "";

	String message() default "{1001E}";

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

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

1.2 国际化资源文件

⏹messages_zh.properties

1001E=请输入{msgArgs}。
1007E={0}和{1}的大小关系不正确。

⏹messages_ja.properties

1001E={msgArgs}を入力してください。
1007E={0}と{1}の大小関係が逆らいました。

⏹置于i18n文件夹下

在这里插入图片描述

1.3 application配置文件

spring:
  messages:
  	# 指定国际化文件所在目录和文件前缀
    basename: i18n/messages
    encoding: UTF-8

1.4 国际化配置文件

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 javax.annotation.Resource;
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.5 待校验Bean

import lombok.Data;

import javax.validation.groups.Default;

@Data
public class Test4Entity {

    @ValidateNotEmpty(msgArgs = "ID项目", groups = {Default.class})
    private String id;

    @ValidateSize(msgArgs = "地址项目", max = 6, groups = {Default.class})
    private String address;

    @ValidateSize(msgArgs = "兴趣项目", max = 5, groups = {Default.class})
    private String hobby;
}
import lombok.Data;

import javax.validation.Valid;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;

@Data
public class Test16Form {

    @ValidateNotEmpty(msgArgs = "姓名")
    private String name;

    private Date birthday;

    private BigDecimal money;

    private Integer fromNumber;

    private Integer toNumber;
	
	// 校验List集合
    @Valid
    private List<Test4Entity> tableList;
}

二. 实现Validator接口

import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

@Component
public class FromToValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {

        // 只支持指定Bean类型的校验
        return Test16Form.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {

        Test16Form form = (Test16Form) target;
		
		// 获取from和to的数字
        Integer fromNumber = form.getFromNumber();
        Integer toNumber = form.getToNumber();
		
		// 有任何一方为空,就不行校验
        if (ObjectUtils.isEmpty(fromNumber) || ObjectUtils.isEmpty(toNumber)) {
            return;
        }

        // 模拟从缓存或者session或者数据库中获取国际化消息
        Map<String, Object[]> languageErrorParamMap = new HashMap<String, Object[]>() {
            {
                put("zh", new Object[] { "开始数字", "结束数字" });
                put("ja", new Object[] { "スタートの数字", "エンドの数字" });
            }
        };

        // 获取当前设置地区的语言
        Locale locale = LocaleContextHolder.getLocale();
        String language = locale.getLanguage();
        Object[] errorParam = languageErrorParamMap.get(language);
		
		// 当from数字 大于 to数字的时候,进行业务校验
        if (fromNumber > toNumber) {
        	/*
        		参数1: bean中被校验住的属性名
        		参数2: 国际化资源文件中的key
        		参数3: error消息的参数
        		参数4: 默认消息
			*/
            errors.rejectValue("fromNumber", "1007E", errorParam, "");
        }
    }
}

三. @InitBinder校验Get请求

3.1 前端

⏹test16.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <button id="getBtn">发送get请求</button>
</body>
<script type="text/javascript" th:src="@{/js/public/jquery-3.6.0.min.js}"></script>
<script>
    let languageFlag = false;

    $("#getBtn").click(function() {

        languageFlag = !languageFlag;

        const urlSearchParams = new URLSearchParams();
        urlSearchParams.append("money", "10000");
        urlSearchParams.append("fromNumber", "20");
        urlSearchParams.append("toNumber", "10");
        urlSearchParams.append("language", languageFlag ? "zh" : "ja");

        const url = `/test16/receiveGet?${urlSearchParams.toString()}`;
        $.ajax({
            url,
            type: 'GET',
            success: function (data, status, xhr) {
                console.log("请求成功");
                console.log(data);
            },
            error: function (xhr, status, error) {
                console.warn("请求失败");
                // 获取后台全局异常捕获中返回的json响应
                const errorJson = xhr.responseJSON;
                console.log(errorJson);
            }
        });
    });
</script>
</html>

3.2 controller层

@Controller
@RequestMapping("/test16")
public class Test16Controller {

	// 注入我们自定义的校验器
    @Resource
    private FromToValidator fromToValidator;

    @InitBinder
    public void initBinder(WebDataBinder binder) {

        // 去除字符串前后的空格
        binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
		// 使用我们自定义的校验器
        binder.addValidators(fromToValidator);
    }

    @GetMapping("/init")
    public ModelAndView init() {

        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("test16");
        return modelAndView;
    }
	
	// 校验
    @GetMapping("/receiveGet")
    @ResponseBody
    public void receiveGet(@Validated Test16Form form) {

        System.out.println(form);
    }
}

3.3 全局捕获BindException异常

  • Get请求被被校验住之后,会抛出BindException异常
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @Resource
    private MessageSource messageSource;

    @ExceptionHandler(BindException.class)
    // 通过注解指定了响应的状态码,前台$.ajax会在error函数的xhr响应中接收错误json
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public List<Map<String, String>> BindExceptionHandle(BindException errors) {
		
		// 存放所有error信息的List
        List<Map<String, String>> errorList = new ArrayList<>();
		
        for(FieldError err : errors.getFieldErrors()){
			
			// 根据当前的FieldError对象从国际化资源文件中获取信息
            String msg = this.messageSource.getMessage(err, LocaleContextHolder.getLocale());
			
			// 封装错误信息
            Map<String, String> errorMap = new HashMap<String, String>() {
                {
                    put("field", err.getField());
                    put("msg", msg);

                }
            };
            errorList.add(errorMap);
        }

        return errorList;
    }
}

3.4 效果

在这里插入图片描述

四. @InitBinder校验Post请求

4.1 前端

⏹test16.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <button id="postBtn">发送post请求</button><br>
</div>
</body>
<script type="text/javascript" th:src="@{/js/public/jquery-3.6.0.min.js}"></script>
<script>

    let languageFlag = false;

    $("#postBtn").click(function() {

        languageFlag = !languageFlag;

        const urlSearchParams = new URLSearchParams();
        urlSearchParams.append("language", languageFlag ? "zh" : "ja");
		
		// 待校验的list对象
        const tableList = [
            {
                id: null,
                address: '测试address123',
                hobby: '测试hobby123'
            },
            {
                id: 110,
                address: '测试',
                hobby: '测试AAAAAAAAAA'
            },
            {
                id: 120
            }
        ];
        
        // 待校验的bean对象
        const paramObj = {
            money: "10000",
            fromNumber: "20",
            toNumber: "10",
            tableList
        };

        $.ajax({
            url: `/test16/receivePost?${urlSearchParams.toString()}`,
            type: 'POST',
            data: JSON.stringify(paramObj),
            // 指定向后台提交json数据
            contentType : 'application/json;charset=utf-8',
            // 指定后台返回json数据给前台
            dataType: 'json',
            success: function (data, status, xhr) {
                console.log("请求成功");
                console.log(data);
            },
            error: function (xhr, status, error) {
                console.warn("请求失败");
                const errorJson = xhr.responseJSON;
                console.log(errorJson);
            }
        });
    });

</script>
</html>

4.2 controller层

@Controller
@RequestMapping("/test16")
public class Test16Controller {

	// 注入我们自定义的校验器
    @Resource
    private FromToValidator fromToValidator;

    @InitBinder
    public void initBinder(WebDataBinder binder) {

        // 去除字符串前后的空格
        binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
		// 使用我们自定义的校验器
        binder.addValidators(fromToValidator);
    }

    @GetMapping("/init")
    public ModelAndView init() {

        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("test16");
        return modelAndView;
    }
	
	// 校验
    @PostMapping("/receivePost")
    @ResponseBody
    public void receivePost(@RequestBody @Validated Test16Form form) {

        System.out.println(form);
    }
}

4.3 全局捕获MethodArgumentNotValidException异常

  • Post请求被被校验住之后,会抛出MethodArgumentNotValidException异常
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @Resource
    private HttpServletResponse response;

    @Resource
    private MessageSource messageSource;

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public List<Map<String, String>> HandleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
		
		// 存放所有error信息的List
        List<Map<String, String>> errorList = new ArrayList<>();

		List<FieldError> errors = ex.getFieldErrors();
        for(FieldError err : errors){

            // 根据当前的FieldError对象从国际化资源文件中获取信息
            String msg = this.messageSource.getMessage(err, LocaleContextHolder.getLocale());
            Map<String, String> errorMap = new HashMap<String, String>() {
                {
                    put("field", err.getField());
                    put("msg", msg);

                }
            };
            errorList.add(errorMap);
        }
        
		// 通过response对象指定了响应的状态码,前台$.ajax会在error函数的xhr响应中接收错误json
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        return errorList;
}

4.4 效果

在这里插入图片描述

五. 注意事项

当前端传入的数据无法通过校验规则的时候,会抛出相应的异常。
我们可通过FieldError对象getMessage方法中获取出相应的错误信息

String msg = this.messageSource.getMessage(err, LocaleContextHolder.getLocale());

错误消息是根据FieldError的code,从国际化资源文件中获取,通过code获取错误消息需要遵循如下的优先顺规则

  1. errorCode.对象名.属性名
  2. errorCode.属性名
  3. errorCode.类型
  4. errorCode

1为最优先,4的优先顺最低

在这里插入图片描述
也就是说,如果国际化资源文件中有如下errorCode的话,会显示优先顺最高的

1007E={0}和{1}的大小关系不正确。
1007E.test16Form.fromNumber=我是测试内容,我的优先顺最高

在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值