SpringBoot是如何解析參數的

前言

前幾天筆者在寫Rest接口的時候,看到了一種傳值方式是之前沒有寫過的,就萌生了一探究竟的想法。在此以前,有篇文章曾涉及到這個話題,但那篇文章着重於處理流程的分析,並未深刻。前端

本文重點來看幾種傳參方式,看看它們都是如何被解析並應用到方法參數上的。web

1、HTTP請求處理流程

不論在SpringBoot仍是SpringMVC中,一個HTTP請求會被DispatcherServlet類接收,它本質是一個Servlet,由於它繼承自HttpServlet。在這裏,Spring負責解析請求,匹配到Controller類上的方法,解析參數並執行方法,最後處理返回值並渲染視圖。json

咱們今天的重點在於解析參數,對應到上圖的目標方法調用這一步驟。既然說到參數解析,那麼針對不一樣類型的參數,確定有不一樣的解析器。Spring已經幫咱們註冊了一堆這東西。bash

它們有一個共同的接口HandlerMethodArgumentResolversupportsParameter用來判斷方法參數是否能夠被當前解析器解析,若是能夠就調用resolveArgument去解析。app

public interface HandlerMethodArgumentResolver {
    //判斷方法參數是否能夠被當前解析器解析
    boolean supportsParameter(MethodParameter var1);
    //解析參數
    @Nullable
    Object resolveArgument(MethodParameter var1, 
			@Nullable ModelAndViewContainer var2, 
			NativeWebRequest var3, 
			@Nullable WebDataBinderFactory var4)throws Exception;
}
複製代碼

2、RequestParam

在Controller方法中,若是你的參數標註了RequestParam註解,或者是一個簡單數據類型。ide

@RequestMapping("/test1")
@ResponseBody
public String test1(String t1, @RequestParam(name = "t2",required = false) String t2,HttpServletRequest request){
	logger.info("參數:{},{}",t1,t2);
	return "Java";
}
複製代碼

咱們的請求路徑是這樣的:http://localhost:8080/test1?t1=Jack&t2=Java函數

若是按照之前的寫法,咱們直接根據參數名稱或者RequestParam註解的名稱從Request對象中獲取值就行。好比像這樣:工具

String parameter = request.getParameter("t1");ui

在Spring中,這裏對應的參數解析器是RequestParamMethodArgumentResolver。與咱們的想法差很少,就是拿到參數名稱後,直接從Request中獲取值。this

protected Object resolveName(String name, MethodParameter parameter, 
		NativeWebRequest request) throws Exception {
		
	HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
	//...省略部分代碼...
	if (arg == null) {
		String[] paramValues = request.getParameterValues(name);
		if (paramValues != null) {
			arg = paramValues.length == 1 ? paramValues[0] : paramValues;
		}
	}
	return arg;
}
複製代碼

3、RequestBody

若是咱們須要前端傳輸更多的參數內容,那麼經過一個POST請求,將參數放在Body中傳輸是更好的方式。固然,比較友好的數據格式當屬JSON。

面對這樣一個請求,咱們在Controller方法中能夠經過RequestBody註解來接收它,並自動轉換爲合適的Java Bean對象。

@ResponseBody
@RequestMapping("/test2")
public String test2(@RequestBody SysUser user){
    logger.info("參數信息:{}",JSONObject.toJSONString(user));
    return "Hello";
}
複製代碼

在沒有Spring的狀況下,咱們考慮一下如何解決這一問題呢?

首先呢,仍是要依靠Request對象。對於Body中的數據,咱們能夠經過request.getReader()方法來獲取,而後讀取字符串,最後經過JSON工具類再轉換爲合適的Java對象。

好比像下面這樣:

@RequestMapping("/test2")
@ResponseBody
public String test2(HttpServletRequest request) throws IOException {
    BufferedReader reader = request.getReader();
    StringBuilder builder = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null){
    	builder.append(line);
    }
    logger.info("Body數據:{}",builder.toString());
    SysUser sysUser = JSONObject.parseObject(builder.toString(), SysUser.class);
    logger.info("轉換後的Bean:{}",JSONObject.toJSONString(sysUser));
    return "Java";
}
複製代碼

固然,在實際場景中,上面的SysUser.class須要動態獲取參數類型。

在Spring中,RequestBody註解的參數會由RequestResponseBodyMethodProcessor類來負責解析。

它的解析由父類AbstractMessageConverterMethodArgumentResolver負責。整個過程咱們分爲三個步驟來看。

一、獲取請求輔助信息

在開始以前須要先獲取請求的一些輔助信息,好比HTTP請求的數據格式,上下文Class信息、參數類型Class、HTTP請求方法類型等。

