SpringBoot Bean校验&国际化校验

参考资料
👍👍👍0. Spring Validation最佳实践及其实现原理,参数校验没那么简单!
https://juejin.cn/post/6856541106626363399
✅0. Hibernate-Validator验证
https://www.yourbatman.cn/tags/Hibernate-Validator/
✅1. @Validated和@Valid的区别?教你使用它完成Controller参数校验(含级联属性校验)以及原理分析【享学Spring】
https://blog.csdn.net/f641385712/article/details/97621783
✅2. Bean Validation完结篇:你必须关注的边边角角(约束级联、自定义约束、自定义校验器、国际化失败消息…)【享学Spring】
https://blog.csdn.net/f641385712/article/details/97968775
✅3. 深入了解数据校验(Bean Validation):从深处去掌握@Valid的作用(级联校验)以及常用约束注解的解释说明【享学Java】
https://blog.csdn.net/f641385712/article/details/97042906
✅4. Spring方法级别数据校验:@Validated + MethodValidationPostProcessor优雅的完成数据校验动作【享学Spring】
https://blog.csdn.net/f641385712/article/details/97402946
✅5. 详述Spring对Bean Validation支持的核心API:Validator、SmartValidator、LocalValidatorFactoryBean…【享学Spring】
https://blog.csdn.net/f641385712/article/details/97270786
✅6. SpringBoot中BeanValidation数据校验与优雅处理详解
https://blog.csdn.net/Sky_QiaoBa_Sum/article/details/109767344
✅7. 在 Spring Boot 使用Bean Validation 完全指南
https://blog.csdn.net/Xiaowu_First/article/details/121444585
✅8. java自定义校验注解
https://dandelioncloud.cn/article/details/1497043194285232129
✅👍👍👍9. Springboot国际化i18n
https://www.dandelioncloud.cn/article/details/1525696496859365377
✅10. 手把手教你利用Spring Boot实现各种参数校验
https://www.jianshu.com/p/bffa168c29e8
✅11. 自定义容器类型元素验证,类级别验证(多字段联合验证)
https://blog.csdn.net/f641385712/article/details/109270066
✅12. 深入了解数据校验:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例【享学Java】
https://blog.csdn.net/f641385712/article/details/96638596
✅13. 让Controller支持对平铺参数执行数据校验(默认Spring MVC使用@Valid
https://blog.csdn.net/f641385712/article/details/97621755


一. 前期准备

1.1 ⏹自定义校验注解

⭕非空校验

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 "";
	
	// {1001E}所对应的错误消息存储在 XXX.properties 中
	String message() default "{1001E}";

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

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

⭕日期格式校验

import org.springframework.util.ObjectUtils;

import javax.validation.*;
import java.lang.annotation.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;

@Documented
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {ValidateDateString.StrictDateStringValidator.class})
public @interface ValidateDateString {

    String msgArgs() default "";
    String pattern() default "yyyy/MM/dd";
    String message() default "{1004E}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    class StrictDateStringValidator implements ConstraintValidator<ValidateDateString, String> {

        private String pattern;

        @Override
        public void initialize(ValidateDateString validStrictDateString) {
            this.pattern = validStrictDateString.pattern();
        }

        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {

            try {

                if (ObjectUtils.isEmpty(value)) {
                    return true;
                }

                SimpleDateFormat sdf = new SimpleDateFormat(pattern);
                // 设置非严格解析日期
                sdf.setLenient(false);
                sdf.parse(value);
            } catch (ParseException e) {
                return false;
            }
            return true;
        }
    }
}

⭕标记校验信息先后顺序的注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@ReportAsSingleViolation
public @interface CheckMsgOrder {

    int value() default 0;
}

1.2 ⏹ValidationMessages.properties校验信息

当Bean中的属性校验失败的时候,默认去ValidationMessages.properties文件中匹配错误信息.如果不想命名为ValidationMessages,需要额外的配置.此部分参照国际化校验错误消息的配置.

# 确认消息
1008Q=确定要放弃编辑的内容吗?
1019Q=确定要取消预约吗?
1021Q=确定要删除吗?

# 错误消息
1001E=请输入{msgArgs}。
1002E=请选择{msgArgs}。
1003E=请输入{msgArgs}全角假名。
1004E=输入的{msgArgs}日期格式不正确。
1005E=请输入半角数字。
1006E={msgArgs}最多不能超过{max}文字。

