【问题标题】:Bean Validation with Extra Information带有额外信息的 Bean 验证
【发布时间】:2019-07-06 18:08:06
【问题描述】:

我正在尝试创建一个 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 中?还是有更好的方法来处理这个验证?这只是一个大对象的一小部分,真正的对象在请求处理程序中有许多其他复杂的验证,这使得代码很脏。

【问题讨论】:

  • 不确定,但您可以使用 Spring 的表达式语言 (SpEL)。使用@Autowired Request request 创建服务。现在,将此表达式添加到@interface 属性中:#{requestService.getField('userId')}。如果需要静态访问,替换为T(package.class):#{T(org.organization...StaticRequestAccess).getField('userId')}
  • ConstrainValidator 应该是静态的,这样做非常丑陋和混乱,我认为这是不可能的,至少以一种可读的正确方式是不可能的。您应该在控制器接受请求后进行验证,因为此验证超出了基本验证,而是需要读取数据库的逻辑验证。
  • 这样使用validation-api不是一个好习惯,你应该在领域层处理这个约束
  • @EduardoEljaiek 你能给我看一个在域层进行这种验证的例子吗?也许只是您在 Internet 上找到的示例的链接
  • 我没有尝试这个,因此没有添加这个作为答案,但是博客中的最​​后一个例子是 @RequestMapping(value = "/path/{id}", method = RequestMethod.GET) public SampleDomainObject get(@ModelAttribute("id") @Validated(IdValidator.class) String id) { return ... // 获取并返回对象 } 应该适用于您的情况。更多信息,copyrightdev.tumblr.com/post/92560458673/…

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


【解决方案1】:

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

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

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

【讨论】:

  • 是的,我认为某种验证器很好。我正在考虑这个问题。你能给我一些很好的例子来实现这个模式吗?我确实有一些想法,但不够成熟。来源是martinfowler.com/articles/replaceThrowWithNotification.html
  • 很抱歉,我不太确定您指的是我的回答的哪一部分,您能否澄清一下?
【解决方案2】:

如果我没记错的话,您要问的是,如何将您的 userId 传递给您的自定义注释,即 @UniqueName,以便您可以访问 userId 以针对现有 @ 验证 projectName 字段987654327@ 已通过userId

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

您也可以参考以下答案:

How to pass value to custom annotation in java?

Passing dynamic parameters to an annotation?

【讨论】:

  • @aisensiy 那么你打算怎么做?你有什么捷径吗?
  • 我想我应该尝试一些验证模式而不是强制使用注释。
【解决方案3】:

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

  • JPA 验证 功能有限,但对于实体类的最简单约束,如果这样的话,它是一个不错的选择 约束可以映射到 DDL。

  • Bean Validation 是一种灵活、简洁、声明性、可重用和可读的方法,涵盖了您可能在 你的领域模型类。这是最好的选择,在大多数情况下, 一旦您不需要在事务中运行验证。

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

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

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

我认为 实体侦听器 符合您的 唯一约束 验证问题,因为在实体侦听器中,您将能够在持久/更新和执行之前访问您的 JPA 实体您的支票查询更容易。

然而,正如@crizzis 指出的那样,这种方法存在很大的限制。如 JPA 2 规范 (JSR 317) 所述:

一般来说,可移植应用程序的生命周期方法不应该 调用 EntityManager 或 Query 操作,访问其他实体 实例,或修改同一持久性中的关系 语境。生命周期回调方法可能会修改非关系 调用它的实体的状态。

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

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

最后,我将这个注解添加到我的Entity Class @EntityListeners(GatewaUniqueIpv4sListener.class)

你可以在这里找到完整的工作代码gateways-java

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

【讨论】:

  • JPA 规范:'一般来说,可移植应用程序的生命周期方法不应调用 EntityManager 或查询操作、访问其他实体实例或修改同一持久性上下文中的关系'
  • 感谢@crizzis,很高兴知道这一点。你能建议另一种方法吗?
  • @crizzis Ups 抱歉,我会看到的
  • @crizzis 感谢您的更正,我已将其包含在我的答案中
【解决方案4】:

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

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

这是我们创建的一个验证器,它确保两个字段在提交时具有相同的值。想想你经常看到网站的密码和确认密码用例,或者电子邮件和确认电子邮件用例。

当然,在您的特定情况下,您需要传入用户的 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://*.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://*.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;

    ...
}

【讨论】: