【SpringBoot】SpringBoot 中使用自定义注解来实现接口参数校验

爱被打了一巴掌 2023-10-01 11:03 15阅读 0赞

在后台接口做参数校验,一般有两种方案:

  • hibernate-validator
  • AOP + 自定义注解 实现方法级的参数校验

开发环境:

JDK:1.8
SpringBoot:2.1.1.RELEASE
IDEA:2019.1.1

1. hibernate-validator

hibernate-validator 是 Hibernate 项目中的一个数据校验框架,是 Bean Validation 的参考实现。

使用 hibernate-validator 能够将数据校验从业务代码中脱离出来,增加代码可读性。同时,也让数据校验变得更加方便、简单。

添加 hibernate-validator 依赖:

  1. <dependency>
  2. <groupId>org.hibernate</groupId>
  3. <artifactId>hibernate-validator</artifactId>
  4. <version>4.3.1.Final</version>
  5. </dependency>

【注意】:在 SpringBoot 2.1.1.RELEASE 中 不需要引入 Hibernate Validator , 因为 在引入的 spring-boot-starter-web(springbootweb启动器)依赖的时候中,内部已经依赖了 hibernate-validator 依赖包。

待校验的Vo:

  1. public class ValidatorVo {
  2. @NotEmpty(message = "用户名不能为空")
  3. private String sName;
  4. @NotEmpty(message = "手机号不能为空")
  5. @Pattern(regexp = "^1[3|4|5|7|8][0-9]\\d{8}$", message = "手机号格式不正确")
  6. private String sPhone;
  7. @NotNull(message = "age 不能为空")
  8. private Integer age;
  9. //getter/setter
  10. }

说明:

  • @NotEmpty:使用 hibernate-validator 校验规则

定义一个控制器:

  1. @RestController
  2. @RequestMapping("/param")
  3. public class ParamController {
  4. @PostMapping("/validator")
  5. public String validator(@RequestBody @Valid ValidatorVo validatorVo) {
  6. return "TEST VALIDATOR";
  7. }
  8. }

说明:

  • @Valid:对其参数启用验证

定义一个全局异常类:

  1. @Slf4j
  2. @RestControllerAdvice
  3. public class GlobalExceptionHandler {
  4. private static final String logExceptionFormat = "Capture Exception By GlobalExceptionHandler: Code: %s Detail: %s";
  5. @ExceptionHandler(value = {
  6. BindException.class, MethodArgumentNotValidException.class})
  7. public Object validationExceptionHandler(Exception ex) {
  8. return validationResult(1001, ex);
  9. }
  10. private <T extends Throwable> ResultVo validationResult(Integer code, T exception) {
  11. // 做日志记录处理
  12. doLog(code, exception);
  13. BindingResult bindResult = null;
  14. if (exception instanceof BindException) {
  15. bindResult = ((BindException) exception).getBindingResult();
  16. } else if (exception instanceof MethodArgumentNotValidException) {
  17. bindResult = ((MethodArgumentNotValidException) exception).getBindingResult();
  18. }
  19. String msg = null;
  20. if (null != bindResult && bindResult.hasErrors()) {
  21. msg = bindResult.getAllErrors().get(0).getDefaultMessage();
  22. if (msg.contains("NumberFormatException")) {
  23. msg = "参数类型错误!";
  24. }
  25. } else {
  26. msg = "系统繁忙,请稍后重试...";
  27. }
  28. return ResultVoUtil.error(code, msg);
  29. }
  30. private <T extends Throwable> void doLog(Integer status, T exception) {
  31. exception.printStackTrace();
  32. log.error(String.format(logExceptionFormat, status, exception.getMessage()));
  33. }
  34. }