1.3 ⏹封装校验错误信息的实体类

@JsonIgnore注解标记的属性,不会携带值返回到前台

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;

@Data
public class ErrorItemEntity implements Serializable {

    // 错误信息ID
    @JsonIgnore
    private String errorMessageId;

    // 错误信息
    private String errorMessage;

    // 前台错误项目ID
    private String errorItemId;

    // 前台输入的值
    @JsonIgnore
    private Object rejectValue;

    // 错误的行号
    @JsonIgnore
    private String errorRowNo;

    // 错误的ID列表
    private List<String> errorIdList;

    public static ErrorItemEntity of(String message) {
        return ErrorItemEntity.of(message, "", "");
    }

    public static ErrorItemEntity of(String message, String errorItemId) {
        return ErrorItemEntity.of(message, errorItemId, "");
    }

    public static ErrorItemEntity of(String errorMessage, String errorItemId, Object rejectValue) {
        ErrorItemEntity entity = new ErrorItemEntity();
        entity.errorMessage = errorMessage;
        entity.errorItemId = errorItemId;
        entity.rejectValue = rejectValue;
        return entity;
    }

    public static ErrorItemEntity of(String errorMessage, String errorRowNo, List<String> errorIdList) {
        ErrorItemEntity entity = new ErrorItemEntity();
        entity.errorMessage = errorMessage;
        entity.errorRowNo = errorRowNo;
        entity.errorIdList = errorIdList;
        return entity;
    }
}

1.4 ⏹返回结果封装类

前台Ajax请求后台时,统一使用下面的实体类返回数据到前台

@Data
@EqualsAndHashCode(callSuper = false)
public class ResultEntity implements Serializable {

	private boolean result;

	private List<ErrorItemEntity> errors = new ArrayList<>();

	private Object entity;


	public ResultEntity() {
	}

	/**
	 * 构造函数
	 * 
	 * @param result 处理结果,true或者false
	 * @param errors 错误信息
	 * @param entity 返回给前台的信息
	 */
	private ResultEntity(boolean result, List<ErrorItemEntity> errors, Object entity) {
		this.result = result;
		this.errors = (errors == null) ? new ArrayList<>() : errors;
		this.entity = entity;
	}

	/**
	 * 请求成功,并且返回值给前台
	 * 
	 * @param entity 返回给前台的信息
	 */
	public static ResultEntity ok(Object entity) {
		return new ResultEntity(true, null, entity);
	}

	/**
	 * 请求成功,不返回值给前台
	 */
	public static ResultEntity ok() {
		return new ResultEntity(true, null, null);
	}

	/**
	 * 请求失败
	 */
	public static ResultEntity ng() {
		return new ResultEntity(false, null, null);
	}
}

1.5 ⏹自定义校验异常

import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = true)
public class ValidationException extends RuntimeException {

    // 错误信息
    private List<ErrorItemEntity> errors;

    /**
     * 生成ValidationException异常对象
     *
     * @param bindingResult 注解校验结果
     */
    public ValidationException(BindingResult bindingResult) {
        super();
        this.errors = ErrorInfoUtils.getErrorResult(bindingResult);
    }

    /**
     * 生成ValidationException异常对象
     *
     * @param errors 业务异常信息
     */
    public ValidationException(List<ErrorItemEntity> errors) {
        super();
        this.errors = errors;
    }
}
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;

import java.util.ArrayList;
import java.util.List;

public final class ErrorInfoUtils {

    private ErrorInfoUtils() { }

    /**
     * 从注解校验结果中获取错误信息
     *
     * @param bindingResult 注解校验结果
     * @return 错误信息
     */
    public static List<ErrorItemEntity> getErrorResult(BindingResult bindingResult) {

        List<ErrorItemEntity> errors = new ArrayList<>();
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            errors.add(ErrorItemEntity.of(fieldError.getDefaultMessage(), fieldError.getField(), fieldError.getRejectedValue()));
        }
        return errors;
    }
}

1.6 ⏹全局异常捕获

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
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.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;

@ControllerAdvice
public class GlobalExceptionHandler {

    @Autowired
    private HttpServletRequest request;
    
