Skip to content

参数验证

更新: 2/10/2025 字数: 0 字 时长: 0 分钟

参数注解

java
import jakarta.validation.constraints.*;
import lombok.Data;
import org.hibernate.validator.constraints.CreditCardNumber;
import org.hibernate.validator.constraints.EAN;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Date;
import java.util.Map;

@Data
static class Entity {
    /**
     * NotNull 注解用于确保被注解的字段不为 null
     */
    @NotNull(message = "用户名不能为空")
    private String username;
    /**
     * NotBlank 注解除了确保字符串不为 null 外,还检查字符串至少有一个非空白字符。
     */
    @NotBlank(message = "评论内容不能为空")
    private String content;
    /**
     * NotEmpty 注解用于验证字符串、集合或数组不为 null 且至少有一个元素(对于集合和数组)或至少有一个非空白字符(对于字符串)。
     */
    @NotEmpty(message = "消息内容不能为空")
    private String text;
    /**
     * Size 注解用于验证字符串、集合、数组或Map的大小是否在指定的范围内。
     */
    @Size(min = 1, max = 100, message = "书名长度必须在1到100个字符之间")
    private String title;
    /**
     * Past 注解用于验证日期类型的字段是否表示过去的日期。
     */
    @Past(message = "事件日期必须是过去的日期")
    private LocalDate eventDate;
    /**
     * Future 注解用于验证日期类型的字段是否表示未来的日期。
     */
    @Future(message = "预计送达日期必须是未来的日期")
    private LocalDate expectedDeliveryDate;
    /**
     * PastOrPresent 注解用于验证日期类型的字段是否表示现在或过去的日期。
     */
    @PastOrPresent(message = "保险生效日期必须为当前或过去的日期")
    private LocalDate effectiveDate;
    /**
     * FutureOrPresent 注解用于验证日期类型的字段是否表示现在或未来的日期。
     */
    @FutureOrPresent(message = "预约日期必须是当前或未来的日期")
    private LocalDate appointmentDate;
    /**
     * Pattern 注解用于验证字符串是否与指定的正则表达式匹配。flags: 正则表达式的匹配标志。
     */
    @Pattern(message = "手机号格式错误", regexp = "^1(3\\d|4[5-9]|5[0-35-9]|6[567]|7[0-8]|8\\d|9[0-35-9])\\d{8}$", flags = {Pattern.Flag.CASE_INSENSITIVE})
    private String phone;
    @Pattern(message = "电子邮箱格式不正确", regexp = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$", flags = {Pattern.Flag.CASE_INSENSITIVE})
    private String email2;
    @Email(message = "电子邮件地址无效", regexp = "^.+@.+\\..+$")
    private String email;
    @Email(message = "邮箱格式错误")
    private String email3;

    /**
     * Min 注解用于验证数值类型的字段是否大于或等于指定的最小值。
     */
    @Min(value = 1, message = "购买数量不能少于1")
    private int quantity;
    /**
     * Max 注解用于验证数值类型的字段是否小于或等于指定的最大值。
     */
    @Max(value = 100, message = "库存数量不能超过100")
    private int stock;
    /**
     * DecimalMin 注解用于验证 BigDecimal 或包装的数值类型字段是否大于或等于指定的十进制最小值。inclusive: 布尔值,指示是否包含指定的边界值(默认为 true)。
     */
    @DecimalMin(value = "0.00", message = "价格必须非负")
    private BigDecimal value;
    /**
     * DecimalMax 注解用于验证 BigDecimal 或包装的数值类型字段是否小于或等于指定的十进制最大值。inclusive: 布尔值,指示是否包含指定的边界值(默认为 true)。
     */
    @DecimalMax(value = "1000.00", message = "价格不能超过1000")
    private BigDecimal value2;
    /**
     * Positive 注解用于验证数值类型的字段是否严格大于 0。
     */
    @Positive(message = "账户余额必须为正数")
    private BigDecimal balance;
    /**
     * PositiveOrZero 注解用于验证数值类型的字段是否大于或等于 0。
     */
    @PositiveOrZero(message = "账户余额不能为负数")
    private BigDecimal balance2;
    /**
     * Negative 注解用于验证数值类型的字段是否严格小于 0。
     */
    @Negative(message = "债务金额必须为负数")
    private BigDecimal amount;
    /**
     * NegativeOrZero 注解用于验证数值类型的字段是否小于或等于 0。
     */
    @NegativeOrZero(message = "债务金额不能为正数")
    private BigDecimal amount2;
    /**
     * Digits 注解用于验证数值类型的字段是否符合指定的整数和小数位数。
     */
    @Digits(integer = 10, fraction = 2, message = "交易金额格式不正确")
    private BigDecimal amount3;


    /**
     * EAN 注解用于验证国际标准书号(ISBN)或欧洲商品编号(EAN-13)。type: 指定要验证的EAN类型
     */
    @EAN(message = "无效的EAN或ISBN编号")
    private String ean;
    /**
     * 虽然 @URL注解在JSR 380规范中没有定义,但Hibernate Validator提供了类似的功能,用于验证字符串是否是有效的URL格式。
     * protocol: 指定要验证的协议(默认为http或https)。
     * host: 指定要验证的主机名。
     * port: 指定要验证的端口号。
     * regexp: 自定义的URL匹配正则表达式。
     */
    @URL(message = "无效的URL", protocol = "https", host = "example.com", port = 443)
    private String url;

}

//@AssertFalse            被注释的元素只能为false
//@AssertTrue             被注释的元素只能为true
//@DecimalMax             被注释的元素必须小于或等于{value}
//@DecimalMin             被注释的元素必须大于或等于{value}
//@Digits                 被注释的元素数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
//@Email                  被注释的元素不是一个合法的电子邮件地址
//@Future                 被注释的元素需要是一个将来的时间
//@FutureOrPresent        被注释的元素需要是一个将来或现在的时间
//@Max                    被注释的元素最大不能超过{value}
//@Min                    被注释的元素最小不能小于{value}
//@Negative               被注释的元素必须是负数
//@NegativeOrZero         被注释的元素必须是负数或零
//@NotBlank               被注释的元素不能为空
//@NotEmpty               被注释的元素不能为空
//@NotNull                被注释的元素不能为null
//@Null                   被注释的元素必须为null
//@Past                   被注释的元素需要是一个过去的时间
//@PastOrPresent          被注释的元素需要是一个过去或现在的时间
//@Pattern                被注释的元素需要匹配正则表达式"{regexp}"
//@Positive               被注释的元素必须是正数
//@PositiveOrZero         被注释的元素必须是正数或零
//@Size                   被注释的元素个数必须在{min}和{max}之间

//@CreditCardNumber       被注释的元素不合法的信用卡号码
//@Currency               被注释的元素不合法的货币 (必须是{value}其中之一)
//@EAN                    被注释的元素不合法的{type}条形码
//@Email                  被注释的元素不是一个合法的电子邮件地址  (已过期)
//@Length                 被注释的元素长度需要在{min}和{max}之间
//@CodePointLength        被注释的元素长度需要在{min}和{max}之间
//@LuhnCheck              被注释的元素${validatedValue}的校验码不合法, Luhn模10校验和不匹配
//@Mod10Check             被注释的元素${validatedValue}的校验码不合法, 模10校验和不匹配
//@Mod11Check             被注释的元素${validatedValue}的校验码不合法, 模11校验和不匹配
//@ModCheck               被注释的元素${validatedValue}的校验码不合法, ${modType}校验和不匹配  (已过期)
//@NotBlank               被注释的元素不能为空  (已过期)
//@NotEmpty               被注释的元素不能为空  (已过期)
//@ParametersScriptAssert 被注释的元素执行脚本表达式"{script}"没有返回期望结果
//@Range                  被注释的元素需要在{min}和{max}之间
//@SafeHtml               被注释的元素可能有不安全的HTML内容
//@ScriptAssert           被注释的元素执行脚本表达式"{script}"没有返回期望结果
//@URL                    被注释的元素需要是一个合法的URL
//@DurationMax            被注释的元素必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
//@DurationMin            被注释的元素必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}

工具类

java
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import lombok.experimental.UtilityClass;
import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

/**
 * @author zhanghaijun
 * @date 2024/3/14 19:27
 * @description validator工具类
 */
@UtilityClass
public class BeanValidator {

