自定义 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 改成资源键,并配置对应文本。
错误响应不附带被拒绝的原值,避免泄露敏感输入;前端展示消息仍需按文本处理。必填检查也不代替字段白名单、权限和业务有效性校验。