SpringBoot参数校验 | 错误信息message格式化参数详解

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

通过以上方法,可以实现灵活、可读性强的错误提示,提升用户体验和开发效率。


作 者:南烛
链 接:https://www.itnotes.top/archives/1080
来 源:IT笔记
文章版权归作者所有,转载请注明出处!


上一篇
下一篇