    private static final ValidatorFactory validatorFactory = Validation.byDefaultProvider().configure().messageInterpolator(new ParameterMessageInterpolator()).buildValidatorFactory();

    private static <T> Map<String, String> validate(T t, Class<?>... groups) {
        Validator validator = validatorFactory.getValidator();
        Set<ConstraintViolation<T>> validate = validator.validate(t, groups);
        if (validate.isEmpty()) {
            return Collections.emptyMap();
        } else {
            LinkedHashMap<String, String> errors = new LinkedHashMap<>();
            for (ConstraintViolation<T> violation : validate) {
                errors.put(violation.getPropertyPath().toString(), violation.getMessage());
            }
            return errors;
        }
    }

    public static <T> String validateResult(T t, Class<?>... groups) {
        Map<String, String> validate = validate(t, groups);
        if (validate.isEmpty()) {
            return "";
        }
        StringBuilder messages = new StringBuilder("参数验证失败: ");
        validate.forEach((k, v) -> messages.append(k).append(": ").append(v).append("; "));
        return messages.toString();
    }
}

示例代码

电商平台

  1. 用户信息(User):必须包含有效的电子邮件和非空的用户名。
  2. 商品详情(Product):需要有有效的库存数量和价格范围。
  3. 订单(Order):必须包含用户信息、商品列表,并且总金额必须为正数。
  4. 订单项(OrderItem):每个订单项需要验证购买数量和商品ID。
  5. 支付信息(PaymentInfo):需要验证支付金额是否正确,并且支付方式是否被接受。
java
class ECommercePlatform {
    // 用户信息
    public class User {
        @Email(message = "电子邮件地址无效", groups = UserChecks.class)
        private String email;
        @NotBlank(message = "用户名不能为空", groups = UserChecks.class)
        private String username;
        // 用户相关方法...
    }

