springboot 中 @NotNull 等参数检查注解非常实用,优化掉了很多的重复代码。
在开发老版本 spring 项目时,没有类似注解,所以自己实现一个类似的功能,优化代码结构。
由于项目中没有使用统一异常处理,注解用于 Service 层,抛出的异常由 Controller 处理。
首先自定义注解:
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface CheckParams { }
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.PARAMETER}) public @interface NotNull { String message() default ""; ParamType type() default ParamType.VALUE; }
@CheckParams 注解用做切点,@NotNull 注解标注参数。切面实现:
@Aspect @Component public class ParamsCheckAspect { private static final Logger logger = Logger.getLogger(ParamsCheckAspect.class); ControllerUtils controllerUtils = new ControllerUtils(); //切点 @Pointcut("@annotation(com.inspur.paramsCheck.annotation.CheckParams)") public void CheckParamsAspect() { } //切面 @Around("CheckParamsAspect()") public Object check(ProceedingJoinPoint joinPoint) throws IllegalAccessException, Throwable { Object[] args = joinPoint.getArgs(); Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); Annotation[][] paramsAnnotation = method.getParameterAnnotations(); if (null != paramsAnnotation && paramsAnnotation.length != 0) { for (int i = 0; i < paramsAnnotation.length; i++) { Annotation[] annotations = paramsAnnotation[i]; if (null == annotations || annotations.length == 0) continue; int notNullPoint = -1; if ((notNullPoint = this.indexOf(annotations, NotNull.class.getName())) != -1) { Object arg = args[i]; NotNull notNull = (NotNull) annotations[notNullPoint]; if (notNull.type() == ParamType.VALUE && isNull(arg)) { throw new LackParamException(notNull.message()); } if (notNull.type() == ParamType.BEAN) this.checkBean(arg, new HashSet<Object>()); } } } return joinPoint.proceed(args); } /** * @Author * @Date 2021/10/2 下午11:53 * @Description 递归检查对象完整性 */ private void checkBean(Object o, Set<Object> hisSet) throws IllegalAccessException { //终止条件,处理循环依赖 if (hisSet.contains(o)) return; hisSet.add(o); Class clazz = o.getClass(); String className = clazz.getSimpleName(); Field[] fields = o.getClass().getDeclaredFields(); for (Field field : fields) { if (!field.isAnnotationPresent(NotNull.class)) continue; NotNull notNull = field.getAnnotation(NotNull.class); ParamType type = notNull.type(); if (type == ParamType.VALUE && this.isNull(o, field)) { throw new LackParamException(className + ":" + notNull.message()); } if (type == ParamType.BEAN) { field.setAccessible(true); Object bean = field.get(o); if (null == bean) throw new LackParamException(className + ":" + notNull.message()); this.checkBean(bean, hisSet); } } } private int indexOf(Annotation[] arr, String annotationName) { for (int i = 0; i < arr.length; i++) { Annotation annotation = arr[i]; String name = annotation.annotationType().getName(); if (name.equals(annotationName)) return i; } return -1; } private boolean isNull(Object obj, Field field) throws IllegalAccessException { field.setAccessible(true); Object value = field.get(obj); return isNull(value); } private boolean isNull(Object obj) { return null == obj || obj.toString().length() == 0; } }
测试,定义 bean:
public class selectMachineVisitNumDto extends BasePo { @NotNull(message = "regionCode不可为空") private String regionCode; @NotNull(message = "machineType不可为空") private String machineType; @NotNull(message = "startTime不可为空") private String startTime; @NotNull(message = "endTime不可为空") private String endTime; @NotNull(type = ParamType.BEAN) private TestDto testDto; public TestDto getTestDto() { return testDto; } public void setTestDto(TestDto testDto) { this.testDto = testDto; } public String getRegionCode() { return regionCode; } public void setRegionCode(String regionCode) { this.regionCode = regionCode; } public String getMachineType() { return machineType; } public void setMachineType(String machineType) { this.machineType = machineType; } public String getStartTime() { return startTime; } public void setStartTime(String startTime) { this.startTime = startTime; } public String getEndTime() { return endTime; } public void setEndTime(String endTime) { this.endTime = endTime; } }
public class TestDto { @NotNull(message = "test不可为空!") private String test; @NotNull(type = ParamType.BEAN) private selectMachineVisitNumDto selectMachineVisitNumDto; public com.inspur.approval.selfstatistic.dto.selectMachineVisitNumDto getSelectMachineVisitNumDto() { return selectMachineVisitNumDto; } public void setSelectMachineVisitNumDto(com.inspur.approval.selfstatistic.dto.selectMachineVisitNumDto selectMachineVisitNumDto) { this.selectMachineVisitNumDto = selectMachineVisitNumDto; } public String getTest() { return test; } public void setTest(String test) { this.test = test; } @Override public String toString() { JSONObject jsonObject = new JSONObject(); jsonObject.put("test", test); jsonObject.put("selectMachineVisitNumDto", selectMachineVisitNumDto.toString()); return jsonObject.toJSONString(); } }
两个 bean 之间可形成循环依赖。Service 层,使用 @NotNull 标注参数,@CheckParams 标注方法:
@CheckParams public String getMachineNum(@NotNull(type = ParamType.BEAN) selectMachineVisitNumDto dto, @NotNull(message = "name不可为空!") String name) throws Exception { JSONObject jsonObject = dto.toJSON(); return jsonObject.toJSONString(); }
Controller 进行异常捕获及处理:
@RequestMapping("test") @ResponseBody public String getMachineNum(selectMachineVisitNumDto dto, String name, String test) { try { TestDto testDto = new TestDto(); testDto.setTest(test); testDto.setSelectMachineVisitNumDto(dto); dto.setTestDto(testDto); String res = selfStatisticService.getMachineNum(dto, name); return controllerUtils.getSuccessJson(res).toJSONString(); } catch (Exception e) { logger.error(e.getMessage(), e); return controllerUtils.getErrorJson(e.getMessage()).toJSONString(); } }
测试,dto 参数缺失:
递归检查 TestDto 中参数缺失:
两个 bean 的参数均完整,测试循环依赖情况的处理:
可以看到,已经跳出了对两个 dto 的检查,没有陷入循环依赖中。将单独参数 name 补充完整: