作者:微信小助手
发布时间:2020-12-08T08:08:57
本篇文章给大家分享平时开发中总结的一点小技巧!在工作中写过Java程序的朋友都知道,目前使用Java开发服务最主流的方式就是通过Spring MVC定义一个Controller层接口,并将接口请求或返回参数分别定义在一个Java实体类中,这样Spring MVC在接收到Http请求(POST/GET)后,就会自动将请求报文自动映射成一个Java对象。这样的代码通常是这样写的:
@RestController
public class OrderController {
@Autowired
private OrderService orderServiceImpl;
@PostMapping("/createOrder")
public CreateOrderBO validationTest(@Validated CreateOrderDTO createOrderDTO) {
return orderServiceImpl.createOrder(createOrderDTO);
}
}
这样的代码相信大家并不陌生,但在后续的逻辑实现过程中却会遇到这样的问题:“在接收请求参数后如何实现报文对象数据值的合法性校验?”。一些同学也可能认为这并不是什么问题,因为具体某个参数字段是否为空、值的取值是否在约定范围、格式是否合法等等,在业务代码中校验就好了。例如可以在Service实现类中对报文格式进行各种if-else的数据校验。
从功能上说冗余的if-else代码没啥毛病,但从代码的优雅性来说冗长的if-else代码会显得非常臃肿。接下来的内容将给大家介绍一种处理此类问题的实用方法。具体将从以下几个方面进行介绍:
使用@Validated注解实现Controller接口层数据直接绑定校验;
扩展约束性注解实现数据取值范围的校验;
更加灵活的对象数据合法性校验工具类封装;
数据合法性校验结果异常统一返回处理;
Controller接口层数据绑定校验
实际上在Java开发中目前普通使用的Bean数据校验工具是"hibernate-validator",它是一个hibernete独立的jar包,所以使用这个jar包并不需要一定要集成Hibernete框架。该jar包主要实现并扩展了javax.validation(是一个基于JSR-303标准开发出来的Bean校验规范)接口。
由于Spring Boot在内部默认集成了"hibernate-validator",所以使用Spring Boot构建的Java工程可以直接使用相关注解来实现Bean的数据校验。例如我们最常编写的Controller层接口参数对象,可以在定义Bean类时直接编写这样的代码:
@Data
public class CreateOrderDTO {
@NotNull(message = "订单号不能为空")
private String orderId;
@NotNull(message = "订单金额不能为空")
@Min(value = 1, message = "订单金额不能小于0")
private Integer amount;
@Pattern(regexp = "^1[3|4|5|7|8][0-9]{9}$", message = "用户手机号不合法")
private String mobileNo;
private String orderType;
private String status;
}
如上所示代码,我们可以使用@NotNull注解来约束该字段必须不能为空,也可以使用@Min注解来约束字段的最小取值,或者还可以通过@Pattern注解来使用正则表达式来约束字段的格式(如手机号格式)等等。
以上这些注解都是“hibernate-validator”依赖包默认提供的,更多常用的注解还有很多,例如:
利用这些约束注解,我们就可以很轻松的搞定接口数据校验,而不需要在业务逻辑中编写大量的if-else来进行数据合法性校验。而定义好Bean参数对象并使用相关注解实现参数值约束后,在Controller层接口定义中只需要使用@Validated注解就可以实现在接收参数后自动进行数据绑定校验了,具体代码如下:
@PostMapping("/createOrder")
public CreateOrderBO validationTest(@Validated CreateOrderDTO createOrderDTO) {
return orderServiceImpl.createOrder(createOrderDTO);
}
如上所示,在Controller层中通过Spring提供的@Validated注解可以自动实现数据Bean的绑定校验,如果数据异常则会统一抛出校验异常!
约束性注解扩展
在“hibernate-validator”依赖jar包中,虽然提供了很多很方便的约束注解,但是也有不满足某些实际需要的情况,例如我们想针对参数中的某个值约定其值的枚举范围,如orderType订单类型只允许传“pay”、“refund”两种值,那么现有的约束注解可能就没有特别适用的了。此外,如果对这样的枚举值,我们还想在约束定义中直接匹配代码中的枚举定义,以更好地统一接口参数与业务逻辑的枚举定义。那么这种情况下,我们还可以自己扩展定义相应地约束注解逻辑。
接下来我们定义新的约束注解@EnumValue,来实现上面我们所说的效果,具体代码如下:
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValueValidator.class})
public @interface EnumValue {
//默认错误消息
String message() default "必须为指定值";
//支持string数组验证
String[] strValues() default {};
//支持int数组验证
int[] intValues() default {};
//支持枚举列表验证
Class<?>[] enumValues() default {};
//分组
Class<?>[] groups() default {};
//负载
Class<? extends Payload>[] payload() default {};
//指定多个时使用
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
EnumValue[] value();
}
/**
* 校验类逻辑定义
*/
class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {
//字符串类型数组
private String[] strValues;
//int类型数组
private int[] intValues;
//枚举类
private Class<?>[] enumValues;
/**
* 初始化方法
*
* @param constraintAnnotation
*/
@Override
public void initialize(EnumValue constraintAnnotation) {
strValues = constraintAnnotation.strValues();
intValues = constraintAnnotation.intValues();
enumValues = constraintAnnotation.enumValues();
}
/**
* 校验方法
*
* @param value
* @param context
* @return
*/
@SneakyThrows
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
//针对字符串数组的校验匹配
if (strValues != null && strValues.length > 0) {
if (value instanceof String) {
for (String s : strValues) {//判断值类型是否为Integer类型
if (s.equals(value)) {
return true;
}
}
}
}
//针对整型数组的校验匹配
if (intValues != null && intValues.length > 0) {
if (value instanceof Integer) {//判断值类型是否为Integer类型
for (Integer s : intValues) {
if (s == value) {
return true;
}
}
}
}
//针对枚举类型的校验匹配
if (enumValues != null && enumValues.length > 0) {
for (Class<?> cl : enumValues) {
if (cl.isEnum()) {
//枚举类验证
Object[] objs = cl.getEnumConstants();
//这里需要注意,定义枚举时,枚举值名称统一用value表示
Method method = cl.getMethod("getValue");
for (Object obj : objs) {
Object code = method.invoke(obj, null);
if (value.equals(code.toString())) {
return true;
}
}