protected <T> Object readWithMessageConverters(){
				   
	boolean noContentType = false;
	MediaType contentType;
	try {
		contentType = inputMessage.getHeaders().getContentType();
	} catch (InvalidMediaTypeException var16) {
		throw new HttpMediaTypeNotSupportedException(var16.getMessage());
	}
	if (contentType == null) {
		noContentType = true;
		contentType = MediaType.APPLICATION_OCTET_STREAM;
	}
	Class<?> contextClass = parameter.getContainingClass();
	Class<T> targetClass = targetType instanceof Class ? (Class)targetType : null;
	if (targetClass == null) {
		ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
		targetClass = resolvableType.resolve();
	}
	HttpMethod httpMethod = inputMessage instanceof HttpRequest ?
	        ((HttpRequest)inputMessage).getMethod() : null;	

	//.......
}
複製代碼

二、肯定消息轉換器

上面獲取到的輔助信息是有做用的,就是要肯定一個消息轉換器。消息轉換器有不少,它們的共同接口是HttpMessageConverter。在這裏,Spring幫咱們註冊了不少轉換器,因此須要循環它們,來肯定使用哪個來作消息轉換。

若是是JSON數據格式的,會選擇MappingJackson2HttpMessageConverter來處理。它的構造函數正是指明瞭這一點。

public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
	super(objectMapper, new MediaType[]{
		MediaType.APPLICATION_JSON, 
		new MediaType("application", "*+json")});
}
複製代碼

三、解析

既然肯定了消息轉換器,那麼剩下的事就很簡單了。經過Request獲取Body,而後調用轉換器解析就行了。

protected <T> Object readWithMessageConverters(){
    if (message.hasBody()) {
	  HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
	  body = genericConverter.read(targetType, contextClass, msgToUse);
	  body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
    }
}
複製代碼

再往下就是Jackson包的內容了,再也不深究。雖然寫出來的過程比較囉嗦,但實際上主要就是爲了尋找兩個東西:

方法解析器RequestResponseBodyMethodProcessor

消息轉換器MappingJackson2HttpMessageConverter

都找到以後調用方法解析便可。

4、GET請求參數轉換Bean

還有一種寫法是這樣的,在Controller方法上用Java Bean接收。

@RequestMapping("/test3")
@ResponseBody
public String test3(SysUser user){
    logger.info("參數:{}",JSONObject.toJSONString(user));
    return "Java";
}
複製代碼

而後用GET方法請求:

http://localhost:8080/test3?id=1001&name=Jack&password=1234&address=北京市海淀區

URL後面的參數名稱對應Bean對象裏面的屬性名稱,也能夠自動轉換。那麼,這裏它又是怎麼作的呢 ?

筆者首先想到的就是Java的反射機制。從Request對象中獲取參數名稱,而後和目標類上的方法一一對應設置值進去。

好比像下面這樣:

public String test3(SysUser user,HttpServletRequest request)throws Exception {
	//從Request中獲取全部的參數key 和 value
	Map<String, String[]> parameterMap = request.getParameterMap();
	Iterator<Map.Entry<String, String[]>> iterator = parameterMap.entrySet().iterator();
	//獲取目標類的對象
	Object target = user.getClass().newInstance();
	Field[] fields = target.getClass().getDeclaredFields();
	while (iterator.hasNext()){
		Map.Entry<String, String[]> next = iterator.next();
		String key = next.getKey();
		String value = next.getValue()[0];
		for (Field field:fields){
			String name = field.getName();
			if (key.equals(name)){
				field.setAccessible(true);
				field.set(target,value);
				break;
			}
		}
	}
	logger.info("userInfo:{}",JSONObject.toJSONString(target));
	return "Python";
}
複製代碼

除了反射,Java還有一種內省機制能夠完成這件事。咱們能夠獲取目標類的屬性描述符對象,而後拿到它的Method對象, 經過invoke來設置。

private void setProperty(Object target,String key,String value) {
    try {
	    PropertyDescriptor propDesc = new PropertyDescriptor(key, target.getClass());
	    Method method = propDesc.getWriteMethod();
	    method.invoke(target, value);
    } catch (Exception e) {
	    e.printStackTrace();
    }
}

複製代碼

而後在上面的循環中,咱們就能夠調用這個方法來實現。

while (iterator.hasNext()){
	Map.Entry<String, String[]> next = iterator.next();
	String key = next.getKey();
	String value = next.getValue()[0];
	setProperty(userInfo,key,value);
}
複製代碼

爲何要說到內省機制呢?由於Spring在處理這件事的時候,最終也是靠它處理的。

