目录

一、创建参数解析器

创建参数注解

参数解析器加入校验 

二、将自定义的解析器加入到参数解析器中

三、测试效果

创建用于接收GET请求的参数dto

编写控制器GET请求方法

通过postman模拟GET请求

测试校验注解

四、结语


一般的接口GET请求方式,我们是怎么接收参数的呢?

@GetMapping("/get_data_demo")
    public FastjsonDemoResponse getDataDemo(@RequestParam("user_name")String username, @RequestParam("age")Integer age) {
        FastjsonDemoResponse response = new FastjsonDemoResponse();
        response.setUserName(username);
        response.setAge(age);

        return response;
    }

我们项目中,一般是通过@RequestParam注解获取query参数值。那么如果参数有好多个的时候,这里就需要写很多个参数。这还不是最麻烦的,如果我们还需要对每个字段进行一些校验,那么代码里面就是一大串的if判断。

那么有没有一些优雅的写法呢?答案是肯定有的。

思路是将get请求参数,即"?"后面的参数值注入到控制器的dto,就好像post请求一样。

一、创建参数解析器

因为需要将query参数注入到控制器的dto,所以需要使用到参数解析器进行处理。

创建参数注解

我们使用注解的方式,来启用对应的参数解析器。

/**
 * fastjson解析注解
 *
 * 一般使用于非json格式请求的参数进行拦截注入
 *
 */
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FastJsonParser {

    boolean require() default true;
}

参数解析器加入校验 

通过HttpServletRequest.getQueryString方法,我们可以获取到请求参数,然后通过hutool工具将其转换成Map,然后再转换为JSON对象,最后将其序列化为方法参数dto。

@Component
public class FastJsonParserArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        //适配FastJsonParser注解
        return methodParameter.hasParameterAnnotation(FastJsonParser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {

        HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);

        //只适配get请求方式
        if (request.getMethod().equals("GET")) {

            //获取get请求参数
            String queryParams = request.getQueryString();
            //使用hutool工具 将query参数转换为map
            Map<String, String> paramMap = HttpUtil.decodeParamMap(queryParams, StandardCharsets.UTF_8);
            //转json
            JSONObject paramJson = JSON.parseObject(JSON.toJSONString(paramMap));
            //转换为对应的方法参数bean
            Object obj = this.mapToBean(paramJson, methodParameter, nativeWebRequest, webDataBinderFactory);

            return obj;

        }

        return null;
    }

    /**
     * 请求参数转化为对应的bean
     *
     * @param paramObject
     * @param methodParameter
     * @param nativeWebRequest
     * @param webDataBinderFactory
     * @return
     * @throws Exception
     */
    private Object mapToBean(JSONObject paramObject, MethodParameter methodParameter, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {

        //将拦截转换好的JSONObject参数,转换为注解FastJsonParser上的请求参数bean
        Object obj = JSON.to(methodParameter.getParameterType(), paramObject);

        //存在映射字段
        if (ObjectUtil.isNotEmpty(obj)) {
            //获取到入参的名称
            String name = methodParameter.getParameterName();
            //只有存在binderFactory才会去完成自动的绑定、校验
            if (webDataBinderFactory != null) {
                WebDataBinder binder = webDataBinderFactory.createBinder(nativeWebRequest, obj, name);
                if (binder.getTarget() != null) {
                    // 如果使用了validation校验, 则进行相应校验
                    if (methodParameter.hasParameterAnnotation(Valid.class)) {
                        // 如果有校验报错,会将结果放在binder.bindingResult属性中
                        binder.validate();
                    }

                    // 如果参数中不包含BindingResult参数,直接抛出异常
                    if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, methodParameter)) {
                        throw new BindException(binder.getBindingResult());
                    }

                }
            }
        }

        return obj;
    }

    /**
     * 检查参数中是否包含BindingResult参数
     *
     * @param binder
     * @param methodParam
     * @return
     */
    protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) {
        int i = methodParam.getParameterIndex();
        Class[] paramTypes = methodParam.getMethod().getParameterTypes();
        boolean hasBindingResult = paramTypes.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1]);
        return !hasBindingResult;
    }

}

在mapToBean方法中,实现了@Valid校验注解,这样才会对@FastJsonParser注解的参数的校验生效。

校验部分是参考了@RequestBody注解对@Valid校验注解是如何能生效的,笔者翻阅了一些资料和源码后发现,原来@RequestBody注解内部是自己去实现了@Valid校验,所以这两个注解同时加在请求参数dto上,为何就能生效,自定义的参数注解对@Valid校验就不生效。

二、将自定义的解析器加入到参数解析器中

/**
 * web配置类
 */
@Configuration
public class BackendWebMvcConfig extends WebMvcConfigurationSupport {

    //加入自定义的参数解析器
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new FastJsonParserArgumentResolver());
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        //定义一个convert转换消息的对象
        FastJsonHttpMessageConverter fastConverter=new FastJsonHttpMessageConverter();
        //添加fastjson的配置信息,比如是否要格式化返回的json数据;
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setWriterFeatures(
                //是否输出值为null的字段,默认为false
                JSONWriter.Feature.WriteMapNullValue,
                //将Collection类型字段的字段空值输出为[]
                JSONWriter.Feature.WriteNullListAsEmpty,
                //将字符串类型字段的空值输出为空字符串
                JSONWriter.Feature.WriteNullStringAsEmpty
        );
        //在convert中添加配置信息
        fastConverter.setFastJsonConfig(fastJsonConfig);
        //设置支持的媒体类型
        fastConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
        //设置默认字符集
        fastConverter.setDefaultCharset(StandardCharsets.UTF_8);
        //将convert添加到converters
        converters.add(0, fastConverter);

        //解决返回字符串带双引号问题
        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
        stringHttpMessageConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.TEXT_PLAIN));
        stringHttpMessageConverter.setDefaultCharset(StandardCharsets.UTF_8);
        converters.add(0, stringHttpMessageConverter);

        super.addDefaultHttpMessageConverters(converters);
    }

    /**
     * 添加静态资源
     *
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/")
                .addResourceLocations("classpath:/templates/")
                .addResourceLocations("classpath:/META-INF/resources/");
    }

    /**
     * 跨域支持
     *
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //对哪些目录可以跨域访问
        registry.addMapping("/**")
                //允许哪些网站可以跨域访问
                .allowedOrigins("*")
                //允许哪些方法
                .allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
                .maxAge(3600 * 24);
    }

}

三、测试效果

接下来,我们测试一下运行的效果。

创建用于接收GET请求的参数dto

我们使用fastjson的@JSONField注解对字段进行命名,同时加入校验注解@NotBlank对用户名字段进行判空。

@Data
public class FastjsonGetDataRequest {

    @NotBlank(message = "用户名称不能为空")
    @JSONField(name = "user_name")
    private String userName;

    @JSONField(name = "age")
    private Integer age;

}

编写控制器GET请求方法

@GetMapping("/get_data")
    public FastjsonDemoResponse getData(@FastJsonParser @Valid FastjsonGetDataRequest fastjsonGetDataRequest) {
        FastjsonDemoResponse response = new FastjsonDemoResponse();
        response.setUserName(fastjsonGetDataRequest.getUserName());
        response.setAge(fastjsonGetDataRequest.getAge());

        return response;
    }

通过postman模拟GET请求

填写请求的参数

spring boot get 请求 springboot get请求获取参数_后端

 下图可看出请求到达了参数解析器中,并能够成功获取到query的参数值。

spring boot get 请求 springboot get请求获取参数_解析器_02

通过解析器后,来到控制器中的调试窗口,可以看到get方式的请求参数已经成功注入到请求的dto中。 

spring boot get 请求 springboot get请求获取参数_java_03

至此,我们的参数解析器就能获取到GET方法的参数并注入到dto中。

测试校验注解

接下来,我们测试一下请求参数的user_name值为空,看看校验注解是否生效。

spring boot get 请求 springboot get请求获取参数_java_04

 在参数解析器中的调试窗口看到,已校验中有问题的字段了。 

spring boot get 请求 springboot get请求获取参数_java_05

从控制台抛出的异常可以看到我们自定义的异常消息,前面我们是对用户名称作为@NotBlank非空的校验。

spring boot get 请求 springboot get请求获取参数_解析器_06

 因为我们没有做统一的异常处理,所以可以看到接口的响应是400。

spring boot get 请求 springboot get请求获取参数_spring boot_07

 如果我们要做统一的异常处理,对应校验注解的异常,我们要这样子来接收。

@ResponseBody
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result methodArgumentNotValidExceptionHandler(HttpServletRequest httpServletRequest, MethodArgumentNotValidException e) {
        //打印第一条错误信息
        String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
        String exceptionText = "MethodArgumentNotValidException-请求参数格式异常";

        this.logger(httpServletRequest, exceptionText, message, e);

        return ResultUtil.fail(ResultEnum.PARAMS_ERROR.getCode(), message);
    }

    @ResponseBody
    @ExceptionHandler(BindException.class)
    public Result bindExceptionHandler(HttpServletRequest httpServletRequest, BindException e) {
        //打印第一条错误信息
        String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
        String exceptionText = "BindException-请求参数格式异常";

        this.logger(httpServletRequest, exceptionText, message, e);

        return ResultUtil.fail(ResultEnum.PARAMS_ERROR.getCode(), message);
    }

四、结语

其实细心的同学可能会发现,在参数解析器中,我们是用了一个if判断来只处理GET请求方式,那么我们是不是也能够对POST请求方式,或者是对某种content-type来独立处理呢?

答案是肯定的,但是笔者通过一系列踩坑后很负责任的告诉同学们,千万别这么做,除非真的情非得已,因为后面要做的事情就相当于要做一套spring对于参数解析器的处理,麻烦得很。