    // 商品详情
    public class Product {
        @NotNull(message = "商品ID不能为空", groups = ProductChecks.class)
        private String productId;
        @PositiveOrZero(message = "库存数量不能为负数", groups = ProductChecks.class)
        private int stock;
        @DecimalMin(value = "0.01", message = "商品价格必须至少为0.01", groups = ProductChecks.class)
        private BigDecimal price;
        // 商品相关方法...
    }

    // 订单
    public class Order {
        @Valid
        private User user;
        @Valid
        @Size(min = 1, message = "订单至少需要一个商品项", groups = OrderChecks.class)
        private List<OrderItem> items;
        @Positive(message = "订单总金额必须为正数", groups = OrderChecks.class)
        private BigDecimal totalAmount;
        // 订单相关方法...
    }

    // 订单项
    public class OrderItem {
        @NotNull(message = "商品ID不能为空", groups = ItemChecks.class)
        private String productId;
        @Positive(message = "购买数量必须为正数", groups = ItemChecks.class)
        private int quantity;
        // 订单项相关方法...
    }

    // 支付信息
    public class PaymentInfo {
        @Positive(message = "支付金额必须为正数", groups = PaymentChecks.class)
        private BigDecimal amount;
        @NotBlank(message = "支付方式不能为空", groups = PaymentChecks.class)
        private String method;
        // 支付信息相关方法...
    }

    //验证组定义
    public interface UserChecks {
    }

    public interface ProductChecks {
    }

    public interface OrderChecks {
    }

    public interface ItemChecks {
    }

    public interface PaymentChecks {
    }

    //错误元数据载荷
    public class ErrorPayloadsimplementsPayload implements Payload {
    }
}

在线预订系统分组验证

在线预订系统允许用户根据其类型(个人或公司)进行预订。系统需要对不同类型的预订应用不同的验证规则。

java
import javax.validation.*;
import javax.validation.constraints.*;
import java.time.LocalDate;
import java.util.Set;

// 验证组接口
public interface PersonalBookingChecks {
}

public interface CorporateBookingChecks {
}

// 预订类型枚举
public enum BookingType {
    PERSONAL, CORPORATE
}

// 预订实体类
@Data
public class Booking {
    private BookingType type;
    // 个人预订字段
    @NotBlank(message = "个人预订的姓名必须填写", groups = PersonalBookingChecks.class)
    private String personalName;
    @Email(message = "个人预订的电子邮箱格式必须正确", groups = PersonalBookingChecks.class)
    private String personalEmail;
    // 公司预订字段
    @NotBlank(message = "公司预订的公司名称必须填写", groups = CorporateBookingChecks.class)
    private String corporateName;
    @PositiveOrZero(message = "公司预订的税务编号必须是非负数", groups = CorporateBookingChecks.class)
    private Long corporateTaxId;
    // 所有预订类型共有的字段
    @NotNull(message = "入住日期不能为空")
    private LocalDate checkInDate;
    @NotNull(message = "退房日期不能为空")
    private LocalDate checkOutDate;
}

// 服务层验证逻辑
public class BookingService {
    private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    public void processBooking(Booking booking) {
        Set<ConstraintViolation<Booking>> violations = switch (booking.getType()) {
            case PERSONAL -> validator.validate(booking, PersonalBookingChecks.class);
            case CORPORATE -> validator.validate(booking, CorporateBookingChecks.class);
            default -> throw new IllegalStateException("未知的预订类型");
        };
        if (!violations.isEmpty()) {
            // 将验证错误转换为用户友好的字符串信息
            throw new IllegalArgumentException("预订验证失败: " + formatValidationErrors(violations));
        }
        // 如果验证通过,继续处理预订逻辑,例如保存到数据库
        // 省略处理预订的代码...
    }

    private String formatValidationErrors(Set<ConstraintViolation<Booking>> violations) {
        StringBuilder sb = new StringBuilder();
        for (ConstraintViolation<Booking> violation : violations) {
            sb.append(violation.getPropertyPath()).append(": ").append(violation.getMessage()).append("\n");
        }
        return sb.toString();
    }
    // 省略其他服务方法...
}

// 测试类
public class BookingApplication {
    public static void main(String[] args) {
        BookingService service = new BookingService();
        Booking booking = new Booking();
        // 这里设置了booking的属性和类型...
        // 处理预订
        service.processBooking(booking);
    }
}