本文转载请联系无敌码农公众号。本文将与大家分享平时开发中总结的一些小技巧!工作中写过Java程序的朋友都知道,使用Java开发服务最主流的方式是通过SpringMVC定义一个Controller层接口,将请求接口或返回参数分别定义在一个Java实体类中,这样SpringMVC在收到Http请求(POST/GET)后,会自动将请求消息映射成Java对象。这样的代码通常是这样写的:但是在逻辑实现过程中,会遇到这样一个问题:“如何实现接收请求参数后对消息对象数据值的合法性校验?”。也有同学认为这不是问题,因为具体的参数字段是否为空,取值是否在约定的范围内,格式是否合法等等,都可以在业务代码中查到。例如,可以在Service实现类中对消息格式进行各种if-else数据检查。从功能上来说,if-else代码冗余没有什么不好,但是从代码优雅上来说,冗长的if-else代码会显得很臃肿。以下部分将介绍处理此类问题的实用方法。具体将从以下几个方面进行介绍:使用@Validated注解实现Controller接口层数据的直接绑定校验;扩展约束注解,实现数据取值范围的校验;更灵活的对象数据有效性验证工具类封装;数据有效性校验结果异常返回处理;Controller接口层数据绑定校验其实目前Java开发中常用的Bean数据校验工具是“hibernate-validator”,它是hibernete的独立jar包,所以使用这个jar包不需要集成Hibernete框架。该jar包主要实现和扩展了javax.validation(一种基于JSR-303标准制定的Bean验证规范)接口。由于SpringBoot内部默认集成了“hibernate-validator”,使用SpringBoot构建的Java项目可以直接使用相关注解实现Bean数据校验。比如我们最常写的Controller层接口参数对象,在定义Bean类的时候可以直接写这样的代码:@DatapublicclassCreateOrderDTO{@NotNull(message="订单号不能为空")privateStringorderId;@NotNull(message="订单金额不能为空")@Min(value=1,message="订单金额不能小于0")privateIntegeramount;@Pattern(regexp="^1[3|4|5|7|8][0-9]{9}$",message="用户手机号无效")privateStringmobileNo;privateStringorderType;privateStringstatus;}如上代码所示,我们可以使用@NotNull注解来约束该字段不能为空,或者使用@Min注解来约束字段的最小值,或者可以使用@Pattern注解使用正则表达式来约束字段的格式(比如手机号码的格式)等等。以上注解是“hibernate-validator”依赖包默认提供的,还有很多比较常用的注解。例如:使用这些约束注解,我们可以轻松的处理接口数据验证,而不需要业务逻辑编写大量的if-else来验证数据的有效性。定义Bean参数对象并使用相关注解实现参数值约束后,只需要在Controller层接口定义中使用@Validated注解即可在接收到参数后自动进行数据绑定校验。具体代码如下:@PostMapping("/createOrder")publicCreateOrderBOvalidationTest(@ValidatedCreateOrderDTOcreateOrderDTO){returnorderServiceImpl.createOrder(createOrderDTO);}如上所示,Spring在Controller层提供的@Validated注解可以自动实现绑定校验数据Bean的,如果数据异常,统一抛出Validation异常!在“hibernate-validator”依赖的jar包中扩展了约束注解。虽然提供了很多方便的约束注解,但也有一些情况不能满足一些实际需要。比如我们要针对某个值的值规定其值的枚举范围。比如orderType订单类型只允许有“pay”和“refund”两个值,那么现有的约束注解可能不是特别适用。另外,对于这样的枚举值,我们也希望在约束定义中直接匹配代码中的枚举定义,从而更好的统一接口参数和业务逻辑的枚举定义。在这种情况下,我们也可以扩展定义来相应地约束注解逻辑。接下来我们定义一个新的约束注解@EnumValue来实现我们上面说的效果。具体代码如下:@Target({METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER})@Retention(RUNTIME)@Documented@Constraint(validatedBy={EnumValueValidator.class})public@interfaceEnumValue{//默认错误信息Stringmessage()default"mustbethespecifiedvalue";//支持字符串数组校验String[]strValues()default{};//支持int数组校验int[]intValues()default{};//支持枚举列表校验Class>[]enumValues()default{};//组类>[]groups()default{};//加载类[]payload()default{};//使用@Target({FIELD,METHOD,PARAMETER,ANNOTATION_TYPE})@Retention(RUNTIME)@Documented@interfaceList{EnumValue[]value();}指定多个时/***验证类逻辑定义*/classEnumValueValidatorimplementsConstraintValidator{//字符串类型数组privateString[]strValues;//int类型数组privateint[]intValues;//枚举类privateClass>[]enumValues;/***初始化方法**@paramconstraintAnnotation*/@Overridepublicvoidinitialize(EnumValueconstraintAnnotation){strValues=constraintAnnotation.strValues();intValues=约束Annotation.intValues();enumValues=constraintAnnotation.enumValues();}/***验证方法**@paramvalue*@paramcontext*@return*/@SneakyThrows@OverridepublicbooleanisValid(Objectvalue,ConstraintValidatorContextcontext){//对字符串数组进行验证matchif(strValues!=null&&strValues.length>0){if(valueinstanceofString){for(Strings:strValues){//判断值类型是否为Integer类型if(s.equals(value)){returntrue;}}}}//匹配整数数组if(intValues!=null&&intValues.length>0){if(valueinstanceofInteger){//判断值类型是否为Integer类型for(Integers:intValues){if(s==value){returntrue;}}}}//枚举类型校验if(enumValues!=null&&enumValues.length>0){for(Class>cl:enumValues){if(cl.isEnum()){//枚举类校验Object[]objs=cl.getEnumConstants();//这里需要注意的是,在定义枚举的时候,枚举值的名称统一用value表示Methodmethod=cl.getMethod("getValue");for(Objectobj:objs){Objectcode=method.invoke(obj,null);if(value.equals(code.toString())){returntrue;}}}}}returnfalse;}}}@EnumValue约束如上图注解为一个非常实用的扩展,通过它我们可以实现对参数取值范围(不是大小范围)的约束,它支持int,string和enum这三种数据类型的约束使用方式如下:/***自定义注解,支持将参数值与指定类型数组列表值匹配(缺点是枚举值需要硬编码在字段定义注解中)*/@EnumValue(strValues={"pay","re??fund"},message="ordertypeerror")privateStringorderType;/***自定义注解实现自动匹配和参数值和枚举列表的校验(可以更好的匹配实际业务开发)*/@EnumValue(enumValues=Status.class,message="状态值不在指定范围内")privateStringstatus;如上代码所示,这个扩展注解可以使用strValues或intValues属性以编程方式枚举取值范围,也可以直接通过enumValues绑定枚举定义但需要注意的是,出于一般考虑,具体枚举定义的属性名称应该统一匹配为value和desc。例如Status枚举定义如下:publicenumStatus{PROCESSING(1,"Processing"),SUCCESS(2,"订单已完成Complete");Integervalue;Stringdesc;Status(Integervalue,Stringdesc){this.value=value;this.desc=desc;}publicIntegergetValue(){returnvalue;}publicStringgetDesc(){returndesc;}}可以通过注解扩展实现更方便的约束注解!更灵活的数据校验工具类封装除了直接在Controller层使用@Validated进行绑定数据校验,在某些情况下,比如你的参数对象中的某个字段是一个Composite对象,或者输入对象定义的某个方法业务层也需要验证数据的有效性,那么这种情况下如何达到和Controller层一样的验证效果呢?需要说明的是,这种情况下@Validated不能再直接使用了,因为@Validated注解发挥作用主要是SpringMVC在接收参数的过程中实现了自动数据绑定校验,但是没有办法直接绑定普通业务方法或复合参数对象中的校准。测试。这种情况下,我们可以通过定义ValidateUtils工具类来达到同样的验证效果。具体代码如下:publicclassValidatorUtils{privatestaticValidatorvalidator=Validation.buildDefaultValidatorFactory().getValidator();,抛出第一个违规异常*/publicstaticvoidvalidate(Objectobj,Class>...groups){Set>resultSet=validator.validate(obj,groups);if(resultSet.size()>0){//如果有错误结果,会解析拼凑抛出异常ListerrorMessageList=resultSet.stream().map(o->o.getMessage()).collect(Collectors.toList());StringBuildererrorMessage=newStringBuilder();errorMessageList.stream().forEach(o->errorMessage.append(o+";"));thrownewIllegalArgumentException(errorMessage.toString());}}}如上图,我们定义了一个基于“javax.validation”接口实现的工具类实现,这样在非@Validated直接绑定验证场景下,可以使用验证工具类来实现Bean对象约束注解的验证处理。具体使用代码如下:publicbooleanorderCheck(OrderCheckBOorderCheckBO){//验证参数对象的数据ValidatorUtils.validate(orderCheckBO);returntrue;}方法入口对象仍然可以使用我们前面介绍的绑定注解进行约定,比如上面方法的入口引用对象定义如下:@Data@BuilderpublicclassOrderCheckBO{@NotNull(message="订单号不能为空")privateStringorderId;@Min(value=1,message="订单金额不能小于0")privateIntegerorderAmount;@NotNull(message="创建者不能为空")privateStringoperator;@NotNull(message="操作时间不能为空")privateStringoperatorTime;}这样可以保持编程体验整体一致!通过我们前面提到的各种约束注解统一处理数据有效性校验结果的异常,我们实现了Controller层接口和业务方法参数对象的统一数据校验。为了保持验证异常处理的统一处理和错误信息的统一输出,我们还可以定义一个通用的异常处理机制,保证各种数据验证错误都能以统一的错误格式反馈给调用方。具体代码如下:@Slf4j@ControllerAdvicepublicclassGlobalExceptionHandler{/***参数校验错误异常统一处理(非Spring接口数据绑定校验)**@paramresponse*@parame*@return*/@ExceptionHandler(BindException.class)@ResponseBodypublicResponseResult>processValidException(HttpServletResponseresponse,BindExceptione){response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());//获取验证错误结果信息,并将信息组装成ListerrorStringList=e.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList());StringerrorMessage=String.join(";",errorStringList);response.setContentType("application/json;charset=UTF-8");log.error(e.toString()+"_"+e.getMessage(),e);returnResponseResult.systemException(GlobalCodeEnum.GL_FAIL_9998.getCode(),errorMessage);}/***统一处理参数校准验证错误异常**@paramresponse*@parame*@return*/@ExceptionHandler(IllegalArgumentException.class)@ResponseBodypublicResponseResult>processValidException(HttpServletResponseresponse,IllegalArgumentExceptione){response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());StringerrorMessage=String.join(";",e.getMessage());response.setContentType("application/json;charset=UTF-8");log.error(e.toString()+"_"+e.getMessage(),e);returnResponseResult.systemException(GlobalCodeEnum.GL_FAIL_9998.getCode(),errorMessage);}...}如上所示,我们对前面两种数据校验方式定义了统一的异常处理机制,使得数据校验的错误信息可以通过统一的消息格式反馈给调用方,从而实现接口数据消息的统一返回!其中,公共接口参数对象ResponseResult的代码定义如下:@Data@Builder@NoArgsConstructor@AllArgsConstructor@JsonPropertyOrder({"code","message","data"})publicclassResponseResultimplementsSerializable{privatestaticfinallongserialVersionUID=1L;/***返回对象*/@JsonInclude(JsonInclude.Include.NON_NULL)privateTdata;/***返回码*/privateIntegercode;/***返回信息*/privateStringmessage;/***@paramdata返回数据*@param返回数据类型*@return响应结果*/publicstaticResponseResultOK(Tdata){returnpackageObject(data,GlobalCodeEnum.GL_SUCC_0);}/***自定义系统异常信息**@paramcode*@parammessage自定义消息*@param*@return*/publicstaticResponseResultsystemException(Integercode,Stringmessage){returnpackageObject(null,code,message);}}当然这样统一的报文格式不仅仅是处理异常返回,正常的数据报文格式也可以通过这个对象统一打包!从实际的角度向大家展示了如何在日常工作中编写通用的数据验证逻辑。原文链接:https://mp.weixin.qq.com/s/9kKIDZYB7bR7jiC5vj6qMg