一、背景
最近很多博主都在讨论对象参数校验的问题,我维护的群里也时不时的讨论这方面的内容。毕竟对象校验在业务代码中是比较常见的,当然,这种对象校验的存在也极其尴尬。因为对象参数校验的场景太多,没有任何一种方式方法能完全满足所有情况,本文将带你体验不同对象参数校验的场景并辅以大量代码示例。希望最后你可以借助本文找到适合你的对象参数校验解决方案。借用孔哥的话说就是茴香豆的茴字我不纠结,我纠结的是哪种味道的茴香豆好吃。
二、场景案例
2.1 校验逻辑在facadeImpl&controller中
这里演示一下比较常见的校验逻辑实现内容,首先看facadeImpl的某方法
public class TradeFacadeImpl{
//创建交易单--实现1
public String createTradeOrder(TradeOrderDto tradeOrder){
if(tradeOrder.sellerId == null || tradeOrder.sellerId <=0L){
return "卖家ID不能为空";
}
}
//创建交易单--实现2
public ResultDataDto createTradeOrder(TradeOrderDto tradeOrder){
if(tradeOrder.sellerId == null || tradeOrder.sellerId <=0L){
return ResultDataDto.fail(500,"卖家ID不能为空");
}
}
//创建交易单--实现3
public ResultDataDto createTradeOrder(TradeOrderDto tradeOrder){
if(tradeOrder.sellerId == null || tradeOrder.sellerId <=0L){
throw new CheckParamException("卖家ID不能为空");
}
}
}
对于controller而言以上方法参数校验类似,后面会将controller的校验,很明显的这是比较常见的校验方式,有区别的就在于校验失败后返回值的处理,当然优雅与否咱们先不管,起码它能工作。
2.2 校验逻辑在DTO,VO中
在这个场景中我们来看另一种校验逻辑在哪里,还是以上面的TradeFacadeImpl的createTradeOrder方法为例,我们看一下下面的代码:
public class TradeFacadeImpl{
//创建交易单--实现1
public String createTradeOrder(TradeOrderDto tradeOrder){
String checkResult = tradeOrder.check();
if(checkResult != null && checkResult !=""){
return checkResult;
}
}
//创建交易单--实现2
public ResultDataDto createTradeOrder(TradeOrderDto tradeOrder){
ResultDataDto checkResult = tradeOrder.check();
if(!checkResult.isSuccess()){
return checkResult;
}
}
}
public class TradeOrderDto{
private Long sellerId;
private String sellerName;
private String customerId;
private String customerName;
//校验实现1
public String check(){
if(this.sellerId == null || this.sellerId <= 0L){
return "卖家ID不能为空";
//return "sellerId不能为空";
//return "卖家ID【sellerId】不能为空";
}
return "success";
//return null;
}
//校验实现2
public ResultDataDto check(){
if(this.sellerId == null || this.sellerId <= 0L){
return ResultDataDto.fail(500,"卖家ID不能为空");
//return ResultDataDto.fail(500,"sellerId不能为空");
//return ResultDataDto.fail(500,"卖家ID【sellerId】不能为空");
}
return ResultDataDto.success();
}
}
这种场景就是将校验逻辑与DTO,VO放在一起了,所以其区别也就是返回参数形式不一样。
2.3 校验逻辑在AOP注解中
同上,我们用AOP类对TradeFacadeImpl的createTradeOrder方法进行切面参数校验,这里我们看一下这种形式的校验代码层面是怎么做的:
public class TradeFacadeImpl{
//创建交易单--实现1--这里我们返回创建后的dto
public TradeOrderDto createTradeOrder(TradeOrderDto tradeOrder){
String checkResult = tradeOrder.check();
if(checkResult != null && checkResult !=""){
return checkResult;
}
}
//创建交易单--实现2--这里直接包装
public ResultDataDto createTradeOrder(TradeOrderDto tradeOrder){
ResultDataDto checkResult = tradeOrder.check();
if(!checkResult.isSuccess()){
return checkResult;
}
}
}
public class TradeOrderDto{
@NotNull()
private Long sellerId;
@NotNull()
private String sellerName;
private String customerId;
private String customerName;
}
public class ParamCheckAspect{
//切FacadeImpl的所有方法
@Pointcut("execution(public * com.spring.aop.*FacadeImpl.*(..))")
private void pointCut(){};
//切面实现1--执行方法前校验
@Before(value = "pointCut()")
public void logStart(JoinPoint joinpoint) {
//切点对象
Object obj = point.getArgs()[0];
Class clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields){
field.setAccessible(true);
//field Annotation check
//field Value Check
//throw new RuntimeException(String.format("【%s】不能为空",field.getName));
}
}
//切面实现2--校验失败包装返回
@Around(value = "pointCut()")
public ResultDataDto logStart(JoinPoint joinpoint) {
//切点对象
Object obj = point.getArgs()[0];
Class clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
boolean check=false;
for (Field field : fields){
field.setAccessible(true);
//field Annotation check
//field Value Check
}
if(!check){
return ResultDataDto.fail(500,"参数校验失败");
}
point.proceed(args);
}
}
2.4 校验逻辑在Validator校验层中
前面几种分别围绕在方法入口和参数本身上面,这里我们尝试一下用单独的校验类来管理整体的校验逻辑,我们看一下下面的代码如何实现校验的,同上,我们仍然以TradeFacadeImpl为例:
@Service
public class TradeFacadeImpl{
@Autowired
private TradeValidator tradeValidator;
//创建交易单--实现1
public String createTradeOrder(TradeOrderDto tradeOrder){
String checkResult = tradeValidator.check(tradeOrder);
if(checkResult != null && checkResult !=""){
return checkResult;
}
}
//创建交易单--实现2--这里直接包装
public ResultDataDto createTradeOrder(TradeOrderDto tradeOrder){
ResultDataDto checkResult = tradeValidator.check(tradeOrder);
if(!checkResult.isSuccess()){
return checkResult;
}
}
}
//这里注入为spring bean
@Service
public class TradeValidator{
@Autowird
public TradeService tradeService;
//对象方法校验--实现1
public String check(TradeOrderDto tradeOrder){
if(this.sellerId == null || this.sellerId <= 0L){
return "卖家ID不能为空";
//return "sellerId不能为空";
//return "卖家ID【sellerId】不能为空";
}
return "success";
}
//对象方法校验--实现2
public ResultDataDto check(TradeOrderDto tradeOrder){
if(this.sellerId == null || this.sellerId <= 0L){
return ResultDataDto.fail(500,"卖家ID不能为空");
//return ResultDataDto.fail(500,"sellerId不能为空");
//return ResultDataDto.fail(500,"卖家ID【sellerId】不能为空");
}
return ResultDataDto.success();
//这里进行唯一性校验
//return checkUniq(tradeOrder);
}
//对象唯一性校验---这里扩展一种场景,对对象的唯一性校验在Validator实现
public ResultDataDto checkUniq(TradeOrderDto tradeOrder){
TradeOrderBO tradeOrderBo = tradeService.getByCondition(tradeOrder);
if(tradeOrderBo == null){
return ResultDataDto.success();
}
return ResultDataDto.fail(500,"该条交易记录已存在");
}
}
2.5 校验逻辑在工具类中
上面的大部分方法都将校验逻辑外置到其他类中,这里我们用工具类的思维看看如何解决这个问题,话不多说,我们看一下下面的代码片段:
@Service
public class TradeFacadeImpl{
@Autowird
public TradeService tradeService;
//创建交易单--实现1
public String createTradeOrder(TradeOrderDto tradeOrder){
String checkResult = ParamCheckUtil.checkParam(tradeOrder);
if(checkResult != null && checkResult !=""){
return checkResult;
}
}
//创建交易单--实现2--这里直接包装
public ResultDataDto createTradeOrder(TradeOrderDto tradeOrder){
ResultDataDto checkResult = tradeValidator.check(tradeOrder);
if(!checkResult.isSuccess()){
return checkResult;
}
}
}
public class ParamCheckUtil{
//通用的对象参数校验方法
public static String checkParam(Object obj){
Class clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
//obj instance list check logic
//obj instance map check logic
boolean check=false;
for (Field field : fields){
field.setAccessible(true);
//field Annotation check
//field Value Check
}
if(!check){
return "参数【field.name】不能为空";
}
return "";
}
}
2.6 校验逻辑在框架注解中
这种情况比较常见于web框架中,所以这里我们用controller来看一下web的参数校验逻辑,用最常见的
Hibernate Validator+springboot /mvc 来看一下代码怎么写:
@Controller(value="/trade")
public class TradeController{
@RequestMapping("/create")
public String createTradeOrder( @RequestBody @Valid TradeOrderVO tradeOrder, BindingResult result ){
if(result.hasErrors()){
return result.errorListstr();
}
}
}
import org.hibernate.validator.constraints.NotBlank;
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.Pattern;
public class TradeOrderDto{
@NotBlank(message="卖家ID不能为空")
private Long sellerId;
@NotBlank(message="金额不能为空")
@Pattern(regexp="^[0-9]{1,2}$",message="金额不正确")
private String amount;
}
2.7 校验逻辑在前端
很多时候做web管理系统的时候web校验依然也无可避免,常见于表单弹框等场景。关于web 表单的校验一般也有很多种情况,基本都是基于JS来做一些表单验证,但是最终还是需要后端来保障数据的完整性和正确性,这里不再深入。
2.8 校验逻辑在业务流程中
除了上面的情况以外,有些校验逻辑是在业务流程中的,比如调用远程接口,或者调用内部处理类返回数据进行校验,这里依然给出大概的代码片段参考:
@Service
public class TradeFacadeImpl{
@Autowired
private UserService userService;
//创建交易单
public String createTradeOrder(TradeOrderDto tradeOrder){
UserDTO sellerDto = userService.getById(tradeOrder.getSellerId);
if(sellerDto == null){
return "卖家不存在";
//throw new Exception("卖家不存在");
}
}
}
2.9 校验逻辑无处不在
这个场景常见于代码比较混乱的情况,在我进行商品中心重构的时候就遇到了这种情况,代码片段参考如下:
@Service
public class ItemSpuFacadeImpl{
@Autowired
private SpuService spuService;
//创建交易单
public String createFrontSpu(ItemFrontSpuDTO frontSpuDto){
frontSpuDto.check();
spuService.createFrontSpu(frontSpuDto.name,frontSpuDto.code,frontSpuDto.propSet);
}
}
@Service
public class SpuService{
@Autowired
private ItemSpuDao itemSpuDao;
//创建spu
public boolean createFrontSpu(String frontSpuName,String code,Set<KVPair> propSet ){
if(frontSpuName == null || String code == null || propSet == null){
return false;
}
if(propSet.isEmpty()){
return false;
}
int count = itemSpuDao.insert(ItemSpuEntity);
}
}
public class ItemSpuDao extends BaseDao{
int insert(ItemSpuEntity spuEntity){
if(spuEntity == null || spuEntity.name == null){
return 0;
}
return super.insert(spuEntity);
}
}
三、总结
以上几乎所有都存在于我们的代码中,这里并不会去明确说明每个场景的适用情况和优劣,毕竟参数校验和异常体系,参数返回等息息相关。但是不管怎么样,你怎么校验跟你返回什么值其实没有太大关系。更特别的情况是我在表现层诸如facadeimpl,controller里的校验其实是可以不用抛出异常的,但是深入到service中间,当我需要中断的时候是返回空还是返回空对象,或者是抛出异常,又或者是返回一个包装对象直接让上层返回等等这些其实深入实践起来还是有点讲究的。
关于对象参数校验其实也算一种软件复杂度的体现,所以我们会在定义接口时参考各种规约,借用各种其他对象或者技术机制去实现。这里就可以看出校验其实有时候会是通用的,而有时候就是定制的,如何处理好这部分逻辑体现着一个程序员的工匠精神,不求最好,但求最简单直接。
四、其他
关于参数校验和异常体系的对接以及如何做代码模块层面的异常体系后面会专门用一篇文章给大家阐述最佳实践。希望本次不同茴香豆的味道给你一些不同的体验,期待下次再见。
原文始发于微信公众号(神帅的架构实战):对象参数校验的花式写法
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/241624.html