zoukankan      html  css  js  c++  java
  • 学习@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来读请求流。

  • 相关阅读:
    jQuery 基本选择器
    JavaScriptif while for switch流程控制 JS函数 内置对象
    JavaScrip基本语法
    数据库 存储引擎 表的操作 数值类型 时间类型 字符串类型 枚举集合 约束
    数据库基础知识 管理员 用户登录授权的操作
    粘包的产生原理 以及如何解决粘包问题
    socket TCP DPT 网络编程
    2018年年终总结
    Android技术分享
    No accelerator found
  • 原文地址:https://www.cnblogs.com/bencakes/p/14524887.html
Copyright © 2011-2022 走看看