一、本文解决的痛点
按照大众认为的开发规范,一般post类型的请求参数应该传在请求body里面。但是我们有些post接口只需要传入一个字段,我们接受这种参数就得像下面这样单独创建一个类,类中再添加要传入的基本类型字段,配合@RequestBody来实现这种功能多少有点繁琐:
@Data
public class TextHolder {
private String text;
}
@PostMapping("test")
public ApiResponse test(@RequestBody TextHolder textHolder){
....
}
那么我们能不能省略类的创建,实现一个类似@RequestParam的注解来实现请求体参数的直接接收呢?本文就是来解决这个问题的!
二、实现步骤
2.1定义我们这个增强版的请求体注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestBodyPlus {
String value() default "";
}
2.2手写一个方法参数解析器
@Slf4j
public class RequestBodyPlusMethodHandler implements HandlerMethodArgumentResolver {
public static final ThreadLocal<Map<String,Object>> requestBodyMap = new ThreadLocal<>();
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.hasParameterAnnotation(RequestBodyPlus.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
RequestBodyPlus parameterAnnotation = methodParameter.getParameterAnnotation(RequestBodyPlus.class);
String paramName = parameterAnnotation.value();
if (StringUtils.isEmpty(paramName)) {
paramName = methodParameter.getParameterName();
}
Map<String, Object> paramsMap = new HashMap<>();
if (requestBodyMap.get() == null) {
String requestBodyString = getRequestBodyString(request);
paramsMap = JSON.parseObject(requestBodyString);
// 需要把请求体Map放入ThreadLocal中,因为request中的inputStream读完一次,下次就读不了了,这也是原生的@RequestBody只能在方法参数中出现一次的原因!
requestBodyMap.set(paramsMap);
}else {
paramsMap = requestBodyMap.get();
}
Object paramValue = paramsMap.get(paramName);
// 有的参数需要databinder处理
if (paramValue != null && webDataBinderFactory != null) {
WebDataBinder binder = webDataBinderFactory.createBinder(nativeWebRequest, paramValue, paramName);
paramValue = binder.convertIfNecessary(paramValue, methodParameter.getParameterType(), methodParameter);
}
return paramValue;
}
private String getRequestBodyString(final ServletRequest request){
StringBuilder stringBuilder = new StringBuilder();
try(InputStream inputStream = request.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));){
String line = "";
while((line = bufferedReader.readLine()) != null){
stringBuilder.append(line);
}
}catch (IOException e){
log.error("request的ServletInputStream转换失败",e);
}finally {
return stringBuilder.toString();
}
}
}
2.3需要写一个拦截器,用来remove上面的threadLocal避免内存泄漏
public class RequestBodyPlusInterceptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
RequestBodyPlusMethodHandler.requestBodyMap.remove();
}
}
2.4需要把拦截器和参数解析器配置好才能生效
@Configuration
public class WebConfigurer extends WebMvcConfigurerAdapter {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new RequestBodyPlusMethodHandler());
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RequestBodyPlusInterceptor());
super.addInterceptors(registry);
}
}
三、使用案例
3.1测试代码:
@RestController
@RequestMapping("plus")
public class TestRequestBodyPlus {
@PostMapping("one")
public String test01(@RequestBodyPlus("dx") Integer dx, @RequestBodyPlus("ls") String ls, @RequestBodyPlus("jk") Date jk, @RequestBodyPlus("el") List<Long> el) {
System.out.println(el);
String format = "%d_______%s_______%s_________%d";
return String.format(format, dx, ls, jk, el.size());
}
//有了下面这个方法,上面的接口的入参数就能传`2023-11-24`这种字符串
@InitBinder //该注解底层的源码:RequestMappingHandlerAdapter#invokeHandlerMethod
public void initBinder(WebDataBinder binder){
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class,new CustomDateEditor(dateFormat,false));
}//该方法的作用域仅在当前的controller,如果想全局生效,需要写在@ControllerAdvice所在的类中
}
3.2测试请求
POST localhost:8088/plus/one
{
"dx" : 3,
"ls" : "bbb",
"jk" : "2023-11-24",
"el" : [1,2,3,4,5]
}