在使用SpringBoot接收请求时,经常会遇到独立参数传递。对于get请求可以使用注解@RequestParam 和 @PathVariable接收参数。而对于post请求,可以通过对象接收,例如以下几种方式。
- 通过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一样方便?故开启了探索之路,此处参考文章。
- 第一步,仿照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;
}
- 第二步,创建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;
}
}
- 第三步,把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);
}
}
- 以上步骤完成,我们就可以愉快的使用@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
欢迎私信或留言探讨。