手写一个类似@RequestParam的注解(用来接收请求体的参数)

发布于:2024-07-03 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、本文解决的痛点

按照大众认为的开发规范,一般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]
}

3.3测试结果

在这里插入图片描述