目录

今日说码

点滴记录中国代码进程

X

支持多 @RequestBody 的自定义注解

概述

  由于 @RequestBody 只能使用一次,所以通常在接收参数时会使用实体或者Map,在参数较多的情况下还要进行拆分,比较麻烦。本文通过自定义注解实现接口多次使用 @RequestBody 的功能,项目上传到96XL/Spring-MultiRequestBody: Spring多@RequestBody支持 (github.com)

功能实现

  核心类为 MultiRequestBodyMultiRequestBodyArgumentResolverWebMvcConfig

自定义注解

package com.xl.multirequestbody.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiRequestBody {

    /**
     * 参数是否必传, 当接收参数是非基本数据类型时生效
     */
    boolean required() default true;

    /**
     * 当 value 的值或者参数名不匹配时, 是否允许解析最外层属性到该对象
     */
    boolean parseAllFields() default true;

    /**
     * 解析时用到的 json key
     */
    String value() default "";
}
  • @Target(ElementType.PARAMETER) 指定注解作用范围是方法参数。
  • @Retention(RetentionPolicy.RUNTIME) 指定注解生命周期是运行时也存在。
  • 注解属性在解析时参数使用,更加灵活的实现功能。

参数解析器

package com.xl.multirequestbody.resolver;

import com.xl.multirequestbody.annotation.MultiRequestBody;
import org.springframework.core.MethodParameter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver;

import java.util.List;

public class MultiRequestBodyMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {

    public MultiRequestBodyMethodArgumentResolver(List<HttpMessageConverter<?>> converters) {
        super(converters);
    }

    /**
     * 设置支持的方法参数类型
     *
     * @param parameter 方法参数
     * @return 是否支持
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // 支持带 @MultiRequestBody 注解的参数
        return parameter.hasParameterAnnotation(MultiRequestBody.class);
    }

    /**
     * 参数解析
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Object arg = readWithMessageConverters(parameter, webRequest);
        String name = parameter.getParameterName();
        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
            if (arg != null) {
                this.validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
                    throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                }
            }

            if (mavContainer != null) {
                mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
            }
        }
        return arg;
    }

    /**
     * 获取参数
     */
    private Object readWithMessageConverters(MethodParameter parameter, NativeWebRequest webRequest) throws Exception {
        // 省略部分代码...
    }
}
  • HandlerMethodArgumentResolver 是参数解析器的接口,所有参数解析器都要实现该接口,接口中有supportsParameter()resolveArgument() 两个方法。其中supportsParameter() 方法表示是否启用这个参数解析器,返回 true 表示启用,返回 false 表示不启用。resolveArgument() 方法是具体的解析过程,就是从 request 中取出参数的过程,方法的返回值就对应了接口中参数的值。
  • 本类中继承的AbstractMessageConverterMethodArgumentResolver 也是HandlerMethodArgumentResolver 接口的一个实现类(其他实现类后面会单独写一个文章来介绍)。至于为什么要继承这个类而非其他实现类,是因为需要用到该类中的validateIfApplicable()isBindExceptionRequired() 两个方法,这两个方法是对@Validated 参数校验注解的支持。其中resolveArgument() 方法的逻辑参考了RequestResponseBodyMethodProcessor(也就是@RequestBody 注解参数解析器)的写法。
  • readWithMessageConverters() 方法是从 request 中取出参数并解析的过程,其中会使用到自定义注解中声明的属性,可以查看代码了解一下逻辑。

添加到适配器执行链

package com.xl.multirequestbody.config;

import com.xl.multirequestbody.resolver.MultiRequestBodyMethodArgumentResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import java.nio.charset.Charset;
import java.util.List;

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {

    /**
     * 添加 MultiRequestBody 参数解析器
     */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new MultiRequestBodyMethodArgumentResolver(super.getMessageConverters()));
    }

    /**
     * 解决中文乱码问题
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        super.configureMessageConverters(converters);
        converters.add(responseBodyConverter());
        converters.add(new MappingJackson2HttpMessageConverter());
    }

    /**
     * 指定编码格式
     */
    @Bean
    public HttpMessageConverter<String> responseBodyConverter() {
        return new StringHttpMessageConverter(Charset.forName("UTF-8"));
    }
}

扩展

  该注解无法和 @RequestBody 注解同时使用,因为读取 http 请求体的操作,最终都要调用 HttpServletRequestgetInputStream() 方法和 getReader() 方法,而这两个方法总共只能被调用一次,第二次调用就会报错。如果同时使用,会调用两次方法进而报错。如果想要同时使用,可以参考多次读取HTTP请求体 - 今日说码 (96xl.top)文章内容。


标题:支持多 @RequestBody 的自定义注解
作者:96XL
地址:https://solo.96xl.top/articles/2023/02/20/1676894141704.html