Java 带有额外信息的Bean验证

Java 带有额外信息的Bean验证,java,spring,spring-boot,spring-mvc,bean-validation,Java,Spring,Spring Boot,Spring Mvc,Bean Validation,我正在尝试创建一个UniqueName注释,作为创建项目api的cutomize bean验证注释: @PostMapping("/users/{userId}/projects") public ResponseEntity createNewProject(@PathVariable("userId") String userId, @RequestBody @Valid ProjectParam projectP

我正在尝试创建一个
UniqueName
注释,作为创建项目api的cutomize bean验证注释:

@PostMapping("/users/{userId}/projects")
public ResponseEntity createNewProject(@PathVariable("userId") String userId,
                                       @RequestBody @Valid ProjectParam projectParam) {
    User projectOwner = userRepository.ofId(userId).orElseThrow(ResourceNotFoundException::new);

    Project project = new Project(
        IdGenerator.nextId(),
        userId,
        projectParam.getName(),
        projectParam.getDescription()
    );
    ...
  }

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
class ProjectParam {

  @NotBlank
  @NameConstraint
  private String name;
  private String description;
}

@Constraint(validatedBy = UniqueProjectNameValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
public @interface UniqueName {

    public String message() default "already existed";

    public Class<?>[] groups() default {};

    public Class<? extends Payload>[] payload() default{};
}

public class UniqueProjectNameValidator implements ConstraintValidator<UniqueName, String> {
   @Autowired
   private ProjectQueryMapper mapper;

   public void initialize(UniqueName constraint) {
   }

   public boolean isValid(String value, ConstraintValidatorContext context) {
      // how can I get the userId info??
      return mapper.findByName(userId, value) == null;
   }
}
@PostMapping(“/users/{userId}/projects”)
public ResponseEntity createNewProject(@PathVariable(“userId”)字符串userId,
@RequestBody@Valid ProjectParam ProjectParam){
User projectOwner=userRepository.ofId(userId).orelsetrow(ResourceNotFoundException::new);
项目=新项目(
IdGenerator.nextId(),
用户ID,
projectParam.getName(),
projectParam.getDescription()
);
...
}
@吸气剂
@NoArgsConstructor(access=AccessLevel.PRIVATE)
类ProjectParam{
@不空白
@名称约束
私有字符串名称;
私有字符串描述;
}
@约束(validatedBy=UniqueProjectNameValidator.class)
@保留(RetentionPolicy.RUNTIME)
@目标({ElementType.FIELD})
public@interface UniqueName{
public String message()默认为“已存在”;
公共类[]组()默认值{};

public Class如果我没有错,那么您要问的是,如何将您的
用户ID
传递给您的自定义注释,即
@UniqueName
,以便访问
用户ID
,针对已传递的
用户ID
验证
projectName
字段

这意味着您要问的是,如何将变量/参数动态传递给注释,这是不可能的。您必须使用其他方法,如拦截器手动进行验证

您也可以参考以下答案:


正如@Abhijeet所提到的,动态地将
userId
属性传递给约束验证器是不可能的。至于如何更好地处理这个验证案例,有干净的解决方案和脏的解决方案

干净的解决方案是将所有业务逻辑提取到服务方法,并在服务级别验证
ProjectParam
。这样,您可以在调用ser之前,将
userId
属性添加到
ProjectParam
,并将其从
@PathVariable
映射到
@RequestBody
反之。然后调整
uniqueprojectnamevalidater
以验证
ProjectParam
s,而不是
String
s

肮脏的解决方案是使用Hibernate验证程序(另请参见示例)。实际上,您将两个控制器方法参数都视为自定义验证程序的输入。

@Mikhail Dyakonov在本文中提出了使用java选择最佳验证方法的经验法则:

  • JPA验证的功能有限,但对于实体类上最简单的约束来说,它是一个很好的选择 约束可以映射到DDL

  • Bean验证是一种灵活、简洁、声明性、可重用和可读的方法,可以覆盖您在测试中可能遇到的大多数检查 您的域模型类。在大多数情况下,这是最佳选择, 一旦您不需要在事务中运行验证

  • 契约验证是方法调用的Bean验证。当需要检查方法的输入和输出参数时,可以使用它 方法,例如,在REST调用处理程序中

  • 实体监听器尽管它们不像Bean验证注释那样具有声明性,但它们是检查大数据的好地方 对象的图形或进行需要在 数据库事务。例如,当您需要读取某些数据时 从数据库中做出决定,Hibernate有类似的例子 听众

  • 事务侦听器是在事务上下文中工作的一种危险但终极的武器。需要决定时使用它 在运行时,必须验证哪些对象,或者何时需要检查 针对同一验证的不同类型的实体 算法

我认为实体侦听器与您独特的约束验证问题相匹配,因为在实体侦听器中,您可以在持久化/更新JPA实体和执行检查查询之前访问它

然而,正如@crizzis向我指出的,这种方法有一个重大限制。如JPA2规范(JSR 317)所述:

通常,便携式应用程序的生命周期方法不应 调用EntityManager或查询操作,访问其他实体 实例,或修改同一持久性中的关系 生命周期回调方法可能会修改非关系 对其调用的实体的状态

无论您是否尝试这种方法,首先您将需要一个
ApplicationContextAware
实现来获取当前的
EntityManager
实例。这是一个旧的Spring框架技巧,可能您已经在使用它了

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public final class BeanUtil implements ApplicationContextAware {

   private static ApplicationContext CONTEXT;

        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            CONTEXT = applicationContext;
        }

        public static <T> T getBean(Class<T> beanClass) {
            return CONTEXT.getBean(beanClass);
        }    
    }
最后,我将此注释添加到我的实体类
@EntityListeners(GatewaUniqueIpv4sListener.Class)

您可以在这里找到完整的工作代码


一种干净、简单的方法可以是检查验证,在这种方法中,您需要访问数据库。甚至您也可以使用规范、策略和责任链模式来实施更好的解决方案。

我相信您可以按要求做,但您可能需要稍微概括一下您的方法

正如其他人所提到的,您不能将两个属性传递给验证器,但是,如果您将验证器更改为类级验证器而不是字段级验证器,那么它可以工作

下面是我们创建的验证程序,它确保提交时两个字段的值相同
@Slf4j
public class GatewaUniqueIpv4sListener { 

    @PrePersist
    void onPrePersist(Gateway gateway) {       
       try {
           EntityManager entityManager = BeanUtil.getBean(EntityManager.class);
           Gateway entity = entityManager
                .createQuery("SELECT g FROM Gateway g WHERE g.ipv4 = :ipv4", Gateway.class)
                .setParameter("ipv4", gateway.getIpv4())
                .getSingleResult();

           // Already exists a Gateway with the same Ipv4 in the Database or the PersistenceContext
           throw new IllegalArgumentException("Can't be to gateways with the same Ip address " + gateway.getIpv4());
       } catch (NoResultException ex) {
           log.debug(ex.getMessage(), ex);
       }
    }
}
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Taken from:
 * http://stackoverflow.com/questions/1972933/cross-field-validation-with-hibernate-validator-jsr-303
 * <p/>
 * Validation annotation to validate that 2 fields have the same value.
 * An array of fields and their matching confirmation fields can be supplied.
 * <p/>
 * Example, compare 1 pair of fields:
 *
 * @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match")
 * <p/>
 * Example, compare more than 1 pair of fields:
 * @FieldMatch.List({
 * @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
 * @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")})
 */
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
public @interface FieldMatch {
    String message() default "{constraints.fieldmatch}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * @return The first field
     */
    String first();

    /**
     * @return The second field
     */
    String second();

    /**
     * Defines several <code>@FieldMatch</code> annotations on the same element
     *
     * @see FieldMatch
     */
    @Target({TYPE, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        FieldMatch[] value();
    }
}
import org.apache.commons.beanutils.BeanUtils;

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

/**
 * Taken from:
 * http://stackoverflow.com/questions/1972933/cross-field-validation-with-hibernate-validator-jsr-303
 */
public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {
    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(FieldMatch constraintAnnotation) {

        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {

        try {
            Object firstObj = BeanUtils.getProperty(value, firstFieldName);
            Object secondObj = BeanUtils.getProperty(value, secondFieldName);

            return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
        } catch (Exception ignore) {
            // ignore
        }
        return true;
    }
}
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;

import javax.validation.GroupSequence;

@GroupSequence({Required.class, Type.class, Data.class, Persistence.class, ChangePasswordCommand.class})
@FieldMatch(groups = Data.class, first = "password", second = "confirmNewPassword", message = "The New Password and Confirm New Password fields must match.")
public class ChangePasswordCommand {

    @NotBlank(groups = Required.class, message = "New Password is required.")
    @Length(groups = Data.class, min = 6, message = "New Password must be at least 6 characters in length.")
    private String password;

    @NotBlank(groups = Required.class, message = "Confirm New Password is required.")
    private String confirmNewPassword;

    ...
}