引子
在做java web服务端时,经常要处理http请求传入参数的校验。从最开始的源生代码校验到校验方法的封装再到使用注解封装校验逻辑等,目的是在完成校验任务的基础上简化校验部分代码工作量。
@GetMapping("test")
public ResultInfo testInfo(TestBean demo){
if(demo.getId() == null || demo.getId()<= 0){
return ResultInfo.error("id不能为空");
}
if(StringUtils.isEmpty(demo.getName())){
return ResultInfo.error("name不能为空");
}
...
}
@GetMapping("test")
public ResultInfo testInfo(@Valid TestBean demo, BindingResult bindingResult){
if (bindingResult.hasErrors()) {
List<ObjectError> errors = bindingResult.getAllErrors();
return ResultInfo.error(errors.get(0).getDefaultMessage());
}
...
}
class TestBean{
@NotNull(message = "id不能为空")
private Integer id;
@NotNull(message = "name不能为空")
@Length(max=50 , message = "name长度必须小于50个字符")
private String name;
}
因系统复杂程度不同,业务逻辑千差万别的情况下每个请求的接口的参数校验也会有很大的不同。使用源生方法校验参数,经常要思考的问题是,这些校验代码放在哪里,是否可以重用。在使用Valid注解校验参数时,需要对每个接口校验逻辑进行分组(使用注解的groups参数);当分组数太多时,校验逻辑分散在不同的成员变量上,加大了阅读调整的工作量;对于比较复杂的关联校验(当成员变量A为某个特定值时,B可以为空,其他情况不为空)无能为力。
@NotNull(groups = {Edit.class}, message = "id不能为空")
private Integer id;
...
public interface Edit {}
...
@GetMapping("test")
public ResultInfo testInfo(@Validated(TestBean.Edit.class)TestBean demo, BindingResult bindingResult){
}
解决思路
引入领域对象的思想,1.将参数校验逻辑放在对应的bean中(同Valid注解思路),2.使用spring中Assert封装的校验参数方法/StringUtils/CollectionUtils等,集中处理参数校验逻辑。这样处理的优点在于:手写代码进行参数校验,有最大的灵活性;校验逻辑封装在bean中,可以达到最大的复用率;参数校验逻辑集中在一块,方便调整。存在的问题是Assert中的校验方法并不充足,很多校验逻辑需要自己手写;使得bean中的代码量变得庞大。
class TestBean{
private Integer id;
private String name;
private List<SubTestBean> subBeans;
public void validateFields(){
Assert.notNull(id, "id不能为空");
Assert.hasText(name, "name不能为空");
if(CollectionUtils.isNotEmpty(subBeans)){
subBeans.forEach(item->item.validateFields());
}
}
}
引申应用
- 默认值的处理
场景:在数据库设计时,很多DBA会要求字段默认值不能为NULL,那么一般在处理数字字段时,会默认0。但是实际业务却要区分用户没填值和填0的两种情况。这个时候可以考虑将字段默认值设为一个永不到的值,譬如金额/比例使用decimal default '-0.00001',在读取数据后返回用户前,再将这些默认值处理一下。
每次用户需要这些数据的时候,都需要处理一遍,使用频率非常高,所以这些处理默认值逻辑的代码,同样适用于放在对应的bean中。另外如果有新增或者减少字段,其默认值的处理调整也可以直接定位到对应的bean中去调整。
class TestBean{
private static BigDecimal DEFAULT_DB_DECIMAL = new BigDecimal("-0.00001");
private BigDecimal money;
public void dealDefaultFields(){
if(DEFAULT_DB_DECIMAL.compareTo(money) == 0 ){
money = null;
}
}
}
- 枚举类型的转化
场景:bean对象中会有些一些状态值等,在将数据返回用户之前,需要把这些数据转化成文字描述。
class TestBean{
private Integer status;
private String statusName;
public void translateEnumFields(){
StatusEnum targetEnum = StatusEnum.getInstance(status);
if(targetEnum != null){
statusName = targetEnum.getName();
}
}
}
思考
在处理参数校验时,校验逻辑可能涉及到数据中的值,譬如由用户输入某个编码作为业务数据,需要校验此编码是否已经被使用了。如果将这些校验逻辑也放入对应的bean中,是否可以考虑将数据库链接对象mapper作为validateFields方法的参数传入。
public void validateFields(DemoMapper demoMapper){
if(demoMapper.existCode(code)){
throw new IllegalArgumentException("编码"+code+"已经存在");
}
}