簡單來講,它是經過BeanWrapperImpl來處理的。關於BeanWrapperImpl有個很簡單的使用方法:

SysUser user = new SysUser();
BeanWrapper wrapper = new BeanWrapperImpl(user.getClass());

wrapper.setPropertyValue("id","20001");
wrapper.setPropertyValue("name","Jack");

Object instance = wrapper.getWrappedInstance();
System.out.println(instance);
複製代碼

wrapper.setPropertyValue最後就會調用到BeanWrapperImpl#BeanPropertyHandler.setValue()方法。

它的setValue方法和咱們上面的setProperty方法大體相同。

private class BeanPropertyHandler extends PropertyHandler {
    //屬性描述符
    private final PropertyDescriptor pd;
    public void setValue(@Nullable Object value) throws Exception {
    	//獲取set方法
    	Method writeMethod = this.pd.getWriteMethod();
    	ReflectionUtils.makeAccessible(writeMethod);
    	//設置
    	writeMethod.invoke(BeanWrapperImpl.this.getWrappedInstance(), value);
    }
}
複製代碼

經過上面的方式,就完成了GET請求參數到Java Bean對象的自動轉換。

回過頭來,咱們再看Spring。雖然咱們上面寫的很簡單,但真正用起來還須要考慮的不少不少。Spring中處理這種參數的解析器是ServletModelAttributeMethodProcessor

它的解析過程在其父類ModelAttributeMethodProcessor.resolveArgument()方法。整個過程,咱們也能夠分爲三個步驟來看。

一、獲取目標類的構造函數

根據參數類型,先生成一個目標類的構造函數,以供後面綁定數據的時候使用。

二、建立數據綁定器WebDataBinder

WebDataBinder繼承自DataBinder。而DataBinder主要的做用,簡言之就是利用BeanWrapper給對象的屬性設值。

三、綁定數據到目標類,並返回

在這裏,又把WebDataBinder轉換成ServletRequestDataBinder對象,而後調用它的bind方法。

接下來有個很重要的步驟是,將request中的參數轉換爲MutablePropertyValues pvs對象。

而後接下來就是循環pvs,調用setPropertyValue設置屬性。固然了,最後調用的其實就是BeanWrapperImpl#BeanPropertyHandler.setValue()

下面有段代碼能夠更好的理解這一過程,效果是同樣的:

//模擬Request參數
Map<String,Object> map = new HashMap();
map.put("id","1001");
map.put("name","Jack");
map.put("password","123456");
map.put("address","北京市海淀區");

//將request對象轉換爲MutablePropertyValues對象
MutablePropertyValues propertyValues = new MutablePropertyValues(map);
SysUser sysUser = new SysUser();
//建立數據綁定器
ServletRequestDataBinder binder = new ServletRequestDataBinder(sysUser);
//bind數據
binder.bind(propertyValues);
System.out.println(JSONObject.toJSONString(sysUser));
複製代碼

5、自定義參數解析器

咱們說全部的消息解析器都實現了HandlerMethodArgumentResolver接口。咱們也能夠定義一個參數解析器,讓它實現這個接口就行了。

首先,咱們能夠定義一個RequestXuner註解。

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestXuner {
    String name() default "";
    boolean required() default false;
    String defaultValue() default "default";
}
複製代碼

而後是實現了HandlerMethodArgumentResolver接口的解析器類。

public class XunerArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestXuner.class);
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter,
                                  ModelAndViewContainer modelAndViewContainer,
                                  NativeWebRequest nativeWebRequest,
                                  WebDataBinderFactory webDataBinderFactory){
	
		//獲取參數上的註解
        RequestXuner annotation = methodParameter.getParameterAnnotation(RequestXuner.class);
        String name = annotation.name();
		//從Request中獲取參數值
        String parameter = nativeWebRequest.getParameter(name);
        return "HaHa,"+parameter;
    }
}
複製代碼

不要忘記須要配置一下。

@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
    @Override
    protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new XunerArgumentResolver());
    }
}
複製代碼

一頓操做後,在Controller中咱們能夠這樣使用它:

@ResponseBody
@RequestMapping("/test4")
public String test4(@RequestXuner(name="xuner") String xuner){
    logger.info("參數:{}",xuner);
    return "Test4";
}
複製代碼

6、總結

本文內容經過相關示例代碼展現了Spring中部分解析器解析參數的過程。說到底,不管參數如何變化,參數類型再怎麼複雜。

它們都是經過HTTP請求發送過來的,那麼就能夠經過HttpServletRequest來獲取到一切。Spring作的就是經過註解,儘可能適配大部分應用場景。

相關文章
相關標籤/搜索