在Jersey2应用中为自定义类型使用JAX-303(Bean Validation)注解校验
嘿,这个场景我太熟悉了!在Jersey2里给自定义类型(像Money、Percentage这类)加@Min/@Max校验,核心就是给这些类型实现对应的校验器,再让Validation框架能找到它们。下面是一步步的实操方案,亲测能解决问题:
假设你的Money类是这样的(核心要有数值字段,比如BigDecimal amount):
public class Money { private BigDecimal amount; // 省略getter、setter、构造方法等 }
Percentage类类似,比如有BigDecimal value字段表示百分比数值。
不用重新定义新注解,直接给JSR-380的原生@Min、@Max写适配自定义类型的校验器就行。
比如针对Money的@Min校验器:
public class MoneyMinValidator implements ConstraintValidator<Min, Money> { private long minThreshold; @Override public void initialize(Min minAnnotation) { // 初始化注解里的最小值参数 this.minThreshold = minAnnotation.value(); } @Override public boolean isValid(Money money, ConstraintValidatorContext context) { // 处理null场景:如果业务允许null值,返回true;否则返回false if (money == null) { return true; } // 把注解的long值转成BigDecimal,和Money的amount比较 BigDecimal min = BigDecimal.valueOf(minThreshold); return money.getAmount().compareTo(min) >= 0; } }
同理实现Money的@Max校验器:
public class MoneyMaxValidator implements ConstraintValidator<Max, Money> { private long maxThreshold; @Override public void initialize(Max maxAnnotation) { this.maxThreshold = maxAnnotation.value(); } @Override public boolean isValid(Money money, ConstraintValidatorContext context) { if (money == null) { return true; } BigDecimal max = BigDecimal.valueOf(maxThreshold); return money.getAmount().compareTo(max) <= 0; } }
Percentage类型的校验器逻辑完全类似,只需要把Money换成Percentage,对应数值字段替换即可。
有两种简单的注册方式:
方式一:通过SPI配置文件注册
在项目的src/main/resources/META-INF/services目录下,创建名为javax.validation.ConstraintValidator的文件,把所有自定义校验器的全类名写进去:
com.yourpackage.MoneyMinValidator com.yourpackage.MoneyMaxValidator com.yourpackage.PercentageMinValidator com.yourpackage.PercentageMaxValidator
(记得替换成你实际的包路径)
方式二:在Jersey配置类中手动注册
如果你的项目用ResourceConfig配置Jersey,可以自定义ValidatorFactory来注册校验器:
@ApplicationPath("/api") public class JerseyAppConfig extends ResourceConfig { public JerseyAppConfig() { // 先注册Jersey的Validation特性 register(ValidationFeature.class); // 绑定自定义的ValidatorFactory register(new AbstractBinder() { @Override protected void configure() { bindFactory(CustomValidatorFactory.class) .to(javax.validation.ValidatorFactory.class) .in(Singleton.class); } }); } // 自定义ValidatorFactory实现 private static class CustomValidatorFactory implements Factory<javax.validation.ValidatorFactory> { @Override public javax.validation.ValidatorFactory provide() { return Validation.byDefaultProvider() .configure() .constraintValidatorFactory(new ConstraintValidatorFactory() { @Override public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> validatorClass) { // 这里可以用依赖注入获取实例,或者直接反射创建 try { return validatorClass.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException("Failed to create validator instance", e); } } @Override public void releaseInstance(ConstraintValidator<?, ?> instance) { // 按需释放资源,一般空实现即可 } }) .buildValidatorFactory(); } @Override public void dispose(javax.validation.ValidatorFactory instance) { instance.close(); } } }
现在就可以直接在自定义类型的字段上用原生注解了:
public class OrderCreateRequest { @Min(0) // 表示订单金额不能小于0 @Max(100000) // 订单金额上限10万 private Money orderAmount; @Min(0) // 折扣不能低于0% @Max(100) // 折扣不能超过100% private Percentage discount; // 其他字段... }
如果想把默认的校验错误转换成更友好的JSON格式,可以注册一个ExceptionMapper:
@Provider public class ValidationExceptionHandler implements ExceptionMapper<ValidationException> { @Override public Response toResponse(ValidationException exception) { List<String> errorMessages = new ArrayList<>(); if (exception instanceof ConstraintViolationException) { ConstraintViolationException cve = (ConstraintViolationException) exception; for (ConstraintViolation<?> violation : cve.getConstraintViolations()) { errorMessages.add(String.format("%s: %s", violation.getPropertyPath(), violation.getMessage())); } } return Response.status(Response.Status.BAD_REQUEST) .entity(errorMessages) .type(MediaType.APPLICATION_JSON) .build(); } }
记得把这个ExceptionMapper也注册到你的JerseyAppConfig里。
这样操作后,你的自定义类型就能正常使用@Min/@Max做校验啦,再也不会出现“无可用校验器”的错误了!
内容的提问来源于stack exchange,提问作者Nathan