	@Autowired
    private HttpServletResponse response;
	
	/*
		当Controller层使用下面这种写法校验数据时
		commonValidate2(@RequestBody @Validated Test4Form form)
		由于没有使用BindingResult接收注解的check结果,
		此时会抛出MethodArgumentNotValidException异常,
		我们在全局异常捕获类,对此异常尽心处理
	*/
	@ExceptionHandler(MethodArgumentNotValidException.class)
    public ModelAndView HandleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {

        BindingResult bindingResult = ex.getBindingResult();
        List<ErrorItemEntity> errorResult = ErrorInfoUtils.getErrorResult(bindingResult);

        // 校验失败,返回400的状态码
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);

        // 自定义结果封装类
        ResultEntity resultEntity = ResultEntity.ng();
        resultEntity.setErrors(errorResult);

        return this.commonErrorHandle(resultEntity);
    }
	
	// 捕获我们收到抛出的ValidationException异常,进行处理
	@ExceptionHandler(ValidationException.class)
    // 通过注解的方式指定相应状态码
    @ResponseStatus(HttpStatus.BAD_REQUEST)
	public ModelAndView HandleValidationException(ValidationException ex) {

	    // 自定义结果封装类
		ResultEntity resultEntity = ResultEntity.ng();

        if (ex.getErrors() != null) {
            resultEntity.setErrors(ex.getErrors());
        }

        return this.commonErrorHandle(resultEntity);
    }

	private ModelAndView commonErrorHandle(ResultEntity resultEntity) {

        // 如果是Ajax请求的话
        if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {

            // MappingJackson2JsonView的作用是把model转换为json
            MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
            jsonView.setExtractValueFromSingleKeyModel(true);

            // ModelAndView是通过MappingJackson2JsonView构成的,因此并不定位到视图,而是返回json数据
            ModelAndView mv = new ModelAndView(jsonView);
            mv.addObject("jsonKey", resultEntity);
            return mv;
        }

        // 重定向到错误页面
        return new ModelAndView(new RedirectView(request.getContextPath() + "/systemError"));
    }
}

1.7 ⏹待校验的Bean

import lombok.Data;

import javax.validation.Valid;
import javax.validation.groups.Default;
import java.util.List;

@Data
public class Test4Form {
	
	// 用来表示错误信息的先后顺序
	@CheckMsgOrder(value = 1)
    @ValidateNotEmpty(msgArgs = "from日期", groups = {Default.class})
    @ValidateDateString(msgArgs = "from日期", pattern = "yyyyMMdd", groups = {Default.class})
    private String fromData;
	
	@CheckMsgOrder(value = 2)
    @ValidateNotEmpty(msgArgs = "to日期", groups = {Default.class})
    @ValidateDateString(msgArgs = "to日期", pattern = "yyyyMMdd", groups = {Default.class})
    private String toData;
	
	/*
		在指定校验AgeGroup分组时,只有被标记了AgeGroup.class的注解才会其作用
		如果指定校验Default.class,所有的注解都会其作用
	*/ 
	@CheckMsgOrder(value = 2)
    @ValidateHalfNumeric(groups = {AgeGroup.class, Default.class})
    private String age;
	
	// 定义一个age分组,
    public interface AgeGroup { }
}

二. 校验

2.1 普通的校验

⭕前台

$("#btn1").click(() => {

    operateFlag = !operateFlag;

    const url = `http://localhost:8080/test4/commonValidate1`;
    const validateParam = {
        fromData: '2021pp',
        age: 'asc',
    };
    
    doAjax(url, validateParam, function(data) {
        console.log(data);
    });
});

⭕后台

使用@Validated注解校验Bean中自定义注解,对属性进行校验.

@PostMapping("/commonValidate1")
@ResponseBody
public ResultEntity commonValidate1(@RequestBody @Validated Test4Form form, BindingResult bindingResult) {
	
	// 使用BindingResult接收注解的check结果,手动抛出自定义异常
    if (bindingResult.hasErrors()) {
        throw new ValidationException(bindingResult);
    }

    System.out.println(form);
    return ResultEntity.ok();
}

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


2.2 不使用BindingResult接收注解的校验结果

⭕后台

