EasyExcel自定义注解进阶:实现动态必填校验与分组验证
1. 为什么需要动态必填校验在日常开发中我们经常遇到这样的场景同一个Excel导入模板在不同业务场景下需要校验的字段可能完全不同。比如员工信息导入时身份证号和手机号是必填项而订单导入时商品编号和价格才是关键字段。传统的固定注解方式显然无法满足这种灵活多变的需求。我曾在项目中遇到过这样的问题一个商品导入功能运营人员反馈说有些字段在特定促销活动时根本不需要填写但系统却强制要求输入。每次都要找开发修改代码重新发布效率极低。这就是典型的静态校验无法适应动态业务场景的痛点。动态必填校验的核心思想是让校验规则能够根据业务场景动态变化。这需要我们在注解设计时就考虑扩展性比如增加业务类型参数、分组标识等。下面我们来看具体实现方案。2. 设计支持动态校验的自定义注解2.1 基础注解改造首先改造之前的RequiredValid注解增加业务类型参数Target(ElementType.FIELD) Retention(RetentionPolicy.RUNTIME) public interface RequiredValid { String message() default 字段不能为空; String[] businessTypes() default {}; // 支持多业务类型 }这个改造后的注解可以这样使用Data public class ImportDataVO { // 员工导入时必填 RequiredValid(message 身份证号必填, businessTypes {STAFF}) private String idCard; // 订单导入时必填 RequiredValid(message 商品编号必填, businessTypes {ORDER}) private String productCode; }2.2 分组校验实现更进一步我们可以引入校验分组的概念。这类似于JSR-303中的groups属性public interface StaffGroup {} public interface OrderGroup {} Target(ElementType.FIELD) Retention(RetentionPolicy.RUNTIME) public interface RequiredValid { String message() default 字段不能为空; Class?[] groups() default {}; }实体类中使用示例Data public class ImportDataVO { RequiredValid(message 员工姓名必填, groups StaffGroup.class) private String staffName; RequiredValid(message 订单金额必填, groups OrderGroup.class) private BigDecimal orderAmount; }3. 实现动态校验逻辑3.1 校验器升级改造原来的校验逻辑需要改造为支持动态判断public class ExcelImportValid { public static void valid(Object object, String businessType) throws Exception { Field[] fields object.getClass().getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); RequiredValid annotation field.getAnnotation(RequiredValid.class); if (annotation ! null) { // 动态判断业务类型 if (shouldValidate(annotation.businessTypes(), businessType)) { Object value field.get(object); if (value null || StringUtils.isEmpty(value.toString())) { throw new BusinessException(annotation.message()); } } } } } private static boolean shouldValidate(String[] supportedTypes, String currentType) { return supportedTypes.length 0 || Arrays.asList(supportedTypes).contains(currentType); } }3.2 分组校验实现对于分组校验可以这样实现public static void valid(Object object, Class?... groups) throws Exception { Field[] fields object.getClass().getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); RequiredValid annotation field.getAnnotation(RequiredValid.class); if (annotation ! null) { // 检查分组匹配 if (groups.length 0 || hasIntersection(annotation.groups(), groups)) { Object value field.get(object); if (value null || StringUtils.isEmpty(value.toString())) { throw new BusinessException(annotation.message()); } } } } } private static boolean hasIntersection(Class?[] annotationGroups, Class?[] validateGroups) { return Arrays.stream(annotationGroups) .anyMatch(ag - Arrays.asList(validateGroups).contains(ag)); }4. 实际应用示例4.1 动态业务类型校验假设我们有一个综合导入接口需要根据不同类型执行不同校验PostMapping(/import) public Result? importData(RequestParam MultipartFile file, RequestParam String businessType) { InputStream inputStream file.getInputStream(); ListImportDataVO dataList EasyExcel.read(inputStream) .head(ImportDataVO.class) .registerReadListener(new AnalysisEventListenerImportDataVO() { Override public void invoke(ImportDataVO data, AnalysisContext context) { ExcelImportValid.valid(data, businessType); } // 其他方法省略... }).sheet().doReadSync(); return Result.success(dataList); }4.2 分组校验实践在更复杂的场景下可以使用分组校验// 员工导入 public void importStaff(MultipartFile file) { EasyExcel.read(file.getInputStream()) .head(ImportDataVO.class) .registerReadListener(new AnalysisEventListenerImportDataVO() { Override public void invoke(ImportDataVO data, AnalysisContext context) { ExcelImportValid.valid(data, StaffGroup.class); } }).sheet().doRead(); } // 订单导入 public void importOrder(MultipartFile file) { EasyExcel.read(file.getInputStream()) .head(ImportDataVO.class) .registerReadListener(new AnalysisEventListenerImportDataVO() { Override public void invoke(ImportDataVO data, AnalysisContext context) { ExcelImportValid.valid(data, OrderGroup.class); } }).sheet().doRead(); }5. 高级技巧与注意事项5.1 组合校验策略在实际项目中我们可能需要组合多种校验策略。比如先按业务类型过滤再按分组校验Target(ElementType.FIELD) Retention(RetentionPolicy.RUNTIME) public interface RequiredValid { String message() default 字段不能为空; String[] businessTypes() default {}; Class?[] groups() default {}; } // 校验逻辑需要同时考虑businessTypes和groups public static void valid(Object object, String businessType, Class?... groups) { // 组合校验逻辑 }5.2 性能优化建议当字段很多时反射校验可能会影响性能。可以考虑以下优化方案缓存注解信息在类加载时解析并缓存字段的注解信息避免每次校验都反射预编译校验规则根据业务类型预先生成校验规则集并行校验对于大批量数据可以使用并行流加速校验5.3 常见问题排查在实现过程中我遇到过几个典型问题注解不生效检查RetentionPolicy是否是RUNTIMETarget是否包含FIELD继承字段校验父类的注解字段默认不会被校验需要特别处理静态字段问题静态字段无法通过实例校验需要特殊处理或排除6. 扩展思考更灵活的校验方案6.1 基于配置的校验规则更进一步我们可以将校验规则外置到配置文件中validation-rules: STAFF: required-fields: - field: idCard message: 身份证号必填 - field: phone message: 手机号必填 ORDER: required-fields: - field: productCode message: 商品编号必填然后开发一个配置化的校验器完全摆脱注解的束缚。6.2 组合其他校验框架可以结合Hibernate Validator等成熟校验框架实现更丰富的校验规则Data public class ImportDataVO { RequiredValid(groups StaffGroup.class) Pattern(regexp ^1[3-9]\\d{9}$, message 手机号格式错误) private String phone; }这种组合方案既能保持灵活性又能复用现有校验逻辑。