自定义 Required 注解能否正确工作,要分开看三步:规则如何定义、请求入口是否触发校验、失败后如何返回错误。名字叫“必填”,也不代表它对所有对象都检查内容非空。

恢复旧规则的完整定义

下面使用 Spring 5、Bean Validation 2 的 javax.validation 包,适合说明旧项目的规则。校验 API 还需要实现提供者,例如兼容的 Hibernate Validator;新 Jakarta 包项目不能直接混用这份 imports。

将以下内容放在 Required.java:

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

@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = Required.RequiredValidator.class)
public @interface Required {
    String message() default "不能为空";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    class RequiredValidator implements ConstraintValidator<Required, Object> {
        @Override
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            if (value == null) {
                return false;
            }
            if (value instanceof String) {
                return !((String) value).trim().isEmpty();
            }
            return true;
        }
    }
}

message、groups、payload 是 Bean Validation 的约定,不是 JDK 自带的“check 注解属性”。@Constraint 关联验证器,@Target 限制标注位置,RUNTIME 使运行时可以读取注解;SOURCE 和 CLASS 的保留范围不同,不具备相同反射可见性。Hibernate Validator 6 自定义约束

“非空”的实际含义

这个规则拒绝 null、空串和 trim 后为空的字符串;空集合、空数组、数字 0 与其他非 null 对象会通过。trim 的空白范围也不等于所有 Unicode 空白。

若只验证文本,优先考虑合适版本的 @NotBlank;集合可考虑 @NotEmpty,普通引用可用 @NotNull。业务上需要不同语义时再自定义,避免一个 Required 名字覆盖多种不一致规则。

原注解允许 TYPE,但上述验证器对普通对象直接返回 true,没有跨字段逻辑。因此这里限制到字段及参数,不把类级标注冒充组合校验。

在 MVC 入口触发并处理错误

下面展示表单参数绑定,不是 JSON 请求体;需要 MVC 已配置校验器:

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RequiredExampleController {
    public static class Form {
        @Required
        private String title;
        public String getTitle() { return title; }
        public void setTitle(String title) { this.title = title; }
    }

    @PostMapping("/examples/required")
    public ResponseEntity<Map<String, List<String>>> check(
            @Validated Form form, BindingResult result) {
        Map<String, List<String>> errors = new LinkedHashMap<>();
        for (FieldError error : result.getFieldErrors()) {
            errors.computeIfAbsent(error.getField(), key -> new ArrayList<>())
                    .add(message(error));
        }
        for (ObjectError error : result.getGlobalErrors()) {
            errors.computeIfAbsent("_global", key -> new ArrayList<>())
                    .add(message(error));
        }
        if (result.hasErrors()) {
            return ResponseEntity.badRequest().body(errors);
        }
        return ResponseEntity.ok(errors);
    }

    private String message(ObjectError error) {
        return error.getDefaultMessage() == null
                ? "输入不符合要求" : error.getDefaultMessage();
    }
}

BindingResult 紧接被验证的表单。getField() 是字段名,不是应用名;getDefaultMessage() 是错误消息,同一字段可能有多个错误,全局错误需另外处理。若使用消息资源,可把注解的 message 改成资源键,并配置对应文本。

错误响应不附带被拒绝的原值,避免泄露敏感输入;前端展示消息仍需按文本处理。必填检查也不代替字段白名单、权限和业务有效性校验。