一、场景介绍
-
在
Hibernate-validator
依赖jar
包中,虽然提供了很多很方便的校验注解,但是也有不满足某些实际需要的场景 -
假如我们想针对参数中的某个属性,约定其值的枚举范围,如:
OrderType
订单类型只允许传PAYED
、FAIL
两种值,那么现有的约束注解就不能适用这种场景了 -
如果对这样的枚举值,我们还想在约束定义中直接匹配代码中的枚举定义,以更好地统一接口参数与业务逻辑的枚举定义,那么这种情况下,我们还可以自己扩展定义相应地约束注解逻辑
二、校验场景
-
用户订单查询场景
-
只允许用户查询支付失败的订单列表
三、定义注解
-
自定义注解
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = {EnumValidated.EnumValidator.class}) public @interface EnumValidated { /** 默认错误信息*/ String message() default "默认错误提示:请参数接口文档传入必要参数"; /** 支持 String 数组校验*/ String[] strValues() default {}; /** 支持 int 数组校验*/ int[] intValues() default {}; /** 支持枚举列表校验*/ Class<?>[] enumValues() default {}; /** 分组*/ Class<?>[] groups() default {}; /** 负载*/ Class<? extends Payload>[] payload() default {}; /** 指定多个时使用*/ @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @interface List { EnumValidated[] value(); } /** * Copyright (C), 1998-2021, Shenzhen Rambo Technology Co., Ltd * 校验类逻辑定义 * * @author Rambo * @date 2021/3/11 18:21 * @since 1.0.0.1 */ class EnumValidator implements ConstraintValidator<EnumValidated, Object> { /** 字符串类型数组*/ private String[] strValues; /** int类型数组*/ private int[] intValues; /** 枚举类*/ private Class<?>[] enumValues; /** * 初始化方法 * * @author Rambo * @date 2021/3/11 18:34 * @param constraintAnnotation 自定义枚举类型注解对象 */ @Override public void initialize(EnumValidated constraintAnnotation) { strValues = constraintAnnotation.strValues(); intValues = constraintAnnotation.intValues(); enumValues = constraintAnnotation.enumValues(); } /** * 校验方法 * * @author Rambo * @date 2021/3/11 18:34 * @param value 待校验的参数值 * @param context 校验对象 * @return boolean 校验结果 */ @SneakyThrows @Override public boolean isValid(Object value, ConstraintValidatorContext context) { // 针对字符串数组的校验匹配 if (strValues != null && strValues.length > 0) { if (value instanceof String) { // 判断值类型是否为 String 类型 for (String s : strValues) { if (s.equals(value)) { return true; } } } } // 针对整型数组的校验匹配 if (intValues != null && intValues.length > 0) { // 判断值类型是否为 Integer 类型 if (value instanceof 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(); // 这里需要注意,定义枚举时,枚举 Key 的字段名称统一用 private String code; 表示 Method method = cl.getMethod("getCode"); for (Object obj : objs) { Object code = method.invoke(obj, (Object[]) null); if (value.toString().equals(code.toString())) { return true; } } } } } return false; } } }
P.S
- 如上所示的
@EnumValidated
约束注解,是一个非常实用的扩展,通过该注解我们可以实现对参数取值范围(不是大小范围)的约束,它支持对int
、String
以及enum
三种数据类型的约束
- 如上所示的
四、使用注解
-
新建
Spring Boot
项目 -
创建订单状态枚举类型
@Getter public enum OrderStateEnum { /** 支付中*/ PAY("PAYING", "支付中"), /** 已支付*/ PAYED("PAYED", "已支付"), /** 支付失败*/ FAIL("FAIL", "支付失败"), ; /** 枚举编码*/ private final String code; /** 枚举描述*/ private final String name; OrderStateEnum(String code, String name) { this.code = code; this.name = name; } /** 根据代码获取枚举名称*/ public static String getNameByCode(String code) { for (OrderStateEnum orderState : OrderStateEnum.values()) { if (orderState.getCode().equals(code)) { return orderState.getName(); } } return null; } /** 根据名称获取枚举代码*/ public static String getCodeByName(String name) { for (OrderStateEnum orderType : OrderStateEnum.values()) { if (orderType.getName().equals(name)) { return orderType.getCode(); } } return null; } /** 根据代码获取枚举对象*/ public static OrderStateEnum getOrderStateEnumByCode(String code) { for (OrderStateEnum orderState : OrderStateEnum.values()) { if (orderState.getCode().equals(code)) { return orderState; } } return null; } /** 根据名称获取枚举对象*/ public static OrderStateEnum getOrderStateEnumByName(String name) { for (OrderStateEnum orderState : OrderStateEnum.values()) { if (orderState.getName().equals(name)) { return orderState; } } return null; } }
-
创建
Order
实体@Data @ToString public class Order implements Serializable { private static final long serialVersionUID = 784930215432L; /** 订单号*/ private int id; /** 订单名称*/ @NotNull(message = "订单名称不能为空") private String orderName; /** 订单状态 定制化注解,支持参数值与指定类型数组列表值进行匹配(缺点是需要将枚举值写死在字段定义的注解中)*/ @EnumValidated(strValues = {"FAIL", "PAYED"}, message = "只能查询指定状态的订单信息-1") private String orderState; /** 订单状态枚举 定制化注解,实现参数值与枚举列表的自动匹配校验(能更好地与实际业务开发匹配)*/ @EnumValidated(enumValues = OrderStateEnum.class, message = "只能查询指定状态的订单信息-2") private String orderStateEnum; }
P.S
- 如上所示代码,该扩展注解既可以使用
strValues
或intValues
属性来编程列举取值范围,也可以直接通过enumValues
来绑定枚举定义。但是需要注意,处于通用考虑,具体枚举定义的属性的名称要统一匹配为code
、name
,具体请参数上述枚举类型到定义
- 如上所示代码,该扩展注解既可以使用
-
创建接口控制器
@PostMapping("/order") @ApiOperation(value = "订单查询", notes = "订单查询,匹配自定义验证注解,抛出异常由统一异常处理") public DataResult orderList(@RequestBody @Validated Order order) { log.info("The oder request is {}", order.toString()); return DataResult.success(); }
-
模拟请求参数
{ "id": 0, "orderState": "FAIL1", "orderStateEnum": "FAIL2" }
-
验证响应结果
{ "code": 10009, "msg": "订单名称不能为空;只能查询指定状态的订单信息-2;只能查询指定状态的订单信息-1;", "detail": null, "data": null }
P.S
以上Controller
通过项目中定义@RestControllerAdvice
来进行异常统一处理,所以看到的响应结果是封装过的