1.不使用BindingResult接收注解的check结果,会抛出MethodArgumentNotValidException异常.需要在全局异常捕获类,处理此异常.
2.因为我们在GlobalExceptionHandler的HandleMethodArgumentNotValidException方法中对异常进行了捕获,因此前台也能正确获取到后台响应.

@PostMapping("/commonValidate2")
@ResponseBody
public ResultEntity commonValidate2(@RequestBody @Validated Test4Form form) {

    System.out.println(form);
    return ResultEntity.ok();
}

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


2.3 分组check

⭕前台

$("#btn2").click(() => {

    const url = "http://localhost:8080/test4/ageGroupValidate";
    const validateParam = {
        fromData: '2021pp',
        // age: 'asc',
    };
    doAjax(url, validateParam, function(data) {
        console.log(data);
    });
});

⭕后台

使用@Validated(Test4Form.AgeGroup.class)注解,声明只校验标记了AgeGroup分组的属性.未标记AgeGroup分组的属性,一概不校验.

@PostMapping("/ageGroupValidate")
@ResponseBody
public ResultEntity ageGroupValidate(@RequestBody @Validated(Test4Form.AgeGroup.class) Test4Form form,
                                     BindingResult validateResult) {

    if (validateResult.hasErrors()) {
        throw new ValidationException(validateResult);
    }

    System.out.println(form);
    return ResultEntity.ok();
}

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

2.4 LocalValidatorFactoryBean手动校验

1.除了使用@Validated在Controller层进行校验之外,还可以使用LocalValidatorFactoryBean进行校验,同样也支持分组.
2.默认情况下,校验之后的error消息是无序的,即属性1到属性5的都被校验住之后,属性5所对应的错误消息可能出现在属性1之前,此时可通过自定义注解@CheckMsgOrder来对错误消息排序.

⭕前台

let operateFlag = false;

$("#btn3").click(() => {

    operateFlag = !operateFlag;

    const url = "http://localhost:8080/test4/conditionGroupValidate";
    const validateParam = {
        fromData: '2021pp',
        age: 'asc',
        operateFlag,
    };
    doAjax(url, validateParam, function(data) {
        console.log(data);
    });
});

⭕后台

@Autowired
private Test4Service service;

@PostMapping("/conditionGroupValidate")
@ResponseBody
public ResultEntity conditionGroupValidate(@RequestBody Test4Form form) {

 	// 在业务层对Bean进行校验
    service.check(form);

    System.out.println(form);
    return ResultEntity.ok();
}
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import javax.validation.ConstraintViolation;
import javax.validation.groups.Default;

@Service
public class Test4Service {
	
	// 注入校验工厂类
    @Autowired
    private LocalValidatorFactoryBean validator;

    public void check(Test4Form form) {

        // 使用JDK的校验对象(无法check一览中的值,只有Spring提供的LocalValidatorFactoryBean才能都check)
        // Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        Set<ConstraintViolation<Test4Form>> errorResult;
		
		// 记性条件进行分组check,校验全部或者仅校验AgeGroup分组
        if (!form.getOperateFlag()) {
            errorResult = validator.validate(form, Default.class);
        } else {
            errorResult = validator.validate(form, Test4Form.AgeGroup.class);
        }
		
		// 若存在错误消息的话
        if (!ObjectUtils.isEmpty(errorResult) && errorResult.size() > 0) {

            Map<Integer, ErrorItemEntity> itemEntityHashMap = new HashMap<>();
            List<ErrorItemEntity> tableItemList = new ArrayList<>();
			
			/*
				默认校验之后的errorResult是无序的,我们通过其中的getPropertyPath
				获取到bean中的属性名称,然后通过反射工具类,获取该属性上标记的
				@CheckMsgOrder注解所对应的value值,然后根据此value值进行排序
			*/ 
            for (ConstraintViolation<Test4Form> error : errorResult) {

                String errorFieldName = error.getPropertyPath().toString();
                int checkOrderValue = ReflectionUtil.getCheckOrderValue(form.getClass(), errorFieldName);
                if (checkOrderValue == 0) {
                    tableItemList.add(ErrorItemEntity.of(error.getMessage(), errorFieldName));
                    continue;
                }

                itemEntityHashMap.put(checkOrderValue, ErrorItemEntity.of(error.getMessage(), errorFieldName));
            }

            // 根据Map中的key进行排序,保证错误消息按照注解@CheckMsgOrder的值由小到大进行排序
            List<ErrorItemEntity> errorList = new ArrayList<>();
            itemEntityHashMap.entrySet().stream()
                    .sorted(Map.Entry.comparingByKey())
                    .forEachOrdered(item -> errorList.add(item.getValue()));
            errorList.addAll(tableItemList);
            throw new ValidationException(errorList);
        }
    }
}

