自定义参数校验以及统一处理结果集

1.引入校验组件

         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

更多校验使用方法请参考:java后端参数校验validaction(用法详解)

2.定义统一返回结果

统一返回值类型无论项目前后端是否分离都是非常必要的,方便对接接口的开发人员更加清晰地知道这个接口的调用是否成功(不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此),使用一个状态码、状态信息就能清楚地了解接口调用情况

@Data
public class ResponseResult<T>  {
    private static final long serialVersionUID = 5337617615451873318L;
    /**
     * code 200 表示业务处理成功,-200表示业务处理失败
     *
     * @since 1.0
     **/
    private String code;
    /**
     * 存放业务提示信息
     *
     * @since 1.0
     **/
    private String message;
    /**
     * subCode 10000 表示10000请求成功 -10000表示请求处理失败
     *
     * @since 1.0
     **/

    private String subCode;
    /**
     * 存放系统异常消息
     *
     * @since 1.0
     **/
    private String subMessage;
    /**
     * 存放系统异常具体错误详情
     *
     * @since 1.0
     **/
    private String errorDetails;
    /**
     * 响应时间
     *
     * @since 1.0
     **/
    private String responseTime;
    /**
     * 出错的服务mac地址
     *
     * @since 1.0
     **/
    private String mac;
    /**
     * 预留处理字段
     *
     * @since 1.0
     **/
    private Object option;
    /**
     * 执行时间
     *
     * @since 1.0
     **/
    private Long executeTime;

    /**
     * 响应数据
     *
     * @since 1.0
     **/
    private T data;

    private static final String UNKNOWN_ERROR_CODE = "9999";

    private static final String SUCCESS_CODE = "10000";
    private static final String ERROR_CODE = "0";

    private static final String BUSINESS_SUCCESS = "200";
    private static final String BUSINESS_ERROR = "500";

    private static final String SYSTEM_ERROR_MESSAGE = "系统异常,请联系管理员处理";
    private static final String SYSTEM_SUCCESS_MESSAGE = "服务调用成功";
    private static final String UNKNOWN_ERROR_MESSAGE = "未知异常";


    private ResponseResult() {
    }

    private ResponseResult(String code, String message, String subMessage, String subCode, T data, String errorDetails,Long executeTime) {
        this.code = code;
        this.message = message;
        this.subMessage = subMessage;
        this.subCode = subCode;
        this.data = data;
        this.errorDetails = errorDetails;
        // 将日期格式化为:yyyy-MM-dd HH:mm:ss
        this.responseTime = DateTools.format(new Date(), DateTools.DEFAULT_DATETIME_FORMAT);
        String mac = SysTemUtil.getLocalMacAddress(null);
        this.mac = StringUtils.hasLength(mac) ? mac.replaceAll("-", "") : "";
        this.executeTime = executeTime;
    }

    private ResponseResult(String code, String message, String subMessage, String subCode, T data, String errorDetails ) {
        this.code = code;
        this.message = message;
        this.subMessage = subMessage;
        this.subCode = subCode;
        this.data = data;
        this.errorDetails = errorDetails;
        // 将日期格式化为:yyyy-MM-dd HH:mm:ss
        this.responseTime = DateTools.format(new Date(), DateTools.DEFAULT_DATETIME_FORMAT);
        String mac = SysTemUtil.getLocalMacAddress(null);
        this.mac = StringUtils.hasLength(mac) ? mac.replaceAll("-", "") : "";
    }
    // 构造一个响应结果对象
    public static <E> ResponseResult<E> build(String code, String message, String subMessage, String subCode, E data, String errorDetails) {
        return new ResponseResult<>(code, message, subMessage, subCode, data, errorDetails);

    }
    // 简单的成功响应不带返回值
    public static <E> ResponseResult<E> success() {
        return build(BUSINESS_SUCCESS, "", SYSTEM_SUCCESS_MESSAGE, SUCCESS_CODE, null, "");
    }
    // 简单的成功响应
    public static <E> ResponseResult<E> success(E data) {
        return build(BUSINESS_SUCCESS, "", SYSTEM_SUCCESS_MESSAGE, SUCCESS_CODE, data, "");
    }

    // 简单的成功响应,携带提示信息
    public static <E> ResponseResult<E> success(String message, E data) {
        return build(BUSINESS_SUCCESS, message, SYSTEM_SUCCESS_MESSAGE, SUCCESS_CODE, data, "");
    }

    // 简单的失败响应
    public static <E> ResponseResult<E> error(String subMessage) {
        return build(BUSINESS_ERROR, subMessage, SYSTEM_SUCCESS_MESSAGE, SUCCESS_CODE, null, "");
    }

    // 系统异常的失败响应
    public static <E> ResponseResult<E> systemError(String errorDetails) {
        return build(BUSINESS_ERROR, SYSTEM_SUCCESS_MESSAGE, SYSTEM_ERROR_MESSAGE, SUCCESS_CODE, null, errorDetails);
    }

    // 失败响应写的异常原因
    public static <E> ResponseResult<E> error(String message, String subMessage) {
        return build(BUSINESS_ERROR, message, subMessage, ERROR_CODE, null, "");
    }

    // 响应失败,携带数据和提示信息
    public static <E> ResponseResult<E> error(String message, E data) {
        return build(BUSINESS_ERROR, message, SYSTEM_SUCCESS_MESSAGE, ERROR_CODE, data, "");
    }
}

获取本机物理地址的工具方法:

   /**
     * 获取本地mac地址
     * 注意:物理地址是48位,别和ipv6搞错了
     * @param inetAddress
     * @return 本地mac地址
     */
    public static   String getLocalMacAddress(InetAddress inetAddress) {
        if (inetAddress==null){
            try {
                inetAddress = InetAddress.getLocalHost();
            } catch (UnknownHostException e) {
                e.printStackTrace();
                throw new RuntimeException("获取主机mac地址失败");
            }
        }
        try {
            //获取网卡,获取地址
            byte[] mac = NetworkInterface.getByInetAddress(inetAddress).getHardwareAddress();
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < mac.length; i++) {
                if (i != 0) {
                    sb.append("-");
                }
                //字节转换为整数
                int temp = mac[i] & 0xff;
                String str = Integer.toHexString(temp);
                if (str.length() == 1) {
                    sb.append("0").append(str);
                } else {
                    sb.append(str);
                }
            }
            return sb.toString();
        } catch (Exception exception) {
            exception.getMessage();
        }
        return "";
    }

3.返回值统一包装处理

Spring 中提供了一个类 ResponseBodyAdvice ,能帮助我们实现上述需求

ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。那这样就可以把统一包装的工作放到这个类里面。

public interface ResponseBodyAdvice<T> {
    boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

    @Nullable
    T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends 		HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);
}

4.@RestControllerAdvice+ResponseBodyAdvice统一处理

import com.springboot.example.bean.ResponseResult;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * @RestControllerAdvice+@ExceptionHandler 异常处理器的优先级要高,如果那边处理过了是ResponseResult,直接返回
 * 所以建议在 @RestControllerAdvice+@ExceptionHandler进行处理可以获取到完整的错误信息,                 			
 * @RestControllerAdvice+ResponseBodyAdvice适合对所有返回值进行第二次处理,不适合异常消息捕获
 * @author compass
 * @date 2023-02-05
 * @since 1.0
 **/
@RestControllerAdvice
public class ResponseHandler implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object result, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
       try {
           if (result instanceof ResponseResult) {
               return result;
           } else if ("error".equals(Objects.requireNonNull(methodParameter.getMethod()).getName())) {
               Map map = (Map) result;
               String path = map.get("path") == null ? "" : map.get("path").toString();
               Object errors = map.get("errors");
               if (errors instanceof List) {
                   List errorList = (List) errors;
                   StringBuilder subMessageBuffer = new StringBuilder();
                   StringBuilder messageBuffer = new StringBuilder();
                   for (Object item : errorList) {
                       if (item instanceof FieldError) {
                           FieldError error = (FieldError) item;
                           String rejectedValue = error.getRejectedValue() + "";
                           String field = error.getField();
                           String message = error.getDefaultMessage();
                           messageBuffer.append("请求路径:").append(path).append(";");
                           messageBuffer.append("校验不通过的字段名:").append(field).append(",")
                                   .append("被拒绝的值:").append(rejectedValue).append(",")
                                   .append("提示信息:").append(message).append(";");
                           subMessageBuffer.append(message).append(",");
                       }
                       String message = messageBuffer.toString();
                       String subMessage = subMessageBuffer.toString();
                       subMessage = subMessage.substring(0, subMessage.lastIndexOf(','));
                       return ResponseResult.build("10000", message, subMessage, "500", null, null);
                   }
               }
               // 处理是 @PathVariable 和 @RequestParam 参数校验
               Object stats = map.get("status");
               if (Integer.valueOf(500).equals(stats)) {
                   String message = "请求路径:"+map.get("path")+";"+map.get("message");
                   String subMessage = map.get("message")+"";
                   return ResponseResult.build("10000", message, subMessage, "500", null, null);
               }
           }
           return result;
       }catch (Exception e){
           e.printStackTrace();
           return ResponseResult.error("系统异常","统一返回值处理失败");
       }
    }
}

5.@RestControllerAdvice+@ExceptionHandler处理

在 SpringMVC 中,有一个类是 RequestResponseBodyMethodProcessor,这个类有两个作用

  1. 用于解析 @RequestBody 标注的参数
  2. 处理 @ResponseBody 标注方法的返回值

解析 @RequestBoyd 标注参数的方法是 resolveArgument

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
      /**
     * Throws MethodArgumentNotValidException if validation fails.
     * @throws HttpMessageNotReadableException if {@link RequestBody#required()}
     * is {@code true} and there is no body content or if there is no suitable
     * converter to read the content with.
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

      parameter = parameter.nestedIfOptional();
      //把请求数据封装成标注的DTO对象
      Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
      String name = Conventions.getVariableNameForParameter(parameter);

      if (binderFactory != null) {
        WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
        if (arg != null) {
          //执行数据校验
          validateIfApplicable(binder, parameter);
          //如果校验不通过,就抛出MethodArgumentNotValidException异常
          //如果我们不自己捕获,那么最终会由DefaultHandlerExceptionResolver捕获处理
          if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
            throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
          }
        }
        if (mavContainer != null) {
          mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
        }
      }

      return adaptArgumentIfNecessary(arg, parameter);
    }
}

public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
  /**
    * Validate the binding target if applicable.
    * <p>The default implementation checks for {@code @javax.validation.Valid},
    * Spring's {@link org.springframework.validation.annotation.Validated},
    * and custom annotations whose name starts with "Valid".
    * @param binder the DataBinder to be used
    * @param parameter the method parameter descriptor
    * @since 4.1.5
    * @see #isBindExceptionRequired
    */
   protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    //获取参数上的所有注解
      Annotation[] annotations = parameter.getParameterAnnotations();
      for (Annotation ann : annotations) {
      //如果注解中包含了@Valid、@Validated或者是名字以Valid开头的注解就进行参数校验
         Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
         if (validationHints != null) {
        //实际校验逻辑,最终会调用Hibernate Validator执行真正的校验
        //所以Spring Validation是对Hibernate Validation的二次封装
            binder.validate(validationHints);
            break;
         }
      }
   }
}

自定义异常处理器进行参数校验失败异常处理:

/**
 * 全局异常处理[包括自定义异常处理]
 *
 * @author compass
 * @date 2022/11/1 22:08
 * @since 1.0.0
 **/
