Spring 服务方法和复杂的验证 logic/rules
Spring service method and a complex validation logic/rules
在我的 Spring/Boot Java 项目中,我有一组服务方法,例如如下所示:
@Override
public Decision create(String name, String description, String url, String imageUrl, Decision parentDecision, Tenant tenant, User user) {
name = StringUtils.trimMultipleSpaces(name);
if (org.apache.commons.lang3.StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Decision name can't be blank");
}
if (!org.apache.commons.lang3.StringUtils.isEmpty(url) && !urlValidator.isValid(url)) {
throw new IllegalArgumentException("Decision url is not valid");
}
if (!org.apache.commons.lang3.StringUtils.isEmpty(imageUrl) && !urlValidator.isValid(imageUrl)) {
throw new IllegalArgumentException("Decision imageUrl is not valid");
}
if (user == null) {
throw new IllegalArgumentException("User can't be empty");
}
if (tenant != null) {
List<Tenant> userTenants = tenantDao.findTenantsForUser(user.getId());
if (!userTenants.contains(tenant)) {
throw new IllegalArgumentException("User doesn't belong to this tenant");
}
}
if (parentDecision != null) {
if (tenant == null) {
if (findFreeChildDecisionByName(parentDecision.getId(), name) != null) {
throw new EntityAlreadyExistsException("Parent decision already contains a child decision with a given name");
}
} else {
if (findTenantedChildDecisionByName(parentDecision.getId(), name, tenant.getId()) != null) {
throw new EntityAlreadyExistsException("Parent decision already contains a child decision with a given name");
}
}
Tenant parentDecisionTenant = tenantDao.findTenantForDecision(parentDecision.getId());
if (parentDecisionTenant != null) {
if (tenant == null) {
throw new IllegalArgumentException("Public decision cannot be added as a child to tenanted parent decision");
}
if (!parentDecisionTenant.equals(tenant)) {
throw new IllegalArgumentException("Decision cannot belong to tenant other than parent decision tenant");
}
} else {
if (tenant != null) {
throw new IllegalArgumentException("Tenanted decision cannot be added as a child to public parent decision");
}
}
} else {
if (tenant == null) {
if (findFreeRootDecisionByName(name) != null) {
throw new EntityAlreadyExistsException("Root decision with a given name already exists");
}
} else {
if (findTenantedRootDecisionByName(name, tenant.getId()) != null) {
throw new EntityAlreadyExistsException("Root decision with a given name for this tenant already exists");
}
}
}
Decision decision = createOrUpdate(new Decision(name, description, url, imageUrl, parentDecision, user, tenant));
if (parentDecision != null) {
parentDecision.addChildDecision(decision);
}
criterionGroupDao.create(CriterionGroupDaoImpl.DEFAULT_CRITERION_GROUP_NAME, null, decision, user);
characteristicGroupDao.create(CharacteristicGroupDaoImpl.DEFAULT_CHARACTERISTIC_GROUP_NAME, null, decision, user);
return decision;
}
如您所见,此方法的大部分代码行都被验证逻辑占用,我继续在那里添加新的验证用例。
我想重构此方法并将验证逻辑移到此方法之外的更合适的位置。请建议如何使用 Spring 框架来完成。
正如评论中提到的 chrylis,您可以通过使用 JSR-303 bean 验证来实现这个目标。第一步是创建一个包含您的输入参数的 class:
public class DecisionInput {
private String name;
private String description;
private String url;
private String imageUrl;
private Decision parentDecision;
private Tenant tenant;
private User user;
// Constructors, getters, setters, ...
}
之后就可以开始添加验证注解了,例如:
public class DecisionInput {
@NotEmpty
private String name;
@NotEmpty
private String description;
@NotEmpty
private String url;
@NotEmpty
private String imageUrl;
private Decision parentDecision;
private Tenant tenant;
@NotNull
private User user;
// Constructors, getters, setters, ...
}
请注意,@NotEmpty
注释不是标准的 JSR-303 注释,而是 Hibernate 注释。如果您更喜欢使用标准的 JSR-303,您始终可以创建自己的自定义验证器。对于您的租户和您的决定,您当然需要一个自定义验证器。首先创建一个注释(例如@ValidTenant
)。在您的注释 class 上,确保添加 @Constraint
注释,例如:
@Constraint(validatedBy = TenantValidator.class) // Your validator class
@Target({ TYPE, ANNOTATION_TYPE }) // Static import from ElementType, change this to METHOD/FIELD if you want to create a validator for a single field (rather than a cross-field validation)
@Retention(RUNTIME) // Static import from RetentionPolicy
@Documented
public @interface ValidTenant {
String message() default "{ValidTenant.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
现在您必须创建 TenantValidator
class 并使其实现 ConstraintValidator<ValidTenant, DecisionInput>
,例如:
@Component
public class TenantValidator implements ConstraintValidator<ValidTenant, DecisionInput> {
@Autowired
private TenantDAO tenantDao;
@Override
public void initialize(ValidTenant annotation) {
}
@Override
public boolean isValid(DecisionInput input, ConstraintValidatorContext context) {
List<Tenant> userTenants = tenantDao.findTenantsForUser(input.getUser().getId());
return userTenants.contains(input.getTenant());
}
}
可以对父决策的验证执行相同的操作。现在您可以将您的服务方法重构为:
public Decision create(@Valid DecisionInput input) {
// No more validation logic necessary
}
如果您想使用自己的错误消息,我建议您阅读 。基本上你创建一个 ValidationMessages.properties
文件并将你的消息放在那里。
在我的 Spring/Boot Java 项目中,我有一组服务方法,例如如下所示:
@Override
public Decision create(String name, String description, String url, String imageUrl, Decision parentDecision, Tenant tenant, User user) {
name = StringUtils.trimMultipleSpaces(name);
if (org.apache.commons.lang3.StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Decision name can't be blank");
}
if (!org.apache.commons.lang3.StringUtils.isEmpty(url) && !urlValidator.isValid(url)) {
throw new IllegalArgumentException("Decision url is not valid");
}
if (!org.apache.commons.lang3.StringUtils.isEmpty(imageUrl) && !urlValidator.isValid(imageUrl)) {
throw new IllegalArgumentException("Decision imageUrl is not valid");
}
if (user == null) {
throw new IllegalArgumentException("User can't be empty");
}
if (tenant != null) {
List<Tenant> userTenants = tenantDao.findTenantsForUser(user.getId());
if (!userTenants.contains(tenant)) {
throw new IllegalArgumentException("User doesn't belong to this tenant");
}
}
if (parentDecision != null) {
if (tenant == null) {
if (findFreeChildDecisionByName(parentDecision.getId(), name) != null) {
throw new EntityAlreadyExistsException("Parent decision already contains a child decision with a given name");
}
} else {
if (findTenantedChildDecisionByName(parentDecision.getId(), name, tenant.getId()) != null) {
throw new EntityAlreadyExistsException("Parent decision already contains a child decision with a given name");
}
}
Tenant parentDecisionTenant = tenantDao.findTenantForDecision(parentDecision.getId());
if (parentDecisionTenant != null) {
if (tenant == null) {
throw new IllegalArgumentException("Public decision cannot be added as a child to tenanted parent decision");
}
if (!parentDecisionTenant.equals(tenant)) {
throw new IllegalArgumentException("Decision cannot belong to tenant other than parent decision tenant");
}
} else {
if (tenant != null) {
throw new IllegalArgumentException("Tenanted decision cannot be added as a child to public parent decision");
}
}
} else {
if (tenant == null) {
if (findFreeRootDecisionByName(name) != null) {
throw new EntityAlreadyExistsException("Root decision with a given name already exists");
}
} else {
if (findTenantedRootDecisionByName(name, tenant.getId()) != null) {
throw new EntityAlreadyExistsException("Root decision with a given name for this tenant already exists");
}
}
}
Decision decision = createOrUpdate(new Decision(name, description, url, imageUrl, parentDecision, user, tenant));
if (parentDecision != null) {
parentDecision.addChildDecision(decision);
}
criterionGroupDao.create(CriterionGroupDaoImpl.DEFAULT_CRITERION_GROUP_NAME, null, decision, user);
characteristicGroupDao.create(CharacteristicGroupDaoImpl.DEFAULT_CHARACTERISTIC_GROUP_NAME, null, decision, user);
return decision;
}
如您所见,此方法的大部分代码行都被验证逻辑占用,我继续在那里添加新的验证用例。
我想重构此方法并将验证逻辑移到此方法之外的更合适的位置。请建议如何使用 Spring 框架来完成。
正如评论中提到的 chrylis,您可以通过使用 JSR-303 bean 验证来实现这个目标。第一步是创建一个包含您的输入参数的 class:
public class DecisionInput {
private String name;
private String description;
private String url;
private String imageUrl;
private Decision parentDecision;
private Tenant tenant;
private User user;
// Constructors, getters, setters, ...
}
之后就可以开始添加验证注解了,例如:
public class DecisionInput {
@NotEmpty
private String name;
@NotEmpty
private String description;
@NotEmpty
private String url;
@NotEmpty
private String imageUrl;
private Decision parentDecision;
private Tenant tenant;
@NotNull
private User user;
// Constructors, getters, setters, ...
}
请注意,@NotEmpty
注释不是标准的 JSR-303 注释,而是 Hibernate 注释。如果您更喜欢使用标准的 JSR-303,您始终可以创建自己的自定义验证器。对于您的租户和您的决定,您当然需要一个自定义验证器。首先创建一个注释(例如@ValidTenant
)。在您的注释 class 上,确保添加 @Constraint
注释,例如:
@Constraint(validatedBy = TenantValidator.class) // Your validator class
@Target({ TYPE, ANNOTATION_TYPE }) // Static import from ElementType, change this to METHOD/FIELD if you want to create a validator for a single field (rather than a cross-field validation)
@Retention(RUNTIME) // Static import from RetentionPolicy
@Documented
public @interface ValidTenant {
String message() default "{ValidTenant.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
现在您必须创建 TenantValidator
class 并使其实现 ConstraintValidator<ValidTenant, DecisionInput>
,例如:
@Component
public class TenantValidator implements ConstraintValidator<ValidTenant, DecisionInput> {
@Autowired
private TenantDAO tenantDao;
@Override
public void initialize(ValidTenant annotation) {
}
@Override
public boolean isValid(DecisionInput input, ConstraintValidatorContext context) {
List<Tenant> userTenants = tenantDao.findTenantsForUser(input.getUser().getId());
return userTenants.contains(input.getTenant());
}
}
可以对父决策的验证执行相同的操作。现在您可以将您的服务方法重构为:
public Decision create(@Valid DecisionInput input) {
// No more validation logic necessary
}
如果您想使用自己的错误消息,我建议您阅读 ValidationMessages.properties
文件并将你的消息放在那里。