温故知新--你一定要懂的参数校验

196 阅读5分钟

背景

最近维护一个老接口,大概是长这样的

@RestController
@RequestMapping("/xxx")
public class XxxxController{

    @PostMapping("/add_config",consume = MediaType.APPLICATTION_JSON_VALUE)
    public XxxVO add(@RequstBody ConfigReq req){
    	AssertUtil.assetNotNull(req.getXxxx(),"xxx不能为空");
        AssertUtil.assetNotNull(req.getXxxx(),"xxx不能为空");
        AssertUtil.assetNotNull(req.getXxxx(),"xxx不能为空");
        AssertUtil.assetNotNull(req.getXxxx(),"xxx不能为空");
        AssertUtil.assetNotNull(req.getXxxx(),"xxx不能为空");
        ...
    }
}

看着代码我陷入了沉思,代码量肯定是妥妥的,但处理方式却十分不优雅。我试着使用bean validation改造,就在网上翻了几篇关于spring validation使用教程的文章看看,然后踩了一些坑,于是借着这个机会重新学习一下参数校验并做一些总结。

介绍

参数校验是指对传入函数或方法的参数进行验证,确保它们满足特定的要求和规范。参数校验的作用大致有以下几点:

  1. 提高程序的健壮性:参数校验可以防止一些错误的输入,从而减少程序崩溃的可能性。
  2. 提高程序的可读性:通过参数校验,可以明确函数或方法的输入条件,让代码更易于理解和维护。
  3. 提高程序的安全性:参数校验可以防止恶意用户通过传入错误参数来攻击系统。
  4. 提高程序的可靠性:参数校验可以确保输入的参数符合预期,减少程序出错的可能性。
  5. 减少调试时间:通过参数校验可以在函数或方法内部排除一些常见的错误,从而减少调试时间。

参数校验在软件开发中具有非常重要的作用,帮助开发同学提高程序的质量和稳定性,同时它也是测试同学定测试边界的依据之一。如何做好参数校验呢?参数校验除了对输入的数据进行全面的检验如数据类型,数据范围,数据格式,sql注入等检验外,我觉得还应

  1. 简洁,可读性高
  2. 符合开闭原则,扩展容易。

JSR 303是Java社区处理Bean验证的规范,它提供了一种基于注解的声明性验证框架,使开发人员能够轻松地在Java应用程序中进行输入验证。Hibernate-Validator提供了对JSR 303规范的实现,Spring Validation又基于Hibernate-Validator做了封装提供了易于Spring使用的bean校验。

梳理&使用

梳理

使用的环境是基于springboot2.3.12.RELEASE版本构建springMVC应用,在使用Validation之前,我们先分析一下那些接口场景下会用到参数校验

yuque_diagram.png

为什么这里只列举这五种情况呢?因为这五种情况是我们平时开发中最常见的接口提供形式,也是我们所推荐的形式。其他的为什么不推荐使用呢?例如GET请求的参数也可以直接绑定的Map<String,Object>这样一个map里,但是可读性太差,可能除了当时开发的人知道会有什么参数,后面维护的人或者是过一段时间后的开发本人也不知道这里会有什么参数,所以在平时的开发中我们一定要面向对象编程(这种可读性不高的Map要尽量避免),这里的对象一定是可读性高的对象。

使用

基于上面梳理的情况,我们对validation做一个简单的使用,并把校验异常整合到全局异常处理器,所有响应结果统一格式

配置统一响应

package org.homeey.deploy.commons.component.handler;

import org.homeey.deploy.interfaces.web.vo.ResultVO;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletResponse;

/**
 * 重写rest请求返回值
 *
 * @author jt4mrg@qq.com
 * 16:39 2022/12/4 2022
 **/
public class RewriteReturnValueHandler implements HandlerMethodReturnValueHandler {

    private MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        //Do a judgement for supporting rewrite method return value type
        return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
                returnType.hasMethodAnnotation(ResponseBody.class)) &&
                !ResultVO.class.equals(returnType.getParameterType());
    }

    @Override
    public void handleReturnValue(Object returnValue,
                                  MethodParameter returnType,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest) throws Exception {
        mavContainer.setRequestHandled(true);
        ResultVO<Object> result = new ResultVO<>();
        result.setCode(0);
        result.setMsg("success");
        result.setData(returnValue);
        ServletServerHttpResponse httpOutMessage = createOutputMessage(webRequest);
        converter.write(result, MediaType.APPLICATION_JSON, httpOutMessage);
    }

    protected ServletServerHttpResponse createOutputMessage(NativeWebRequest webRequest) {
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        Assert.state(response != null, "No HttpServletResponse");
        return new ServletServerHttpResponse(response);
    }
}

@Configuration
public class WebMvcConfiguration {