ResultVoUtil:返回前端对象信息工具类

  1. public class ResultVoUtil {
  2. public static ResultVo success() {
  3. return success(null);
  4. }
  5. public static ResultVo success(Object object) {
  6. ResultVo result = new ResultVo();
  7. result.setCode(ResultCodeEnum.SUCCESS.getCode());
  8. result.setMsg("成功");
  9. result.setData(object);
  10. return result;
  11. }
  12. public static ResultVo success(Integer code, Object object) {
  13. return success(code, null, object);
  14. }
  15. public static ResultVo success(Integer code, String msg, Object object) {
  16. ResultVo result = new ResultVo();
  17. result.setCode(code);
  18. result.setMsg(msg);
  19. result.setData(object);
  20. return result;
  21. }
  22. public static ResultVo error(String msg) {
  23. ResultVo result = new ResultVo();
  24. result.setCode(ResultCodeEnum.ERROR.getCode());
  25. result.setMsg(msg);
  26. return result;
  27. }
  28. public static ResultVo error(Integer code, String msg) {
  29. ResultVo result = new ResultVo();
  30. result.setCode(code);
  31. result.setMsg(msg);
  32. return result;
  33. }
  34. }
  35. @Data
  36. public class ResultVo<T> {
  37. // 错误码
  38. private Integer code;
  39. // 提示信息
  40. private String msg;
  41. // 返回的数据
  42. private T data;
  43. public boolean checkSuccess() {
  44. return ResultCodeEnum.SUCCESS.getCode().equals(this.code);
  45. }
  46. }

好了,这就是使用了 hibernate-validator 校验。

2. AOP + 自定义注解 实现方法级的参数校验

hibernate-validator 是在实体类上添加注解;但对于不同的方法,所应用的校验规则也是不一样的,这样子可能就会需要创建多个实体类或者组,甚至于一些接口根本就没实体类参数;所以实际应用过程中还是有一定的困难;

所以,这里简单地实现了一套基于 自定义注解 + AOP 的方式实现接口参数校验框架。在方法体上使用@CheckParam 或者 @CheckParams 注解标注需要校验的参数

步骤一:自定义注解

自定义注解,给需要校验的方法进行注解。分为:单参数校验 @MyCheckParam 和多参数校验 @MyCheckParams

MyCheckParam:

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface MyCheckParam {
  4. // 字段校验规则
  5. MyCheckParamEnum value() default MyCheckParamEnum.NOT_NULL;
  6. // 参数名称。用"."表示层级。最多支持2级。如:userVo.userName
  7. String argName();
  8. // 表达式。多个值用","分割。跟argName有关。
  9. String express() default "";
  10. // 自定义提示信息
  11. String msg() default "";
  12. }

【注意】:MyCheckParamEnum 是一个枚举类哈。

MyCheckParams:

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface MyCheckParams {
  4. // 多个MyCheckParam,由上至下校验
  5. MyCheckParam[] value();
  6. }

可以校验多个参数。

步骤二:自定义枚举校验类

  1. @Getter
  2. public enum MyCheckParamEnum {
  3. NULL("参数必须为 null", MyCheckParamUtil::isNull),
  4. NOT_NULL("参数必须不为 null", MyCheckParamUtil::isNotNull),
  5. EMPTY("参数的必须为空", MyCheckParamUtil::isEmpty),
  6. NOT_EMPTY("参数必须非空", MyCheckParamUtil::isNotEmpty),
  7. LENGTH("参数长度必须在指定范围内", MyCheckParamUtil::inLength),
  8. GE("参数必须大于等于指定值", MyCheckParamUtil::isGreaterThanEqual),
  9. LE("参数必须小于等于指定值", MyCheckParamUtil::isLessThanEqual),
  10. ;
  11. private String msg;
  12. // 接收字段值(Object)和 表达式(String),返回是否符合规则(Boolean)
  13. private BiFunction<Object, String, Boolean> fun;
  14. MyCheckParamEnum(String msg, BiFunction<Object, String, Boolean> fun) {
  15. this.msg = msg;
  16. this.fun = fun;
  17. }
  18. }

说明:

  1. 此枚举类是用于参数校验的。它有两个属性:msgfunmsg:参数校验不通过时的默认报错信息;fun:进行参数校验时,需要执行的方法,位于 MyCheckParamUtil 类中。一个枚举实例对应一个 fun

步骤三:自定义枚举校验工具类

校验时,会调用此类中的方法。根据校验规则不同,调用的方法不同。如:

