【问题标题】:Spring service method and a complex validation logic/rulesSpring服务方法和复杂的验证逻辑/规则
【发布时间】:2017-02-02 07:11:24
【问题描述】:

在我的 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 框架来完成。

【问题讨论】:

  • 大部分情况下使用 JSR-303,并且可能有一个自定义验证类来检查复合 logiv。例如,您的前几次检查可以减少到@NotEmpty

标签: java spring spring-boot spring-validator


【解决方案1】:

正如 cmets 中提到的 chrylis,您可以通过使用 JSR-303 bean 验证来实现此目标。第一步是创建一个包含输入参数的类:

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)。在您的注解类上,确保添加@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 类并使其实现ConstraintValidator&lt;ValidTenant, DecisionInput&gt;,例如:

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

如果你想使用自己的错误信息,我建议阅读this answer。基本上你创建一个ValidationMessages.properties 文件并将你的消息放在那里。

【讨论】:

  • 感谢您的详细解答!另一个问题 - 所有验证调用是否与服务方法在同一事务中执行?
  • @alexanoid 我不是 100% 确定,但据我所知方面是在相同的事务上下文中执行的,所以我的猜测是验证逻辑也将在相同的上下文中执行。
猜你喜欢
  • 2011-09-21
  • 1970-01-01
  • 2015-05-22
  • 1970-01-01
  • 2012-08-21
  • 1970-01-01
  • 2013-09-02
  • 2020-09-06
  • 2010-12-04
相关资源
最近更新 更多