    @Autowired
    public void resetRequestMappingHandlerAdapter(RequestMappingHandlerAdapter requestMappingHandlerAdapter) {
        List<HandlerMethodReturnValueHandler> oldReturnValueHandlers = requestMappingHandlerAdapter.getReturnValueHandlers();
        List<HandlerMethodReturnValueHandler> newReturnValueHandlers = new ArrayList<>(oldReturnValueHandlers);
        newReturnValueHandlers.add(0, new RewriteReturnValueHandler());
        requestMappingHandlerAdapter.setReturnValueHandlers(newReturnValueHandlers);
    }

}

@Data
public class ResultVO<T> {
    /**
     * 协议状态码
     */
    @JsonProperty("status_code")
    private Integer code;

    /**
     * 状态描述
     */
    @JsonProperty("status_msg")
    private String msg;

    /**
     * 数据域
     */
    private T data;

    /**
     * 错误时的详细信息,正常返回时此处为空
     */
    @JsonProperty("detail_msg")
    private String detailMsg;
    /**
     * 链路id
     */
    @JsonProperty("trace_id")
    private String traceId;
}

GET 单独参数绑定

@Slf4j
@Validated
@RestController
public class GetSimpleController {
    @GetMapping(value = "/get_simple", produces = MediaType.APPLICATION_JSON_VALUE)
    public String simpleValidate(@RequestParam("name") @NotBlank(message = "名字不能为空") String name,
                                 @RequestParam("age") @Max(value = 100, message = "年龄不能超过100岁") @Min(value = 0, message = "年能不能为负数") Integer age) {
        log.info("request param:name={},age={}", name, age);
        return "hello world";
    }
}

GET 绑定对象

@Slf4j
@RestController
public class GetObjController {

    @GetMapping(value = "/get_object", produces = MediaType.APPLICATION_JSON_VALUE)
    public String validateGetError(@Validated UserReq userReq) {
        log.info("request param:{}", userReq);
        return "hello world";
    }
}

POST json到对象绑定

@RestController
public class PostController {
    @PostMapping(value = "/post_json", consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public String validateError(@RequestBody @Validated UserReq userReq) {
        log.info("request param:{}", userReq);
        return "hello world";
    }
}

POST form对象参数绑定

@Slf4j
@RestController
public class PostFormObjController {
    @PostMapping(value = "/post_form_obj", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public String validatePostError(@Validated UserReq req) {
        log.info("request param:{}", req);
        return "hello world";
    }
}

POST form单独参数绑定

@Validated
@Slf4j
@RestController
public class PostFormSingleController {
    @PostMapping(value = "/post_form_simple", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public String validatePostError(@RequestParam("name") @NotBlank(message = "名字不能为空") String name,
                                    @RequestParam("age") @Min(value = 0, message = "年龄不能为负") @Max(value = 100, message = "年龄不能超过一百岁") String age) {
        log.info("request param:name={},age={}", name, age);
        return "hello world";
    }
}

全局异常处理

@Slf4j
@RestControllerAdvice
public class WebMvcExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResultVO<Object> revealAllExp(Exception e) {
        ResultVO<Object> resultVO = new ResultVO<>();
        resultVO.setMsg("fail");
        resultVO.setCode(-1);
        return resultVO;
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultVO<Object> invalidPostArgs(MethodArgumentNotValidException e, HttpServletRequest request) {
        ResultVO<Object> resultVO = new ResultVO<>();
        resultVO.setMsg("PARAM_ERROR");
        resultVO.setCode(4000);
        resultVO.setDetailMsg(this.parseErrMsg(e.getBindingResult().getFieldErrors()));
        return resultVO;
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public ResultVO<Object> invalidGetArgs(ConstraintViolationException e, HttpServletRequest request) {
        ResultVO<Object> resultVO = new ResultVO<>();
        resultVO.setMsg("PARAM_ERROR");
        resultVO.setCode(4000);
        String message = e.getMessage();
        resultVO.setDetailMsg(message);
        return resultVO;
    }

    @ExceptionHandler(BindException.class)
    public ResultVO<Object> invalidGetObjArgs(BindException e, HttpServletRequest request) {
        ResultVO<Object> resultVO = new ResultVO<>();
        resultVO.setMsg("PARAM_ERROR");
        resultVO.setCode(4000);
        BindingResult bindingResult = e.getBindingResult();
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        String errMsg = parseErrMsg(fieldErrors);
        resultVO.setDetailMsg(errMsg);
        return resultVO;
    }

    private String parseErrMsg(List<FieldError> fieldErrors) {
        return fieldErrors.stream()
                .map(fieldError -> fieldError.getField() + "=[" + fieldError.getDefaultMessage() + "]")
                .collect(Collectors.joining(","));
    }
}

我们可以看到参数校验有三种类型的的异常,它对应了参数绑定的三种场景。从使用者的角度,其实我觉得都是参数校验可以使用一种校验异常如果校验失败把校验结果通过异常透传给我们,这样可以减少使用者的心智负担。但是从开发者的角度呢,细化场景是很有必要的,这样可以快读定位异常出在哪里,就像我们做业务开发,统一定义的用户场景相关的异常,也会细化到像用户不存在,用户无权限等细分场景。

总结

最后用一幅图总结下Validation使用

2.png