校验规则是 NULL,则它调用的方法是:MyCheckParamUtil::isNull

  1. public class MyCheckParamUtil {
  2. // 判断对象是否不为 null
  3. public static Boolean isNotNull(Object value, String express) {
  4. if (null == value) {
  5. return Boolean.FALSE;
  6. }
  7. return Boolean.TRUE;
  8. }
  9. public static Boolean isNull(Object value, String express) {
  10. return !isNotNull(value, express);
  11. }
  12. // 判断value !=null && length、size > 0
  13. public static Boolean isNotEmpty(Object value, String express) {
  14. if(isNull(value, express)) {
  15. return Boolean.FALSE;
  16. }
  17. if(value instanceof String && "".equals(((String) value).trim())) {
  18. return Boolean.FALSE;
  19. }
  20. if(value instanceof Collection && CollectionUtils.isEmpty((Collection) value)) {
  21. return Boolean.FALSE;
  22. }
  23. if (value instanceof Map && ((Map) value).isEmpty()) {
  24. return Boolean.FALSE;
  25. }
  26. return Boolean.TRUE;
  27. }
  28. public static Boolean isEmpty(Object value, String express) {
  29. return !isNotEmpty(value, express);
  30. }
  31. // 判断某个值的长度是否在某个范围
  32. public static Boolean inLength(Object value, String express) {
  33. if(isNull(value, express)) {
  34. return Boolean.FALSE;
  35. }
  36. if(null == express || "".equals(express)) {
  37. return Boolean.FALSE;
  38. }
  39. String[] split = express.split(",");
  40. if (null == split || split.length != 2) {
  41. return Boolean.FALSE;
  42. }
  43. if (value instanceof String) {
  44. Integer begin = Integer.valueOf(split[0].trim());
  45. Integer end = Integer.valueOf(split[1].trim());
  46. Integer length = ((String) value).length();
  47. return begin <= length && length <= end;
  48. }
  49. return Boolean.FALSE;
  50. }
  51. // 判断是否大于等于某个值
  52. public static Boolean isGreaterThanEqual(Object value, String express) {
  53. if (value == null) {
  54. return Boolean.FALSE;
  55. }
  56. if(value instanceof Integer) {
  57. return ((Integer) value) >= Integer.valueOf(express);
  58. }
  59. if(value instanceof Long) {
  60. return ((Long) value) >= Long.valueOf(express);
  61. }
  62. if(value instanceof Short) {
  63. return ((Short) value) >= Short.valueOf(express);
  64. }
  65. if(value instanceof Float) {
  66. return ((Float) value) >= Float.valueOf(express);
  67. }
  68. if(value instanceof Double) {
  69. return ((Double) value) >= Double.valueOf(express);
  70. }
  71. if(value instanceof String) {
  72. return ((String) value).length() >= Integer.valueOf(express);
  73. }
  74. if(value instanceof Collection) {
  75. return ((Collection) value).size() >= Integer.valueOf(express);
  76. }
  77. return Boolean.FALSE;
  78. }
  79. // 判断是否大于等于某个值
  80. public static Boolean isLessThanEqual(Object value, String express) {
  81. if (value == null) {
  82. return Boolean.FALSE;
  83. }
  84. if(value instanceof Integer) {
  85. return ((Integer) value) <= Integer.valueOf(express);
  86. }
  87. if(value instanceof Long) {
  88. return ((Long) value) <= Long.valueOf(express);
  89. }
  90. if(value instanceof Short) {
  91. return ((Short) value) <= Short.valueOf(express);
  92. }
  93. if(value instanceof Float) {
  94. return ((Float) value) <= Float.valueOf(express);
  95. }
  96. if(value instanceof Double) {
  97. return ((Double) value) <= Double.valueOf(express);
  98. }
  99. if(value instanceof String) {
  100. return ((String) value).length() <= Integer.valueOf(express);
  101. }
  102. if(value instanceof Collection) {
  103. return ((Collection) value).size() <= Integer.valueOf(express);
  104. }
  105. return Boolean.FALSE;
  106. }
  107. }

步骤四:自定义 AOP

在 AOP 中,对注解进行解析、处理。主要逻辑在方法 doCheckParam()

