在使用SpringBoot接收请求时,经常会遇到独立参数传递。对于get请求可以使用注解@RequestParam 和 @PathVariable接收参数。而对于post请求,可以通过对象接收,例如以下几种方式。

  1. 通过VO对象接收,需要创建VO。
	@PostMapping("path")
    public  Result doSomething(@RequestBody Object objectVO){
    	String field = objectVO.getxxx();
    }

2.通过Map接收,需要约定key。

	@PostMapping("path")
    public  Result doSomething(@RequestBody Map map){
        String field = map.getKey("key");
    }

3.通过JsonObject接收,需要约定key。

    @PostMapping("path")
    public  Result doSomething(@RequestBody JSONObject jsonObject){
        String field = jsonObject.get("key");
    }

以上方式虽然可以解决问题,对“偷懒”的我来说不够便捷,不够优雅。能不能有一个注解如@RequestParam一样方便?故开启了探索之路,此处参考文章

  1. 第一步,仿照RequestParam自定义注解PostRequestParam。
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface PostRequestParam {
    /**
     * Alias for {@link #name}.
     */
    @AliasFor("name")
    String value() default "";

    /**
     * The name of the request parameter to bind to.
     */
    @AliasFor("value")
    String name() default "";

    /**
     * Whether the parameter is required.
     * <p>
     * Defaults to {@code true}, leading to an exception being thrown if the parameter is missing in the request. Switch
     * this to {@code false} if you prefer a {@code null} value if the parameter is not present in the request.
     * <p>
     * Alternatively, provide a {@link #defaultValue}, which implicitly sets this flag to {@code false}.
     */
    boolean required() default true;

    /**
     * The default value to use as a fallback when the request parameter is not provided or has an empty value.
     * <p>
     * Supplying a default value implicitly sets {@link #required} to {@code false}.
     */
    String defaultValue() default ValueConstants.DEFAULT_NONE;

}
  1. 第二步,创建Resolver从 RequestBody里获取参数。 此处有踩坑,后面说明。
import com.alibaba.fastjson.JSONObject;
import com.doit.annotation.PostRequestParam;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
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.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Objects;
/**
 * @Author Doit
 * @Date 2022/8/21 11:02 
 * @Desc Argument resolver for independent params of post request. 
 * @Version 1.0
 * @Slogan Just do it.
 */
@Slf4j
@Component
public class PostRequestParamResolver implements HandlerMethodArgumentResolver {
    private static final String REQUEST_POST = "post";
    private static final String REQUEST_CONTENT = "application/json";
    private final static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    /**
     * @param parameter the method parameter to check
     * @return {@code true} if this resolver supports the supplied parameter;{@code false} otherwise;
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(PostRequestParam.class);
    }

    /**
     * @param parameter the method parameter to resolve. This parameter must
     * have previously been passed to {@link #supportsParameter} which must
     * have returned {@code true}.
     * @param mavContainer the ModelAndViewContainer for the current request
     * @param webRequest the current request
     * @param binderFactory a factory for creating {@link WebDataBinder} instances
     * @return object
     * @throws Exception
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, @NotNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        String contentType = Objects.requireNonNull(servletRequest).getContentType();

        if (contentType == null || !contentType.contains(REQUEST_CONTENT)) {
            log.error("PostRequestParam' contentType must be[{}]", REQUEST_CONTENT);
            throw new RuntimeException("PostRequestParam' contentType must be application/json");
        }

        if (!REQUEST_POST.equalsIgnoreCase(servletRequest.getMethod())) {
            log.error("PostRequestParam' request type must be post");
            throw new RuntimeException("PostRequestParam' request type must be post ");
        }
        return this.bindRequestParams(parameter, servletRequest);
    }

    /**
     * Annotates that the parameters of {@code #PostRequestParam} are bound with the request.
     * @param parameter
     * @param servletRequest
     * @return object
     */
    private Object bindRequestParams(MethodParameter parameter, HttpServletRequest servletRequest) throws IOException {
        String requestBody = this.assembleRequestBody(servletRequest);
        JSONObject jsonObj =  JSONObject.parseObject(requestBody);
        if (jsonObj == null){
            throw new RuntimeException("Request body has no parameters.");
        }

        PostRequestParam postRequestParam = parameter.getParameterAnnotation(PostRequestParam.class);
        Class<?> parameterType = parameter.getParameterType();
        String parameterName = StringUtils.isBlank(postRequestParam.value())? parameter.getParameterName() : postRequestParam.value();
        Object value = jsonObj.get(parameterName);
        if (postRequestParam.required() && value == null) {
            log.error("PostRequestParam' require is true,[{}] must not be null!", parameterName);
            throw new RuntimeException("PostRequestParam' require is true,[{".concat(parameterName).concat("}] must not be null!"));
        }else{
            return ConvertUtils.convert(value,parameterType);
        }
    }

    /**
     * Assemble body from request.
     * @param request
     * @return String
     * @throws IOException
     */
    private String assembleRequestBody(HttpServletRequest request) throws IOException {
        String requestBody = new String(StreamUtils.copyToByteArray(request.getInputStream()),request.getCharacterEncoding());
        if(StringUtils.isNotBlank(requestBody)){
            threadLocal.set(requestBody);
        }else{
            requestBody = threadLocal.get();
        }
        return requestBody;
    }
}
  1. 第三步,把Resolver添加到HandlerMethodArgumentResolver里,让SpringBoot可以识别和调用。
import com.doit.resolver.PostRequestParamResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;
import java.util.List;
/**
 * @Author Doit
 * @Date 2022/8/21 10:42
 * @Desc ResolverConfiguration for {@link PostRequestParamResolver}
 * @Version 1.0
 * @Slogan Just do it.
 */
@Configuration
public class PostRequestParamResolverConfiguration implements WebMvcConfigurer {

    @Resource
    private PostRequestParamResolver postRequestParamResolver;
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers){
        resolvers.add(postRequestParamResolver);
    }
}
  1. 以上步骤完成,我们就可以愉快的使用@PostRequestParam,并且支持多参数。
    @PostMapping("path")
    public  Result doSomething(@PostRequestParam Long id , @PostRequestParam("token") String path ){
       return doSomething(id,path);
    }

踩坑分享:原来获取RequestBody的代码如下

    private String assembleRequestBody(HttpServletRequest request) throws IOException{
        if(StringUtils.isNotBlank(threadLocal.get())){
            return threadLocal.get();
        }else{
            String requestBody = new String(StreamUtils.copyToByteArray(request.getInputStream()),request.getCharacterEncoding());
            threadLocal.set(requestBody);
            return requestBody;
        }
    }

因为request的流只能被读一次,就考虑把RequestBody放入ThreadLocal中,却忽略了线程复用的问题。当线程复用时,只能取到第一次的RequestBody。所以建议每次主动覆盖ThreadLocal,或者使用完主动ThreadLocal.remove()。
复现也比较简单,将tomcat线程池最大数量设为1即可。

server:
  tomcat:
    max-threads: 1

欢迎私信或留言探讨。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