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 補充完整: