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