MyCheckParamAspect:

  1. @Slf4j
  2. @Aspect
  3. @Component
  4. public class MyCheckParamAspect {
  5. // 单个参数校验切入点
  6. @Pointcut("@annotation(com.tinady.annotation.MyCheckParam)")
  7. public void doMyCheckParam() {
  8. }
  9. // 多个参数校验切入点
  10. @Pointcut("@annotation(com.tinady.annotation.MyCheckParams)")
  11. public void doMyCheckParams() {
  12. }
  13. // 单参数校验
  14. @Around("doMyCheckParam()")
  15. public Object doMyCheckParamAround(ProceedingJoinPoint joinPoint) throws Throwable {
  16. String msg = doCheckParam(joinPoint, false);
  17. // 参数校验未通过,则直接抛出自定义异常
  18. if (null != msg) {
  19. throw new MyCheckParamException(msg);
  20. }
  21. // 参数校验通过,则继续执行原来方法
  22. Object proceed = joinPoint.proceed();
  23. return proceed;
  24. }
  25. // 多参数校验
  26. @Around("doMyCheckParams()")
  27. public Object doMyCheckParamsAround(ProceedingJoinPoint joinPoint) throws Throwable {
  28. String msg = doCheckParam(joinPoint, true);
  29. // 参数校验未通过,则直接抛出自定义异常
  30. if (null != msg) {
  31. throw new MyCheckParamException(msg);
  32. }
  33. // 参数校验通过,则继续执行原来方法
  34. Object proceed = joinPoint.proceed();
  35. return proceed;
  36. }
  37. /**
  38. *
  39. * 功能描述: 参数校验
  40. *
  41. * @param joinPoint 切点
  42. * @param isMulti 是否是多参数校验
  43. * @return java.lang.String 错误信息
  44. * @date 2022-01-01 14:25
  45. */
  46. private String doCheckParam(ProceedingJoinPoint joinPoint, boolean isMulti) {
  47. Method method = this.getMethod(joinPoint);
  48. String[] paramNames = this.getParamNames(joinPoint);
  49. // 获取前端传递后台接口的所有入参对应的入参值
  50. Object[] arguments = joinPoint.getArgs();
  51. Boolean isValid = Boolean.TRUE;
  52. String msg = null;
  53. // 单参数校验
  54. if (!isMulti) {
  55. // AOP监听带注解的方法,所以不用判断注解是否为空
  56. MyCheckParam myCheckParam = method.getAnnotation(MyCheckParam.class);
  57. String argName = myCheckParam.argName();
  58. Object value = getParamValue(arguments, paramNames, argName);
  59. // 通过执行fun来校验value
  60. isValid = myCheckParam.value().getFun().apply(value, myCheckParam.express());
  61. msg = myCheckParam.msg();
  62. if (null == msg || "".equals(msg)) {
  63. msg = argName + ": " + myCheckParam.value().getMsg() + " " + myCheckParam.express();
  64. }
  65. } else {
  66. MyCheckParams myCheckParams = method.getAnnotation(MyCheckParams.class);
  67. MyCheckParam[] checkParams = myCheckParams.value();
  68. for (MyCheckParam checkParam : checkParams) {
  69. String argName = checkParam.argName();
  70. Object value = this.getParamValue(arguments, paramNames, argName);
  71. isValid = checkParam.value().getFun().apply(value, checkParam.express());
  72. // 只要有一个参数判断不通过,立即返回
  73. if (!isValid) {
  74. msg = checkParam.msg();
  75. if(null == msg || "".equals(msg)) {
  76. msg = argName + ": " + checkParam.value().getMsg() + " " + checkParam.express();
  77. }
  78. break;
  79. }
  80. }
  81. }
  82. if (!isValid) {
  83. log.error("校验未通过");
  84. return msg;
  85. }
  86. log.info("校验通过");
  87. return null;
  88. }
  89. }

说明:

  1. isValid = myCheckParam.value().getFun().apply(value, myCheckParam.express());:获取自定义注解中的 value,它是个枚举类。枚举类中有两个属性:msgfun。然后获取枚举中的 fun 方法,传入入参:value 和 自定义注解中的 express,然后执行此方法,获取其返回值

MyCheckParamAspect#getMethod():获取当前正在执行的方法

  1. private Method getMethod(JoinPoint joinPoint) {
  2. MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
  3. Method method = methodSignature.getMethod();
  4. if (method.getDeclaringClass().isInterface()) {
  5. try {
  6. method = joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(), method.getParameterTypes());
  7. } catch (NoSuchMethodException e) {
  8. e.printStackTrace();
  9. }
  10. }
  11. return method;
  12. }

