1. 程式人生 > 其它 >SpringBoot解析HTTP引數分析

SpringBoot解析HTTP引數分析

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

一、HTTP請求處理流程

在SpringBoot中,一個HTTP請求會被DispatcherServlet類接收,它本質是一個Servlet,因為它繼承自HttpServlet。在這裡,Spring負責解析請求,匹配到Controller類上的方法,解析引數並執行方法,最後處理返回值並渲染檢視。 我們今天的重點在於解析引數,對應到上圖的目標方法呼叫這一步驟。既然說到引數解析,那麼針對不同型別的引數,肯定有不同的解析器。Spring已經幫我們註冊了一堆這東西。 它們有一個共同的介面HandlerMethodArgumentResolver。supportsParameter用來判斷方法引數是否可以被當前解析器解析,如果可以就呼叫resolveArgument去解析。
public
interface HandlerMethodArgumentResolver { //判斷方法引數是否可以被當前解析器解析 boolean supportsParameter(MethodParameter var1); //解析引數 @Nullable Object resolveArgument(MethodParameter var1, @Nullable ModelAndViewContainer var2, NativeWebRequest var3, @Nullable WebDataBinderFactory var4)
throws Exception; }

二、RequestParam

在Controller方法中,如果你的引數標註了RequestParam註解,或者是一個簡單資料型別。
@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");
在Spring中,這裡對應的引數解析器是RequestParamMethodArgumentResolver。與我們的想法差不多,就是拿到引數名稱後,直接從Request中獲取值。
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;
}

三、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負責。整個過程我們分為三個步驟來看。

1、獲取請求輔助資訊

在開始之前需要先獲取請求的一些輔助資訊,比如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; 

    //.......
}

2、確定訊息轉換器

上面獲取到的輔助資訊是有作用的,就是要確定一個訊息轉換器。訊息轉換器有很多,它們的共同介面是HttpMessageConverter。在這裡,Spring幫我們註冊了很多轉換器,所以需要迴圈它們,來確定使用哪一個來做訊息轉換。 如果是JSON資料格式的,會選擇MappingJackson2HttpMessageConverter來處理。它的建構函式正是指明瞭這一點。
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
    super(objectMapper, new MediaType[]{
        MediaType.APPLICATION_JSON, 
        new MediaType("application", "*+json")});
}

3、解析

既然確定了訊息轉換器,那麼剩下的事就很簡單了。通過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
都找到之後呼叫方法解析即可。

四、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()方法。整個過程,我們也可以分為三個步驟來看。

1、獲取目標類的建構函式

根據引數型別,先生成一個目標類的建構函式,以供後面繫結資料的時候使用。

2、建立資料繫結器WebDataBinder

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

3、繫結資料到目標類,並返回

在這裡,又把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));

五、自定義引數解析器

我們說所有的訊息解析器都實現了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";
}

六、總結

本文內容通過相關示例程式碼展示了Spring中部分解析器解析引數的過程。說到底,無論引數如何變化,引數型別再怎麼複雜。它們都是通過HTTP請求傳送過來的,那麼就可以通過HttpServletRequest來獲取到一切。Spring做的就是通過註解,儘量適配大部分應用場景。