⭕效果

  • 当operateFlag为true
    在这里插入图片描述
  • 当operateFlag为false
    在这里插入图片描述

2.5 对Bean中的实体类List进行校验

⭕前台

const tableList = [
    {
        id: null,
        address: '测试address123',
        hobby: '测试hobby123'
    },
    {
        id: 110,
        address: '测试',
        hobby: '测试AAAAAAAAAA'
    },
    {
        id: 120
    }
];

$("#btn4").click(() => {

    const url = 'http://localhost:8080/test4/validateAnnotationTableList';
    const validateParam = {
        fromData: '2021pp',
        age: 'asc',
        tableList,
    };
    doAjax(url, validateParam, function(data) {
        console.log(data);
    });
});

⭕后台

嵌套校验的情况下,需要使用@Valid注解

在这里插入图片描述

向Test4Form中追加属性

import javax.validation.Valid;

@Data
public class Test4Form {
	// ...
	
    @Valid
    private List<Test4Entity> tableList;
	
	// ...
}
@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;
}
@PostMapping("/validateAnnotationTableList")
@ResponseBody
public ResultEntity validateAnnotationTableList(@RequestBody @Validated(Default.class) Test4Form form) {

    System.out.println(form);
    return ResultEntity.ok();
}

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

三. 国际化校验

⭕前台

let operateFlag = false;

$("#btn4").click(() => {

    operateFlag = !operateFlag;
	
	// 根据operateFlag来决定,返回的校验消息是中文还是日文
    const url = `http://localhost:8080/test4/validateAnnotationTableList?language=${operateFlag ? 'zh' : 'jp'}`;
    const validateParam = {
        fromData: '2021pp',
        age: 'asc',
        tableList,
        operateFlag,
    };
    doAjax(url, validateParam, function(data) {
        console.log(data);
    });
});

⭕后台配置

根据前台传入的?language所对应的值来指定要返回的国家语言错误消息

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
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;
    }

    // 自定义国际化环境下要显示的校验消息
    @Bean
    public LocalValidatorFactoryBean localValidatorFactoryBean() {

        LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();

        // 使用Spring加载国际化资源文件
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        /*
            设置资源文件的前缀名称
            配合 localeChangeInterceptor 中设置的?language所对应的参数值
            就可以加载对应国际化校验消息
         */
        messageSource.setBasename("messages");
        messageSource.setDefaultEncoding("UTF-8");

        localValidatorFactoryBean.setValidationMessageSource(messageSource);
        return localValidatorFactoryBean;
    }

    // 将我们自定义的国际化语言参数拦截器放入Spring MVC的默认配置中
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
}

⭕国际化校验文件

messages_jp.properties

# 情報I
1031I=予約枠の一括作成を行いました。
1032I=予約枠の一括削除を行いました。
1033I=予約枠状態の一括変更を行いました。

# エラーE
1001E={msgArgs}を入力してください。
1002E={msgArgs}を選択してください。
1003E={msgArgs}は全角カタカナまたは半角アルファベットで入力してください。
1004E=入力された{msgArgs}日付は妥当ではありません。
1005E=半角数字を入力してください。
1006E={msgArgs}を{max}文字以内で入力してください。

messages_zh.properties

# 確認Q
1008Q=确定要放弃编辑的内容吗?
1019Q=确定要取消预约吗?
1021Q=确定要删除吗?

# エラーE
1001E=请输入{msgArgs}。
1002E=请选择{msgArgs}。
1003E=请输入{msgArgs}全角假名。
1004E=输入的{msgArgs}日期格式不正确。
1005E=请输入半角数字。
1006E={msgArgs}最多不能超过{max}文字。

⭕效果

  • ?language=zh
    在这里插入图片描述
  • ?language=jp
    在这里插入图片描述
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值