HandlerMethodArgumentResolver - 自定义参数解析器代替特定场景中的@RequestBody
写在前面
今天一个C++转java的大佬和我吐槽说项目中使用的@RequestBody为什么不能映射多个对象,每次前端有变动都要改接收对象,非常难顶。
一、关于@RequestBody的痛点
项目中遇到的问题是这样的。有一个附件管理模块,只要有一个ID和一个文件列表,我们就能将附件列表与该条记录绑定起来。从而实现,附件与其他的模块解耦。但是要求数据插入的时候,同时要进行绑定。
class BaseDto{
String id;
String context;
}
Class FileDto{
String id;
List<String> fileList;
}
@RequestMapping("/test")
public String test(@RequestBody BaseDto baseDto){
...
}
前端使用的是json对象传输,请求如下:
{
"context":"12345678",
"fileList":[ "1", "2", "3"]
}
这时候后端如果想获取BaseDto类里面没有的参数,我们可以通过继承,让子类可以接收更多的参数
class BaseParams extends BaseDto{
List<String> fileList;
}
@RequestMapping("/upload")
public String upload(@RequestBody BaseParams baseParams){
...
}
这种方法好,也不好。因为FileDto是一个容易变动的类,请求体总是会出现各种各样奇怪的参数。比如,文件类型权限列表、文件类型列表等等。再或许文件系统变动,原本的字符串列表,变成map列表。这样的话FileDto的变动又要改BaseParams
如果能直接用BaseDto和FileDto同时接收参数,那就不用去维护一个接收的参数的类了
@RequestMapping("/upload")
public String upload(@RequestBody BaseDto baseDto, @RequestBody FileDto fileDto){
...
}
但这样子肯定是报错的,因为@RequestBody 只能在一个参数上使用
二、解决方法
1. 使用Map<String,Object>
遇事不决用Map,可能会麻烦点,但不会出错。拿到全部值再映射到对象上
@RequestMapping("/test")
public String test(@RequestBody Map<String, Object> map){
...
}
public static <T> T parseEntity(Map<String, Object> map, Class<T> entity){
return JSON.parseObject(JSON.toJSONString(map), entity);
}
2. 使用json字符串
用JSON去做对象的映射,json字符串比Map还方便呢
@RequestMapping("/test")
public String test(@RequestBody String jsonString){
BaseDto baseDto = JSON.parseObject(jsonString, BaseDto.class);
FileDto file = JSON.parseObject(jsonString, FileDto.class);
}
3. 自定参数解析器封装 @MultiRequestBody
其实就是把对象的映射封装进参数解析器里面,让我们能像@RequestBody一样,一个注解就能完成上面的事。只不过我们的注解是能重复使用的。
1.创建注解
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiRequestBody {
String value() default "";
}
2.实现HandlerMethodArgumentResolver接口
重写supportsParameter方法,判断当前的接受对象是否合适调用我们的ArgumentResolver
public class MultiRequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
MultiRequestBody ann = methodParameter.getParameterAnnotation(MultiRequestBody.class);
// 判断是否带上了我们的MultiRequestBody注解
if (ann == null){
return false;
}
Class<?> parameterType = methodParameter.getParameterType();
// 如果是基本类型,不支持
if (parameterType.isPrimitive()){
return false;
}
// 一些业务判断逻辑...
return true;
}
}
接着就是重写resolveArgument方法,把参数映射到对象的逻辑写到这里
在处理这个问题之前还有一个问题。HttpServletRequest 获取POST请求数据只能获取一次,这也是为何 @RequestBody只能在一个参数上的原因。获取完,流就关闭了,所以我们如果要多次获取的话,就要将这个流序列化。
解决思路是继承HttpServletRequestWrapper这个包装类,来实现流的序列化,以及序列化值的获取
public class CachingRequestWrapper extends HttpServletRequestWrapper {
private byte [] bodyCache;
public CachingRequestWrapper(HttpServletRequest request) {
super(request);
try{
InputStream requestInputStream = request.getInputStream();
this.bodyCache = StreamUtils.copyToByteArray(requestInputStream);
} catch (IOException e) {
bodyCache = new byte[0];
}
}
public String getHttpRequestBody() {
return bodyCache.length == 0 ? "" : new String(bodyCache);
}
}
再重写OncePerRequestFilter,将包装类替换成我们重写的包装类
@Component
public class CachingRequestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain)
throws ServletException, IOException {
// 替换成自己写的 CachingRequestWrapper
CachingRequestWrapper requestWrapper = new CachingRequestWrapper(httpServletRequest);
filterChain.doFilter(requestWrapper, httpServletResponse);
}
}
终于可以重写resolveArgument方法了 先写一个通用获取 get、post请求体的方法
public class MultiRequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
private String getHttpRequestBody(HttpServletRequest request) {
if (request.getMethod().equalsIgnoreCase("get")) {
return request.getQueryString();
} else {
if (request instanceof CachingRequestWrapper) {
return ((CachingRequestWrapper) request).getHttpRequestBody();
}
}
System.out.println(String.format("request 非 CachingRequest %s", request.getClass()));
return "";
}
}
然后写参数映射到对象的逻辑
public class MultiRequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
String jsonString = getHttpRequestBody(request);
MultiRequestBody ann = methodParameter.getParameterAnnotation(MultiRequestBody.class);
Class<?> parameterType = methodParameter.getParameterType();
// 如果注解的值不为空,我们则先查找到该 key
// 不然直接解析
if (StringUtils.isEmpty(ann.value())) {
return JSON.parseObject(jsonString, parameterType);
} else {
JSONObject jsonObject = JSON.parseObject(jsonString);
return jsonObject.getObject(ann.value(), parameterType);
}
}
}
- 继承WebMvcConfigurer,将我们的自定义参数解析器加到配置里
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public MultiRequestBodyArgumentResolver getMultiRequestBodyArgumentResolver(){
return new MultiRequestBodyArgumentResolver();
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(getMultiRequestBodyArgumentResolver());
}
}
三、使用@MultiRequestBody
可以直接映射多个对象
四、使用@MultiRequestBody导致@RequestBody失效
因为我们CachingRequestWrapper的构造方法把request的InputStream给占用了。导致@RequestBody调用时,流会为空。所以还要重写CachingRequestWrapper中的getInputStream和getReader这两个方法
1. 继承ServletInputStream
由于需要 ServletInputStream ,故我们需要写一个自己继承 ServletInputStream 的流
public class CachingInputStream extends ServletInputStream {
private InputStream cachedBodyInputStream;
public CachingInputStream(byte[] cachedBody) {
this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public boolean isFinished() {
try {
return cachedBodyInputStream.available() == 0;
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
return cachedBodyInputStream.read();
}
}
2. 重写getInputStream和getReader
public class CachingRequestWrapper extends HttpServletRequestWrapper {
@Override
public ServletInputStream getInputStream() throws IOException {
return new CachingInputStream(this.bodyCache);
}
@Override
public BufferedReader getReader() throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.bodyCache);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
}
3. 运行结果
完美解决问题
写在最后
此文基本是大佬的思路和实现,我就做了一些修改,或许使用Map或者json字符串会更加方便,但是愿意去改进和思考改进思路,让代码能更加简洁,值得尊敬。解决问题的过程也收益良多。
参考资料
HandlerMethodArgumentResolver(四):自定参数解析器处理特定应用场景 Spring多次读取HttpServletRequest
转载自:https://juejin.cn/post/7040480740957487134