MyCheckParamAspect#getParamNames():获取当前正在执行的方法的入参

  1. private String[] getParamNames(JoinPoint joinPoint) {
  2. MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
  3. String[] parameterNames = methodSignature.getParameterNames();
  4. return parameterNames;
  5. }

MyCheckParamAspect#getParamValue():获取参数对应的值

  1. private Object getParamValue(Object[] arguments, String[] paramNames, String argName) {
  2. Object value = null;
  3. String name = argName;
  4. // 从对象中取值
  5. if (argName.contains(".")) {
  6. name = argName.split("\\.")[0];
  7. }
  8. int index = 0;
  9. for (String s : paramNames) {
  10. if (s.equals(name)) {
  11. value = arguments[index];
  12. break;
  13. }
  14. index++;
  15. }
  16. if (argName.contains(".")) {
  17. argName = argName.split("\\.")[1];
  18. JSONObject jsonObject = (JSONObject)JSONObject.toJSON(value);
  19. value = jsonObject.get(argName);
  20. }
  21. return value;
  22. }

步骤五:自定义异常类

当参数校验不通过时,立即抛出异常。中断执行。

  1. @Data
  2. public class MyCheckParamException extends RuntimeException {
  3. // 错误码
  4. private Integer code;
  5. // 错误消息
  6. private String msg;
  7. public MyCheckParamException() {
  8. this(303, "参数错误");
  9. }
  10. public MyCheckParamException(String msg) {
  11. this(300, msg);
  12. }
  13. public MyCheckParamException(Integer code, String msg) {
  14. super(msg);
  15. this.code = code;
  16. this.msg = msg;
  17. }
  18. }

步骤六:自定义统一异常处理类

抛出异常后,经过统一异常处理,然后返回给前台

  1. @RestControllerAdvice
  2. public class GlobalExceptionHandler {
  3. @ExceptionHandler(MyCheckParamException.class)
  4. public Object MyCheckParamException(MyCheckParamException myCheckParamException) {
  5. return ResultVoUtil.error(myCheckParamException.getCode(), myCheckParamException.getMsg());
  6. }
  7. }

【注意】:ResultVoUtil 类上文有啊。这里就不再贴出了。

步骤七:使用自定义注解

Controller 中,哪个方法需要校验,就在哪个方法上加自定义注解 @MyCheckParam@MyCheckParams

  1. @RestController
  2. @RequestMapping("/param")
  3. public class ParamController {
  4. @PostMapping("/oneChechNotNull")
  5. @MyCheckParam(value = MyCheckParamEnum.NOT_EMPTY, argName = "userName", msg = "草,这个是必填参数!")
  6. public String oneChechNotNull(String userName) {
  7. return "oneChechNotNull";
  8. }
  9. @PostMapping("/oneChechObjectAttrNotNull")
  10. @MyCheckParam(value = MyCheckParamEnum.NOT_EMPTY, argName = "userVo.name")
  11. public String oneChechObjectAttrNotNull(@RequestBody UserVo userVo) {
  12. return "oneChechObjectAttrNotNull";
  13. }
  14. @PostMapping("/oneChechLengthIn")
  15. @MyCheckParam(value = MyCheckParamEnum.LENGTH, argName = "password", express = "6,18", msg = "密码length必须在6-18位之间!")
  16. public String oneChechLengthIn(String password) {
  17. return "oneChechLengthIn";
  18. }
  19. @PostMapping("/multiCheckLengthIn")
  20. @MyCheckParams({
  21. @MyCheckParam(value = MyCheckParamEnum.GE, argName = "password", express = "6"),
  22. @MyCheckParam(value = MyCheckParamEnum.LE, argName = "password", express = "18"),
  23. })
  24. public String multiCheckLengthIn(String password) {
  25. return "multiCheckLengthIn";
  26. }
  27. }

使用 AOP 实现接口参数校验就到这了。可以自己运行代码看看。

发表评论

表情:
评论列表 (有 0 条评论,15人围观)

还没有评论,来说两句吧...

相关阅读