You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何创建带范围验证的自定义校验注解?库存验证异常排查

库存范围校验问题排查与修复

问题背景

需要实现库存值范围校验:当库存低于最小值或高于最大值时触发错误提示。尝试使用@Range、@Min等标准校验注解无法满足需求,自定义了@invrange注解及对应校验器,但仍出现异常,怀疑是注解属性绑定变量的问题。


现有代码

自定义注解代码

package com.example.demo.validators;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = {invrangeValidator.class})
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface invrange {
    String message() default "库存值必须在最小值和最大值之间!";
    Class<?> [] groups() default{};
    Class<? extends Payload> [] payload() default {};

    int maxInv();
    int minInv();
}

校验器代码

package com.example.demo.validators;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class invrangeValidator implements ConstraintValidator<invrange, Integer> {
    private int maxInv;
    private int minInv;

    @Override
    public void initialize(invrange constraintAnnotation) {
        maxInv = constraintAnnotation.maxInv();
        minInv = constraintAnnotation.minInv();
    }

    @Override
    public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
        if (integer > maxInv && integer < minInv) {
            return false;
        } else {
            return true;
        }
    }
}

实体类代码

package com.example.demo.domain;

import com.example.demo.validators.ValidDeletePart;
import com.example.demo.validators.invrange;
import org.hibernate.validator.constraints.Range;

import javax.persistence.*;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

@Entity
@ValidDeletePart
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="part_type",discriminatorType = DiscriminatorType.INTEGER)
@Table(name="Parts")
public abstract class Part implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    long id;
    String name;
    @Min(value = 0, message = "价格必须为正数")
    double price;

    int inv;
    int maxInv;
    int minInv;

    @ManyToMany
    @JoinTable(name="product_part", joinColumns = @JoinColumn(name="part_id"),
            inverseJoinColumns=@JoinColumn(name="product_id"))
    Set<Product> products= new HashSet<>();

    public Part() {}

    public Part(String name, double price, int inv, int maxInv, int minInv) {
        this.name = name;
        this.price = price;
        this.inv = inv;
        this.maxInv = maxInv;
        this.minInv = minInv;
    }

    public Part(long id, String name, double price, int inv, int maxInv, int minInv) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.inv = inv;
        this.maxInv = maxInv;
        this.minInv = minInv;
    }

    // 省略getter/setter方法
    public int getInv() { return inv; }

    @invrange(maxInv = minInv, minInv = maxInv)
    public void setInv(int inv) { this.inv = inv; }

    public int getMaxInv() {return maxInv;}
    public void setMaxInv(int maxInv){this.maxInv = maxInv;}
    public int getMinInv(){return minInv;}
    public void setMinInv(int minInv){this.minInv = minInv;}
    
    // 省略equals、hashCode、toString方法
}

表单代码

<!DOCTYPE html>
<html lang="en">
<html xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>零件表单</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>外协零件详情</h1>

<form action="#" th:action="@{/showFormAddOutPart}" th:object="${outsourcedpart}" method="POST">
    <p><input type="hidden" th:field="*{id}"/></p>

    <p>名称:</p>
    <p><input type="text" th:field="*{name}" placeholder="名称" class="form-control mb-4 col-4"/></p>

    <p>价格:</p>
    <p><input type="text" th:field="*{price}" placeholder= "价格" class="form-control mb-4 col-4"/></p>
    <p th:if="${#fields.hasErrors('price')}" th:errors="*{price}">价格错误</p>

    <p>库存:</p>
    <p><input type="text" th:field="*{inv}" placeholder="库存" class="form-control mb-4 col-4"/></p>
    <p th:if="${#fields.hasErrors('inv')}" th:errors="*{inv}">库存错误</p>

    <p>最大库存:</p>
    <p><input type="text" th:field="*{maxInv}" placeholder="最大库存" class="form-control mb-4 col-4"></p>

    <p>最小库存:</p>
    <p><input type="text" th:field="*{minInv}" placeholder="最小库存" class="form-control mb-4 col-4"></p>

    <p>公司名称:</p>
    <p><input type="text" th:field="*{companyName}" placeholder="公司名称" class="form-control mb-4 col-4"/></p>

    <p><input type="submit" value="提交" /></p>
</form>
<a href="http://localhost:8080/">返回主页面</a>
</body>
</html>

问题分析

  1. 注解属性绑定错误:实体类setInv方法上的@invrange(maxInv = minInv, minInv = maxInv)写法错误,注解属性无法直接引用实体类的字段值,JSR-380标准不支持字段间的直接引用。
  2. 校验逻辑错误:校验器中的if (integer > maxInv && integer < minInv)逻辑矛盾,应该用||(或)判断库存超出范围的情况。
  3. 校验器类型不匹配:当前校验器仅接收单个Integer值,无法获取实体类中的maxInv和minInv字段值,需要改为校验整个实体对象。

修复方案

1. 修改自定义注解为类级别校验

package com.example.demo.validators;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = {InvrangeValidator.class})
@Target({ElementType.TYPE}) // 修改为类级别校验
@Retention(RetentionPolicy.RUNTIME)
public @interface Invrange {
    String message() default "库存值必须在最小值和最大值之间!";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

2. 修改校验器,校验整个Part对象

package com.example.demo.validators;

import com.example.demo.domain.Part;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class InvrangeValidator implements ConstraintValidator<Invrange, Part> {

    @Override
    public void initialize(Invrange constraintAnnotation) {}

    @Override
    public boolean isValid(Part part, ConstraintValidatorContext context) {
        if (part == null) return true;
        
        int inv = part.getInv();
        int maxInv = part.getMaxInv();
        int minInv = part.getMinInv();
        
        // 修复逻辑:库存超出范围时返回false
        if (inv > maxInv || inv < minInv) {
            // 将错误提示绑定到inv字段
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                    .addPropertyNode("inv")
                    .addConstraintViolation();
            return false;
        }
        return true;
    }
}

3. 在实体类上添加类级别注解

@Entity
@ValidDeletePart
@Invrange // 添加类级别校验注解
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="part_type",discriminatorType = DiscriminatorType.INTEGER)
@Table(name="Parts")
public abstract class Part implements Serializable {
    // ... 移除setInv方法上的@invrange注解,其他代码不变
}

4. 控制器触发校验

确保控制器方法添加@Valid注解触发校验:

@PostMapping("/showFormAddOutPart")
public String addOutsourcedPart(@Valid @ModelAttribute("outsourcedpart") OutsourcedPart part, BindingResult result) {
    if (result.hasErrors()) {
        return "outsourced-part-form"; // 返回表单展示错误
    }
    // 执行保存逻辑
    return "redirect:/";
}

内容的提问来源于stack exchange,提问作者user22196580

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 22:50:53