• 学习@RequestBody注解解析请求参数流程


    一、背景

    研究对象是Springboot的一个后台Web系统。
    想了解,在SpringMVC对@RequestBody的参数进行注入之前,执行了request.getInputStream()/request.getReader()或者request.getParameter()方法,会不会对参数的获取造成影响。也就是@RequestBody是如何获取到Http请求体中的参数的。

    二、Controller中Handler的注册

    留意到每次系统启动的时候,Spring会打印这类日志

    Mapped "{[/xxx/yyyy/aaa],methods=[POST]}" onto ......
    因此,对于Controller中对RequestMapping的解析,就从此处的日志开始。看看在解析RequestMapping的时候,有没有对@RequestBody注解的参数进行处理。

    1. 找到打印这行日志的类以及行数, RequestMappingHandlerMapping:547.
      发现这个类中总共都没有547行,那么就去它继承的父类中去找,结果在AbstractHandlerMethodMapping这个类中找到了对应的行和方法。
    public void register(T mapping, Object handler, Method method) {
      // 省略
    }
    

    从方法名以及参数上来看,肯定是将Controller中每个RequestMapping对应的Method注册起对应关系。但是入参到底是啥也不清楚,那么就看哪些地方调用了这个方法。
    发现了如下的调用链路:

    AbstractHandlerMethodMapping#initHandlerMethods
    AbstractHandlerMethodMapping#detectHandlerMethods
    AbstractHandlerMethodMapping#registerHandlerMethod
    AbstractHandlerMethodMapping#register

    1. 那么从initHandlerMethods开始看
    protected void initHandlerMethods() {
        // 省略,获取所有beanNames
    		for (String beanName : beanNames) {
    			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
    				Class<?> beanType = null;
    				try {
    					beanType = obtainApplicationContext().getType(beanName);
    				}
    				catch (Throwable ex) {
    					// An unresolvable bean type, probably from a lazy bean - let's ignore it.
    					if (logger.isDebugEnabled()) {
    						logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
    					}
    				}
                                    // 关键代码处
    				if (beanType != null && isHandler(beanType)) {
    					detectHandlerMethods(beanName);
    				}
    			}
    		}
    		handlerMethodsInitialized(getHandlerMethods());
    	}
    

    在这个方法中的关键代码处可以看到,会对bean进行判断isHandler, 如果是Handler,那么就去解析里面的Methods

    @Override
    	protected boolean isHandler(Class<?> beanType) {
    		return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
    				AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
    	}
    

    可以看到,只要是有@Controller或者@RequestMapping的Bean,都是Handler。
    也可以看到所谓的Handler就是我们日常说的身为Controller的Bean。
    3. 继续进入detectHandlerMethods方法中

    Class<?> handlerType = (handler instanceof String ?
    				obtainApplicationContext().getType((String) handler) : handler.getClass());
    
    		if (handlerType != null) {
    			final Class<?> userType = ClassUtils.getUserClass(handlerType);
    			Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
    					(MethodIntrospector.MetadataLookup<T>) method -> {
    						try {
    							return getMappingForMethod(method, userType);  //关键处1
    						}
    						catch (Throwable ex) {
    							throw new IllegalStateException("Invalid mapping on handler class [" +
    									userType.getName() + "]: " + method, ex);
    						}
    					});
    			if (logger.isDebugEnabled()) {
    				logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods);
    			}
    			methods.forEach((method, mapping) -> {
    				Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
    				registerHandlerMethod(handler, invocableMethod, mapping);  //关键处2
    			});
    		}
    

    这个方法就是获取Controller中的所有方法,并一个个去解析
    在关键处1,里面所做的事情就是先判断这个方法是否有@RequestMapping注解,其次获取注解里面的信息(请求头、请求方法,请求参数等)并记录到RequestMappingInfo对象中。 并且假如Controller上也有RequestMapping注解,那就还要进行一些合并操作。都做完了就返回一个整体的RequestMappingInfo对象

    在关键处2,就是建立Mapping与Method的映射关系,等到实际调用的时候,根据请求的地址解析得到Mapping,取出相应的Method进行调用。
    当然这里面是有代理的,具体细节就没有去详细看。因为我的目的是先了解流程。
    在这个解析过程中,好像并没有对@RequestBody的处理,那么就看看在实际调用的时候,是怎么处理@RequestBody的

    二、@RequestBody参数解析

    一个Http请求,必然从Servlet的doService开始的(抛开拦截器与过滤器),那么就从SpringMVC的DispatcherServlet开始入手。顺着doService看到doDispatch,然后doDispatch中可以看到一行:

    // Actually invoke the handler.
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    

    那么实际的接口处理流程就应该是在这里面了,跟进可以看到RequestMappingHandlerAdapter#handleInternal方法中,

    mav = invokeHandlerMethod(request, response, handlerMethod);
    

    从方法名就可以看出是进行Controller中的Method调用的,继续跟进去,会发现RequestMappingHandlerAdapter有一个成员变量是argumentResolvers,那么从名称来看,很大概率就是我想要找到的参数解析器。
    这个argumentResolvers是个HandlerMethodArgumentResolverComposite类的实例,进入这个类中,就有一个HandlerMethodArgumentResolver数组,里面就是所有的参数解析器了。
    继续在这个类中往下看看,会发现这么两个方法:

            @Override
    	@Nullable
    	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
    			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    
    		HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
    		if (resolver == null) {
    			throw new IllegalArgumentException("Unknown parameter type [" + parameter.getParameterType().getName() + "]");
    		}
    		return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    	}
    
    	/**
    	 * Find a registered {@link HandlerMethodArgumentResolver} that supports the given method parameter.
    	 */
    	@Nullable
    	private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    		HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
    		if (result == null) {
    			for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
    				if (logger.isTraceEnabled()) {
    					logger.trace("Testing if argument resolver [" + methodArgumentResolver + "] supports [" +
    							parameter.getGenericParameterType() + "]");
    				}
    				if (methodArgumentResolver.supportsParameter(parameter)) {
    					result = methodArgumentResolver;
    					this.argumentResolverCache.put(parameter, result);
    					break;
    				}
    			}
    		}
    		return result;
    	}
    

    先不去追究细节,这两个方法所表达出来的意思很明了。先根据Controller中的Method中的参数,来获取到对应的解析器,也就是HandlerMethodArgumentResolver的一个实例。然后用这个解析器来解析参数。
    其中,获取解析器的时候,会先从缓存中获取,如果缓存中没有,那么就遍历所有的解析器,找到一款能够解析这个参数的解析器,并存入缓存中。
    对于@RequestBody的解析器,可以先看看HandlerMethodArgumentResolver有哪些实现类,发现有很多种解析器,包括我们常用的一些@RequestParam,@RequestHeader等等

    其中想要去看到就是框红的那个。进入这个类中,找到resolveArgument方法:

          /**
    	 * Throws MethodArgumentNotValidException if validation fails.
    	 * @throws HttpMessageNotReadableException if {@link RequestBody#required()}
    	 * is {@code true} and there is no body content or if there is no suitable
    	 * converter to read the content with.
    	 */
    	@Override
    	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
    			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    
    		parameter = parameter.nestedIfOptional();
    		Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
    		String name = Conventions.getVariableNameForParameter(parameter);
    
    		if (binderFactory != null) {
    			WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
    			if (arg != null) {
    				validateIfApplicable(binder, parameter);
    				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
    					throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
    				}
    			}
    			if (mavContainer != null) {
    				mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
    			}
    		}
    
    		return adaptArgumentIfNecessary(arg, parameter);
    

    这个方法也是有两步,先readWithMessageConverters解析参数,后面就是对这个参数进行校验,比如你使用了require=true,或者@Valid/@Validate。
    接着readWithMessageConverters往里走,来到了AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters中,其中对于我想要了解的东西,最关键的一行代码就是

    message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
    

    在这个构造函数里面可以看到

    		public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
    			this.headers = inputMessage.getHeaders();
    			InputStream inputStream = inputMessage.getBody();
    			if (inputStream.markSupported()) {
    				inputStream.mark(1);
    				this.body = (inputStream.read() != -1 ? inputStream : null);
    				inputStream.reset();
    			}
    			else {
    				PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
    				int b = pushbackInputStream.read();
    				if (b == -1) {
    					this.body = null;
    				}
    				else {
    					this.body = pushbackInputStream;
    					pushbackInputStream.unread(b);
    				}
    			}
    		}
    

    这里面就对输入流进行了包装处理,流程图:

    其中PushbackInputStream,就是增加了一个unread功能。read是往前读一个字节,而unread就是往后读一个自己。

    三、结论

    1. request.getParameter()不会对@RequestBody的解析造成影响,因为这完全是两种获取参数的方式,两个赛道。对于POST请求而言,getParameter是解析application/x-www-form-urlencoded类型的参数,而@RequestBody是解析application/json类型的参数

    2. 一般情况下,假如你在过滤器或任何@RequestBody解析之前的地方,读完了请求流,那么@RequestBody是获取不到参数内容的。

    3. 因此对于需要可重复读的请求流,一般网上也给了方案,对Request进行一层包装,且要覆写其中的getInputStream方法,这样才能随便通过getInputStream来读请求流。

  • 相关阅读:
    使用JavaWeb实现文件的上传和下载
    IDEA使用Maven创建Web项目的两种方式
    Window10系统下联想笔记本进入BIOS界面方法
    MarkDown语法
    PV-UV-QPS
    expect 安装 salt 客户端
    fg、bg、jobs、&、nohup、ctrl+z、ctrl+c 命令
    查看CPU 内存 硬盘 网络 查看进程使用的文件 uptime top ps -aux vmstat iostat iotop nload iptraf nethogs
    rpm 命令使用 和 lsof -p 1406 使用
    salt 执行shell 脚本 修改名字
  • 原文地址:https://www.cnblogs.com/bencakes/p/14524887.html
Copyright © 2020-2023  润新知