当进行DDD编程过程中,有个非常繁琐,但又是非常重要的步骤:整理出某个实体的所有业务字段约束。
关于业务校验,业界已经有一些框架支持,如validator框架。但大部分人只用于参数校验,并未将其用于实体内。
本人整理出了一种声明式表达业务的方式。写起约束来个人感觉比较聚焦,从而可以辅助思考和整理约束。
直接上示例代码:
以下代码体现了一个品牌的业务约束和行为操作。
- BrandNameDuplicateChecker:展示针对实体的复杂校验
- BrandLogoChecker:展示针对实体单个字段的复杂校验
- Spec:复杂校验注解声明,内部类SpecChecker是关键代码
品牌实体
@Data
@Setter(AccessLevel.PROTECTED)
@Accessors(chain = true)
@Spec(value = BrandNameDuplicateChecker.class) //复杂规则校验,一般涉及bean依赖的校验
public class Brand extends BaseEntity<Brand> {
@ApiModelProperty("品牌ID")
private Long id;
@Size(min=1, max=100, message = "品牌名称字符数限制为[1,100]")
@ApiModelProperty("品牌名称")
private String name;
@Spec(value = BrandLogoChecker.class)
@Pattern(regexp = "(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]", message = "logo必须为合法的URL")
@Size(min=1, max = 128, message = "品牌名称长度必须在1-128字符内")
@ApiModelProperty("品牌logo,为标准的URL图片格式")
private String logo;
@ApiModelProperty("排序分值")
private Long score;
@ApiModelProperty("是否隐藏")
private Boolean hidden;
@ApiModelProperty("版本号")
private Long version;
@ApiModelProperty("创建时间")
private Date createdAt;
@ApiModelProperty("更新时间")
private Date updatedAt;
protected Brand(){}
public Brand(Long id, String name, String logo, Long score, Boolean hidden) {
this.id = id;
this.name = name;
this.logo = logo;
this.score = score;
this.hidden = hidden;
version = 0L;
createdAt = new Date();
updatedAt = new Date();
this.validate(); //触发校验
}
public Brand changeName(String name){
Brand toUpdate = new Brand().setId(this.id).setName(name);
toUpdate.validate("name");
DomainRegistry.bean(BrandNameDuplicateChecker.class).check(toUpdate);
this.name = name;
return this;
}
public Brand changeLogo(String logo){
Brand toUpdate = new Brand().setId(this.id).setLogo(logo);
toUpdate.validate("logo");
this.logo = logo;
return this;
}
public Brand changeScore(Long score){
Brand toUpdate = new Brand().setId(this.id).setScore(score);
toUpdate.validate("score");
this.score = score;
return this;
}
public Brand hidden(){
this.hidden = true;
return this;
}
public Brand visible(){
this.hidden = false;
return this;
}
public static Brand load(Long id){
return Optional.ofNullable(DomainRegistry.repo(BrandRepo.class).findById(id))
.orElseThrow(()->new BusinessException("品牌不存在"));
}
}
针对实体本身的复杂校验
@Component
@Slf4j
public class BrandNameDuplicateChecker implements Checker<Brand>{
@Autowired
@Setter
private BrandRepo brandRepo;
@Override
public void check(Brand brand) {
if(Strings.isNullOrEmpty(brand.getName())){
return;
}
Brand exist = brandRepo.findByName(brand.getName());
if(exist!=null && ! Objects.equals(brand.getId(), exist.getId())){
throw new BusinessException("品牌名称已经被使用");
}
}
}
针对实体字段的复杂校验
@Component
public class BrandLogoChecker implements Checker<String> {
@Autowired
@Setter
private PhotoUrlChecker photoUrlChecker;
@Override
public void check(String logo) {
if(photoUrlChecker.invalid(logo)){
throw new BusinessException("品牌Logo的URL不合法");
}
}
}
复杂校验实现
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = { Spec.SpecChecker.class })
public @interface Spec {
Class<? extends Checker>[] value();
String message() default "字段不符合条件约束";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Slf4j
public static class SpecChecker implements ConstraintValidator<Spec, Object> {
Spec annotation;
List<Class<? extends Checker>> checkerClasses;
Map<Class<? extends Checker>, ? extends Checker> allCheckers;
@Override
public void initialize(Spec constraintAnnotation) {
annotation = constraintAnnotation;
checkerClasses = Arrays.asList(annotation.value());
allCheckers = DomainRegistry.beanMap(Checker.class)
.values().stream()
.collect(Collectors.toMap(Checker::getClass, Function.identity()));
}
@Override
public boolean isValid(Object object, ConstraintValidatorContext constraintValidatorContext) {
try {
StringBuilder sb = new StringBuilder();
checkerClasses.forEach(c->{
Checker checker = allCheckers.get(c);
if(checker!=null){
try {
checker.check(object);
}catch (Exception e){
sb.append(e.getMessage()).append(" ");
}
}
});
if(sb.length()>0) {
constraintValidatorContext.disableDefaultConstraintViolation();
constraintValidatorContext
.buildConstraintViolationWithTemplate(sb.toString())
.addConstraintViolation();
return false;
}else{
return true;
}
}catch (Exception e){
log.warn("", e);
return false;
}
}
}
}
应用服务
@Transactional
public Brand create(@Valid BrandCreateParam param){
Brand brand = new Brand(
idGen.generateId(),
param.getName(),
param.getLogo(),
param.getScore(),
param.getHidden()
);
brandRepo.create(brand);
return brand;
}
具体代码已经放在github上:Brand.java