Spring中对于@RequestBody的参数解析问题

文章目录

  • 问题起源
  • 问题延伸
  • 代码实现
    • 前置准备阶段
    • 选择解决方案
      • 如何自定义Resolver
    • 处理类型
      • 如何自定义HttpMessageConverter
  • 思考总结

问题起源

今天后端与前端同事在讨论对于只有一个参数的接口,能否不将参数当作url的一部分传递,而都通过参数进行传递。
比如说:访问车辆详情页面只需要一个车辆id。

  • 第一种方式:通过访问路径传递就是:getDetail/{carId}这种方式。
  • 第二种方式:如果以get方式进行请求,那么就可以写成:getDetail?carId=xxxx;
  • 第三种方式:如果以post方式,并且访问的content-type:application/json。

对于上述三种方式在springBoot中的解析方案:

  • 第一种:@PathVariable(“carId”)
  • 第二种:@RequestParam(“carId”)
  • 第三种:@RequestBody

但是对于第三方种方案就出现了问题,前端传递的依旧是json字符串:{“carId”:“xxxx”}.
那么我们解析的时候如果入参设置为

getDetail(@RequestBody String carId)

那么carId中的值不是 "xxxx"而是整个json字符串{“carId”:“xxxx”}。
而如果我们想要解析就需要在包装一层对象,类似这样

@Data
public class CarInfo implements Serializable{
    private String carId;
}

这样按照json的规范就可以将key与属性值进行映射,然后赋予对应的值。

那么如果像删除车辆之类的POST操作,前端如果不是将参数放到url中当成路径的部分,那么放入body中的数据就不能以json格式进行封装,而只能放入carId的字符串值。
(在进行操作之类的请求将参数放入url中是否存在安全性问题,没有深入思考过有同学了解过的话可以指教下)

问题延伸

那这里就产生了问题,springBoot如何进行判断不同类型?
springBoot是如何对于不同类型进行不同的处理?
能否进行自定义的解析方法?
如何实现?

代码实现

思路:当容器收到http请求后,将请求转发给通用处理组件,将请求进行基本处理,然后交给业务进行处理,最后在对业务的返回进行处理,最后返回结果。

容器接受到http请求
前置处理组件
业务流程
后置处理组件
返回结果

其中【前置处理组件】部分就是处理@RequestBody等一些组件的地方,而【业务流程】就是日常我们编写@Controller接收各个请求后处理的业务流程 (前置处理组件内容非常多,本文只关注入参注解相关的处理)

前置准备阶段

请求进行之后会创建一个ServletInvocableHandlerMethod类进行处理

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
		implements BeanFactoryAware, InitializingBean {
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
			HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        ......
  		//创建处理器
		ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
		
		//注册方法参数解决方案,也就是本文章的重点关注对象
		if (this.argumentResolvers != null) {
		invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
   		}
		
		......
		
		//对传入的请求进行处理,我们日常写的业务流程就是在该方法中进行的,后续流程就在该方法中
  invocableMethod.invokeAndHandle(webRequest, mavContainer);
		
		......
		//业务处理完毕进行返回
		return getModelAndView(mavContainer, modelFactory, webRequest);
		
	}
}	

选择解决方案

在前置的处理过程中,我们会加载一系列实现了HandlerMethodArgumentResolver接口的方法,而这些实现类就是用来处理各种入参类型的解决方案


private Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {
        
        //1.获取定义方法上的入参信息
		MethodParameter[] parameters = getMethodParameters();
		Object[] args = new Object[parameters.length];
		//2.遍历所有入参,进行处理
		for (int i = 0; i < parameters.length; i++) {
			MethodParameter parameter = parameters[i];
			parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
			args[i] = resolveProvidedArgument(parameter, providedArgs);
			if (args[i] != null) {
				continue;
			}
			//3.从所有的Resolver列表中找到能处理当前入参的解决方案
			if (this.argumentResolvers.supportsParameter(parameter)) {
				try {
				    //4.进行处理
					args[i] = this.argumentResolvers.resolveArgument(
							parameter, mavContainer, request, this.dataBinderFactory);
					continue;
				}
				catch (Exception ex) {
					if (logger.isDebugEnabled()) {
						logger.debug(getArgumentResolutionErrorMessage("Failed to resolve", i), ex);
					}
					throw ex;
				}
			}
			if (args[i] == null) {
				throw new IllegalStateException("Could not resolve method parameter at index " +
						parameter.getParameterIndex() + " in " + parameter.getExecutable().toGenericString() +
						": " + getArgumentResolutionErrorMessage("No suitable resolver for", i));
			}
		}
		return args;
	}

this.argumentResolvers中加载了很多解决方案

  • 比如说对@RequetBody注解进行处理的,RequestResponseBodyMethodProcessor
  • 处理@RequestHeader的,RequestHeaderMethodArgumentResolver
  • 处理@RequestParam的,RequestParamMethodArgumentResolver

如果我们想要自定义一个注解,那么我们就需要实现HandlerMethodArgumentResolver并且将其注册到this.argumentResolvers中。

如何自定义Resolver

1.第一步,我们需要自己实现HandlerMethodArgumentResolver接口

@Component
public class MyTestHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
   @Override
   public boolean supportsParameter(MethodParameter parameter) {
       //处理入参的注解为@MyTest的参数
       if(parameter.hasMethodAnnotation(MyTest.class)){
           return true;
       }
       return false;
   }
   @Override
   public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
       System.out.println("进行处理");

       //这里就可以将对象进行必要的处理了,然后生成最终提供给业务的对象信息

       return "返回固定的字符串";
   }
}

2.第二步,将实现类加入到spring的解决方案集合中

我们只需要用一个类去实现 WebMvcConfigurer 接口中的

@Configuration
public class XxxConfiguration implements WebMvcConfigurer
{

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    //将实现类添加进去
    resolvers.add(xxxx);
}
   
}

处理类型

到这里其实只是区分了不同注解的解决方案,但是对于同一种注解不同的入参类型的转换问题还没有解决。
而对于不同类型的转换也定义了一个接口HttpMessageConverter 代码位置:

	protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
   		Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
   
   	   //寻找对应类型的转换器    
       for (HttpMessageConverter<?> converter : this.messageConverters) {
          if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
   					(targetClass != null && converter.canRead(targetClass, contentType))) {
   		  //转换处理
   		....
   					    
   	    }
   	    //一个处理完毕直接跳出
   	    break;
       }
   		    
   }

对于不同类型有不同的转换器,这里以@RequestBody为例,其中的转换器有如下一些:

this.messageConverters = {ArrayList@19036}  size = 10
0 = {ByteArrayHttpMessageConverter@19806} 
1 = {StringHttpMessageConverter@19070} 
2 = {StringHttpMessageConverter@19821} 
3 = {ResourceHttpMessageConverter@19822} 
4 = {ResourceRegionHttpMessageConverter@19823} 
5 = {SourceHttpMessageConverter@19824} 
6 = {AllEncompassingFormHttpMessageConverter@19825} 
7 = {MappingJackson2HttpMessageConverter@19826} 
8 = {MappingJackson2HttpMessageConverter@19827} 
9 = {Jaxb2RootElementHttpMessageConverter@19828} 

这里对于String类型的入参类型,就是使用StringHttpMessageConverter来处理。
而对于对象来说而且content-type:application/json的就使用MappingJackson2HttpMessageConverter来处理。

如何自定义HttpMessageConverter

其实方法与上面的自定义Resolver一致

  1. 自己实现一个HttpMessageConverter
  2. 通过WebMvcConfigurer实现,然后将自己定义的添加到其中
void extendMessageConverters(List<HttpMessageConverter<?>> converters) 

思考总结

spring框架中定义了非常多的扩展点,在使用过程中如果能深刻理解背后的设计,那么在应用中能避免很多不必要的错误。

你可能感兴趣的:(spring系列,spring,java,后端)