带有额外信息的Bean验证

时间:2019-07-06 18:08:06

标签: java spring spring-boot spring-mvc bean-validation

我正在尝试创建一个UniqueName注释,作为创建项目api的定制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;
   }
}

问题在于name字段仅需要用户级别的唯一性。因此,我需要从URL字段获取{userId}进行验证。但是如何将其添加到UniqueProjectNameValidator中呢?还是有一些更好的方法来处理此验证?这只是一个大对象的一小部分,实际对象在请求处理程序中还有许多其他复杂的验证,这些验证使代码很脏。

5 个答案:

答案 0 :(得分:1)

如果您试图从当前请求中获取信息,则可以在验证器中使用RequestContextHolder,如下所示:

HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

答案 1 :(得分:1)

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

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

肮脏的解决方案是使用Hibernate Validator的cross-parameter constraints(示例,另请参见this link)。您实际上将这两个控制器方法参数都作为自定义验证器的输入。

答案 2 :(得分:0)

如果我没看错,您要问的是,如何将userId传递到自定义注释即@UniqueName,以便您可以访问userId来验证{对于已通过projectName的现有projectNames的{​​1}}字段。

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

您也可以参考以下答案:

How to pass value to custom annotation in java?

Passing dynamic parameters to an annotation?

答案 3 :(得分:0)

@Mikhail Dyakonov在此article中提出了一个经验法则,以使用Java选择最佳验证方法:

  
      
  • JPA验证的功能有限,但如果对实体类进行最简单的约束,则它是一个很好的选择   约束可以映射到DDL。

  •   
  • Bean验证是一种灵活,简洁,声明性,可重用和可读的方式,涵盖了您可能拥有的大多数检查   您的域模型类。在大多数情况下,这是最佳选择,   无需在事务内运行验证。

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

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

  •   
  • 事务侦听器是在交易环境中起作用的危险但终极武器。需要决定时使用它   在运行时必须验证哪些对象或何时需要检查   针对相同验证的不同类型的实体   算法。

  •   

我认为实体侦听器与您的唯一约束验证问题匹配,因为在实体侦听器中,您可以在持久/更新和执行JPA实体之前访问它您的支票查询更加轻松。

但是,正如@crizzis指出的那样,这种方法存在很大的局限性。如JPA 2规范(JSR 317)中所述:

  

通常,便携式应用程序的生命周期方法不应   调用EntityManager或Query操作,访问其他实体   实例,或在相同持久性内修改关系   上下文。生命周期回调方法可能会修改非关系   对其进行调用的实体的状态。

是否尝试这种方法,首先需要一个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);
        }    
    }

这是我的实体监听器

@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);
       }
    }
}

最后,我将此注释添加到我的实体类 @EntityListeners(GatewaUniqueIpv4sListener.class)

您可以在此处gateways-java

中找到完整的工作代码。

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

答案 4 :(得分:0)

我相信您可以按照自己的要求做,但是您只需要概括一下您的方法即可。

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

这是我们创建的验证器,可确保提交时两个字段的值相同。考虑一下密码并确认您经常看到的密码用例,或者通过电子邮件确认电子邮件用例。

当然,在您的特定情况下,您需要输入用户的ID和他们尝试创建的项目的名称。

注释:

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;

    ...
}