@RestControllerAdvice
public class ExceptionHandlerCase {
    // 处理 @RequestBody
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    @ResponseBody
    public ResponseResult methodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
        try {
            BindingResult bindingResult = e.getBindingResult();
            List<FieldError> errorList = bindingResult.getFieldErrors();
            StringBuilder subMessageBuffer = new StringBuilder();
            StringBuilder messageBuffer = new StringBuilder();
            String requestURI = request.getRequestURI();
            messageBuffer.append("接口地址:").append(requestURI).append(";");
            for (FieldError error : errorList) {
                String rejectedValue = error.getRejectedValue() + "";
                String field = error.getField();
                String message = error.getDefaultMessage();
                messageBuffer.append("校验不通过的字段名:").append(field).append(",")
                        .append("被拒绝的值:").append(rejectedValue).append(",")
                        .append("提示信息:").append(message).append(";");
                subMessageBuffer.append(message).append(",");
            }
            String message = messageBuffer.toString();
            String subMessage = subMessageBuffer.toString();
            subMessage = subMessage.substring(0, subMessage.lastIndexOf(','));
            return ResponseResult.build("10000", message, subMessage, "500", null, null);
        } catch (Exception ex) {
            ex.printStackTrace();
            return ResponseResult.error("参数校验返回结果集处理出错");
        }


    }

   // 处理 @PathVariable 和 @RequestParam 参数校验
    @ExceptionHandler(value = ConstraintViolationException.class)
    @ResponseBody
    public ResponseResult constraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
        try {
            Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
            StringBuilder subMessageBuffer = new StringBuilder();
            StringBuilder messageBuffer = new StringBuilder();
            String requestURI = request.getRequestURI();
            messageBuffer.append("接口地址:").append(requestURI).append(";");
            for (ConstraintViolation<?> violation : violations) {
                String message = violation.getMessage();
                String rejectedValue = violation.getInvalidValue() + "";
                String field = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName();
                messageBuffer.append("校验不通过的字段名:").append(field).append(",")
                        .append("被拒绝的值:").append(rejectedValue).append(",")
                        .append("提示信息:").append(message).append(";");
                subMessageBuffer.append(message).append(",");
            }
            String message = messageBuffer.toString();
            String subMessage = subMessageBuffer.toString();
            subMessage = subMessage.substring(0, subMessage.lastIndexOf(','));
            return ResponseResult.build("10000", message, subMessage, "500", null, null);

        } catch (Exception ex) {
            ex.printStackTrace();
            return ResponseResult.error("参数校验返回结果集处理出错");
        }
    }

}

6.方法基本参数校验原理

7.方法级别的参数校验实现原理

上面提到的将参数一个个平铺到方法参数中,然后在每个参数前面声明约束注解的校验方式,就是方法级别的参数校验。实际上,这种方式可用于任何Spring Bean的方法上,比如Controller/Service等。其底层实现原理就是AOP,具体来说是通过MethodValidationPostProcessor动态注册AOP切面,然后使用MethodValidationInterceptor对切点方法织入增强

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {
    @Override
    public void afterPropertiesSet() {
        //为所有`@Validated`标注的Bean创建切面
        Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
        //创建Advisor进行增强
        this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
    }
 
    //创建Advice,本质就是一个方法拦截器
    protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
        return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
    }
}

接着看一下MethodValidationInterceptor

public class MethodValidationInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //无需增强的方法,直接跳过
        if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
            return invocation.proceed();
        }
        //获取分组信息
        Class<?>[] groups = determineValidationGroups(invocation);
        ExecutableValidator execVal = this.validator.forExecutables();
        Method methodToValidate = invocation.getMethod();
        Set<ConstraintViolation<Object>> result;
        try {
            //方法入参校验,最终还是委托给Hibernate Validator来校验
            result = execVal.validateParameters(
                invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        catch (IllegalArgumentException ex) {
            ...
        }
        //有异常直接抛出
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        //真正的方法调用
        Object returnValue = invocation.proceed();
        //对返回值做校验,最终还是委托给Hibernate Validator来校验
        result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
        //有异常直接抛出
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        return returnValue;
    }
}

不管是 requestBody参数校验 还是 方法级别的校验,最终都是调用 Hibernate Validator 执行校验, Spring Validation 只是做了一层封装

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值