我正在尝试创建一个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
中呢?还是有一些更好的方法来处理此验证?这只是一个大对象的一小部分,实际对象在请求处理程序中还有许多其他复杂的验证,这些验证使代码很脏。
答案 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}}字段。
这意味着您要问的是,如何将变量/参数动态传递给注释,这是不可能的。您必须使用其他方法,例如拦截器或手动进行验证。
您也可以参考以下答案:
答案 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;
...
}