在SpringBoot参数校验中,message属性支持丰富的格式化参数功能,可以动态插入校验注解的属性值,使错误提示更加友好和精确。以下是message格式化参数的完整介绍:
一、内置注解的格式化参数
1. 常用注解的占位符
SpringBoot的校验注解内置了多种占位符,可以直接在message中使用:
public class UserDTO {
@Size(min = 6, max = 20, message = "密码长度必须在{min}到{max}位之间")
private String password;
@Min(value = 18, message = "年龄不能小于{value}岁")
@Max(value = 100, message = "年龄不能大于{value}岁")
private Integer age;
@DecimalMin(value = "0.01", message = "金额不能小于{value}")
@DecimalMax(value = "9999.99", message = "金额不能大于{value}")
private BigDecimal amount;
}
支持的占位符:
{min}:最小值限制{max}:最大值限制{value}:注解中指定的值{integer}:整数位数限制{fraction}:小数位数限制
2. 国际化消息的占位符
在国际化配置文件中,同样支持占位符:
# ValidationMessages.properties
user.password.size=密码长度必须在{min}到{max}位之间
user.age.min=年龄不能小于{value}岁
user.age.max=年龄不能大于{value}岁
使用方式:
public class UserDTO {
@Size(min = 6, max = 20, message = "{user.password.size}")
private String password;
@Min(value = 18, message = "{user.age.min}")
@Max(value = 100, message = "{user.age.max}")
private Integer age;
}
二、自定义注解的格式化参数
1. 定义带参数的注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = LengthRangeValidator.class)
public @interface LengthRange {
String message() default "长度必须在{min}到{max}之间";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int min() default 0;
int max() default Integer.MAX_VALUE;
}
2. 实现校验器并支持参数替换
public class LengthRangeValidator implements ConstraintValidator<LengthRange, String> {
private int min;
private int max;
@Override
public void initialize(LengthRange constraintAnnotation) {
this.min = constraintAnnotation.min();
this.max = constraintAnnotation.max();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true;
int length = value.length();
if (length < min || length > max) {
// 禁用默认消息
context.disableDefaultConstraintViolation();
// 构建自定义消息
context.buildConstraintViolationWithTemplate(
context.getDefaultConstraintMessageTemplate()
.replace("{min}", String.valueOf(min))
.replace("{max}", String.valueOf(max))
).addConstraintViolation();
return false;
}
return true;
}
}
3. 使用自定义注解
public class UserDTO {
@LengthRange(min = 6, max = 20, message = "用户名长度必须在{min}到{max}之间")
private String username;
}
三、国际化配置中的占位符
1. 基本配置
在resources/ValidationMessages.properties中:
# 中文配置
user.name.required=用户名不能为空
user.password.size=密码长度必须在{min}到{max}位之间
user.age.range=年龄必须在{min}到{max}岁之间
# 英文配置
user.name.required=Username is required
user.password.size=Password length must be between {min} and {max}
user.age.range=Age must be between {min} and {max}
2. 支持多语言
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidationException(MethodArgumentNotValidException ex,
HttpServletRequest request) {
// 获取请求的语言环境
Locale locale = request.getLocale();
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(fieldError -> {
String message = fieldError.getDefaultMessage();
// 如果消息包含占位符,进行替换
if (message != null && message.contains("{") && message.contains("}")) {
// 获取注解属性
Object[] arguments = fieldError.getArguments();
if (arguments != null && arguments.length > 1) {
Object[] annotationAttributes = (Object[]) arguments[1];
if (annotationAttributes != null) {
// 替换占位符
for (int i = 0; i < annotationAttributes.length; i++) {
message = message.replace("{" + i + "}",
String.valueOf(annotationAttributes[i]));
}
}
}
}
return message;
})
.collect(Collectors.toList());
return Result.fail(String.join(", ", errors));
}
}
四、复杂场景的占位符处理
1. 多个占位符
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = RangeValidator.class)
public @interface Range {
String message() default "值必须在{min}到{max}之间,当前值:{actual}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
double min() default Double.MIN_VALUE;
double max() default Double.MAX_VALUE;
}
2. 校验器实现
public class RangeValidator implements ConstraintValidator<Range, Number> {
private double min;
private double max;
@Override
public void initialize(Range constraintAnnotation) {
this.min = constraintAnnotation.min();
this.max = constraintAnnotation.max();
}
@Override
public boolean isValid(Number value, ConstraintValidatorContext context) {
if (value == null) return true;
double doubleValue = value.doubleValue();
if (doubleValue < min || doubleValue > max) {
context.disableDefaultConstraintViolation();
String message = context.getDefaultConstraintMessageTemplate()
.replace("{min}", String.valueOf(min))
.replace("{max}", String.valueOf(max))
.replace("{actual}", String.valueOf(doubleValue));
context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
return false;
}
return true;
}
}
3. 使用示例
public class ProductDTO {
@Range(min = 0.01, max = 9999.99, message = "价格必须在{min}到{max}之间")
private BigDecimal price;
}
五、全局异常处理中的占位符解析
1. 增强版异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler {
@Autowired
private MessageSource messageSource;
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidationException(MethodArgumentNotValidException ex,
HttpServletRequest request) {
Locale locale = request.getLocale();
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(fieldError -> {
String message = fieldError.getDefaultMessage();
Object[] arguments = fieldError.getArguments();
// 如果消息是国际化key
if (message != null && message.startsWith("{") && message.endsWith("}")) {
String messageKey = message.substring(1, message.length() - 1);
return messageSource.getMessage(messageKey, arguments, locale);
}
// 如果消息包含占位符
if (message != null && arguments != null && arguments.length > 1) {
Object[] annotationAttributes = (Object[]) arguments[1];
if (annotationAttributes != null) {
for (int i = 0; i < annotationAttributes.length; i++) {
message = message.replace("{" + i + "}",
String.valueOf(annotationAttributes[i]));
}
}
}
return message;
})
.collect(Collectors.toList());
return Result.fail(String.join(", ", errors));
}
}
2. 配置MessageSource
@Configuration
public class MessageConfig {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("ValidationMessages");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(3600);
return messageSource;
}
@Bean
public LocalValidatorFactoryBean validator(MessageSource messageSource) {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(messageSource);
return validator;
}
}
六、最佳实践
1. 统一错误消息格式
# ValidationMessages.properties
validation.common.required={0}不能为空
validation.common.size={0}长度必须在{1}到{2}之间
validation.common.min={0}不能小于{1}
validation.common.max={0}不能大于{1}
2. 使用统一格式
public class UserDTO {
@NotBlank(message = "{validation.common.required}")
private String username;
@Size(min = 6, max = 20, message = "{validation.common.size}")
private String password;
@Min(value = 18, message = "{validation.common.min}")
private Integer age;
}
3. 自定义消息解析器
@Component
public class CustomMessageInterpolator implements MessageInterpolator {
private final MessageInterpolator defaultInterpolator;
public CustomMessageInterpolator(MessageInterpolator defaultInterpolator) {
this.defaultInterpolator = defaultInterpolator;
}
@Override
public String interpolate(String messageTemplate, Context context) {
return interpolate(messageTemplate, context, Locale.getDefault());
}
@Override
public String interpolate(String messageTemplate, Context context, Locale locale) {
String message = defaultInterpolator.interpolate(messageTemplate, context, locale);
// 自定义占位符替换逻辑
if (message.contains("{field}")) {
// 获取字段名
String fieldName = context.getConstraintDescriptor()
.getAttributes()
.get("fieldName")
.toString();
message = message.replace("{field}", fieldName);
}
return message;
}
}
通过以上方法,可以实现灵活、可读性强的错误提示,提升用户体验和开发效率。