精盡Spring MVC原始碼分析 - HandlerAdapter 元件(三)之 HandlerMethodArgumentResolver

月圓吖發表於2020-12-18

該系列文件是本人在學習 Spring MVC 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋 Spring MVC 原始碼分析 GitHub 地址 進行閱讀

Spring 版本:5.2.4.RELEASE

該系列其他文件請檢視:《精盡 Spring MVC 原始碼分析 - 文章導讀》

HandlerAdapter 元件

HandlerAdapter 元件,處理器的介面卡。因為處理器 handler 的型別是 Object 型別,需要有一個呼叫者來實現 handler 是怎麼被執行。Spring 中的處理器的實現多變,比如使用者的處理器可以實現 Controller 介面或者 HttpRequestHandler 介面,也可以用 @RequestMapping 註解將方法作為一個處理器等,這就導致 Spring MVC 無法直接執行這個處理器。所以這裡需要一個處理器介面卡,由它去執行處理器

由於 HandlerMapping 元件涉及到的內容較多,考慮到內容的排版,所以將這部分內容拆分成了五個模組,依次進行分析:

HandlerAdapter 元件(三)之 HandlerMethodArgumentResolver

本文是接著《HandlerAdapter 元件(二)之 ServletInvocableHandlerMethod》一文來分享 HandlerMethodArgumentResolver 元件。在 HandlerAdapter 執行處理器的過程中,具體的執行過程交由 ServletInvocableHandlerMethod 物件來完成,其中需要先通過 HandlerMethodArgumentResolver 引數解析器從請求中解析出方法的入參,然後再通過反射機制呼叫對應的方法。

回顧

先來回顧一下 ServletInvocableHandlerMethod 在哪裡呼叫引數解析器的,可以回到 《HandlerAdapter 元件(二)之 ServletInvocableHandlerMethod》InvocableHandlerMethod 小節下面的 getMethodArgumentValues 方法,如下:

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {
    // 獲得方法的引數
    MethodParameter[] parameters = getMethodParameters();
    // 無參,返回空陣列
    if (ObjectUtils.isEmpty(parameters)) {
        return EMPTY_ARGS;
    }
    // 將引數解析成對應的型別
    Object[] args = new Object[parameters.length];
    for (int i = 0; i < parameters.length; i++) {
        // 獲得當前遍歷的 MethodParameter 物件,並設定 parameterNameDiscoverer 到其中
        MethodParameter parameter = parameters[i];
        parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
        // <1> 先從 providedArgs 中獲得引數。如果獲得到,則進入下一個引數的解析,預設情況 providedArgs 不會傳參
        args[i] = findProvidedArgument(parameter, providedArgs);
        if (args[i] != null) {
            continue;
        }
         // <2> 判斷 resolvers 是否支援當前的引數解析
        if (!this.resolvers.supportsParameter(parameter)) {
            throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
        }
        try {
            // 執行解析,解析成功後,則進入下一個引數的解析
            args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
        }
        catch (Exception ex) {
            // Leave stack trace for later, exception may actually be resolved and handled...
            if (logger.isDebugEnabled()) {
                String exMsg = ex.getMessage();
                if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
                    logger.debug(formatArgumentError(parameter, exMsg));
                }
            }
            throw ex;
        }
    }
    return args;
}
  • <2> 處,在獲取到 Method 方法的所有引數物件,依次處理,根據 resolvers 判斷是否支援該引數的處理,如果支援則進行引數轉換

  • resolvers 為 HandlerMethodArgumentResolverComposite 組合物件,包含了許多的引數解析器

HandlerMethodArgumentResolver 介面

org.springframework.web.method.support.HandlerMethodArgumentResolver,方法引數解析器

public interface HandlerMethodArgumentResolver {
	/**
	 * 是否支援解析該引數
	 */
	boolean supportsParameter(MethodParameter parameter);
	/**
	 * 解析該引數
	 */
	@Nullable
	Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

類圖

精盡Spring MVC原始碼分析 - HandlerAdapter 元件(三)之 HandlerMethodArgumentResolver

因為請求入參的場景非常多,所以 HandlerMethodArgumentResolver 的實現類也非常多,上面僅列出了部分實現類,本文也分析上面圖中右側常見的幾種引數場景

HandlerMethodArgumentResolverComposite

org.springframework.web.method.support.HandlerMethodArgumentResolverComposite,實現 HandlerMethodArgumentResolver 介面,複合的 HandlerMethodArgumentResolver 實現類

構造方法

public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
	/**
	 * HandlerMethodArgumentResolver 陣列
	 */
	private final List<HandlerMethodArgumentResolver> argumentResolvers = new LinkedList<>();
	/**
	 * MethodParameter 與 HandlerMethodArgumentResolver 的對映,作為快取
	 */
	private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache = new ConcurrentHashMap<>(256);
}
  • argumentResolvers:HandlerMethodArgumentResolver 陣列。這就是 Composite 複合~
  • argumentResolverCache:MethodParameter 與 HandlerMethodArgumentResolver 的對映,作為快取。因為,MethodParameter 是需要從 argumentResolvers 遍歷到適合其的解析器,通過快取後,無需再次重複遍歷

《HandlerAdapter 元件(一)之 HandlerAdapter》RequestMappingHandlerAdapter小節的 getDefaultArgumentResolvers 方法中可以看到,預設的 argumentResolvers 有哪些 HandlerMethodArgumentResolver 實現類,注意這裡是有順序的新增哦

getArgumentResolver

getArgumentResolver(MethodParameter parameter) 方法,獲得方法引數對應的 HandlerMethodArgumentResolver 物件,方法如下:

@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    // 優先從 argumentResolverCache 快取中,獲得 parameter 對應的 HandlerMethodArgumentResolver 物件
    HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
    if (result == null) {
        // 獲得不到,則遍歷 argumentResolvers 陣列,逐個判斷是否支援。
        for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
            // 如果支援,則新增到 argumentResolverCache 快取中,並返回
            if (resolver.supportsParameter(parameter)) {
                result = resolver;
                this.argumentResolverCache.put(parameter, result);
                break;
            }
        }
    }
    return result;
}

很簡單,先從argumentResolverCache快取中獲取,沒有獲取到則遍歷 argumentResolvers,如果支援該引數則該 HandlerMethodArgumentResolver 物件並快取起來

注意,往 argumentResolvers 新增的順序靠前,則優先判斷是否支援該引數哦~

supportsParameter

實現 supportsParameter(MethodParameter parameter) 方法,如果能獲得到對應的 HandlerMethodArgumentResolver 引數處理器,則說明支援處理該引數,方法如下:

@Override
public boolean supportsParameter(MethodParameter parameter) {
    return getArgumentResolver(parameter) != null;
}

resolveArgument

實現 resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) 方法,解析出指定引數的值,方法如下:

@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("Unsupported parameter type [" +
                parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
    }
    /**
     * 進行解析
     *
     * 基於 @RequestParam 註解
     * {@link org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveArgument}
     * 基於 @PathVariable 註解
     * {@link org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver#resolveArgument}
     */
    return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}

很簡單,獲取到該方法引數對應的 HandlerMethodArgumentResolver 引數處理器,然後呼叫其 resolveArgument 執行解析

AbstractNamedValueMethodArgumentResolver

org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver,實現 ValueMethodArgumentResolver 介面,基於名字獲取值的HandlerMethodArgumentResolver 抽象基類。例如說,@RequestParam(value = "username") 註解的引數,就是從請求中獲得 username 對應的引數值。? 明白了麼?

AbstractNamedValueMethodArgumentResolver 的子類也有挺多了,我們僅分析它的兩個子類,如上面類圖的下面兩個:

  • RequestParamMethodArgumentResolver:基於 @RequestParam 註解( 也可不加該註解的請求引數 )的方法引數,詳情見下文
  • PathVariableMethodArgumentResolver ,基於 @PathVariable 註解的方法引數,詳情見下文

構造方法

public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver {

	@Nullable
	private final ConfigurableBeanFactory configurableBeanFactory;

	@Nullable
	private final BeanExpressionContext expressionContext;
	/**
	 * MethodParameter 和 NamedValueInfo 的對映,作為快取
	 */
	private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256);
}

NamedValueInfo 內部類

AbstractNamedValueMethodArgumentResolver 的靜態內部類,程式碼如下:

protected static class NamedValueInfo {
    /**
     * 名字
     */
    private final String name;

    /**
     * 是否必填
     */
    private final boolean required;

    /**
     * 預設值
     */
    @Nullable
    private final String defaultValue;

    public NamedValueInfo(String name, boolean required, @Nullable String defaultValue) {
        this.name = name;
        this.required = required;
        this.defaultValue = defaultValue;
    }
}

getNamedValueInfo

private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
    // <1> 從 namedValueInfoCache 快取中,獲得 NamedValueInfo 物件
    NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
    if (namedValueInfo == null) {
        // <2> 獲得不到,則建立 namedValueInfo 物件。這是一個抽象方法,子類來實現
        namedValueInfo = createNamedValueInfo(parameter);
         // <3> 更新 namedValueInfo 物件
        namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
        // <4> 新增到 namedValueInfoCache 快取中
        this.namedValueInfoCache.put(parameter, namedValueInfo);
    }
    return namedValueInfo;
}
  1. namedValueInfoCache 快取中,獲得 NamedValueInfo 物件,獲取到則直接返回

  2. 獲得不到,則呼叫 createNamedValueInfo(MethodParameter parameter) 方法,建立 NamedValueInfo 物件。這是一個抽象方法,交由子類來實現

  3. 呼叫 updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) 方法,更新 NamedValueInfo 物件,方法如下:

    private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) {
        String name = info.name;
        if (info.name.isEmpty()) {
            // 【注意!!!】如果 name 為空,則使用引數名
            name = parameter.getParameterName();
            if (name == null) {
                throw new IllegalArgumentException(
                        "Name for argument type [" + parameter.getNestedParameterType().getName() +
                        "] not available, and parameter name information not found in class file either.");
            }
        }
        // 獲得預設值
        String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
        // 建立 NamedValueInfo 物件
        return new NamedValueInfo(name, info.required, defaultValue);
    }
    

    如果名稱為空,則取引數名,獲取預設值,建立一個新的 NamedValueInfo 物件返回

  4. 新增到 namedValueInfoCache 快取中

  5. 返回該 NamedValueInfo 物件

resolveArgument

resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) 方法,從請求中解析出指定引數的值

@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

    // <1> 獲得方法引數對應的 NamedValueInfo 物件。
    NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
    // <2> 如果 parameter 是內嵌型別(Optional 型別)的,則獲取內嵌的引數。否則,還是使用 parameter 自身
    MethodParameter nestedParameter = parameter.nestedIfOptional();

    // <3> 如果 name 是佔位符,則進行解析成對應的值
    Object resolvedName = resolveStringValue(namedValueInfo.name);
    if (resolvedName == null) {
        // 如果解析不到,則丟擲 IllegalArgumentException 異常
        throw new IllegalArgumentException(
                "Specified name must not resolve to null: [" + namedValueInfo.name + "]");
    }

    // <4> 解析 name 對應的值
    Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
    // <5> 如果 arg 不存在,則使用預設值
    if (arg == null) {
        // <5.1> 使用預設值
        if (namedValueInfo.defaultValue != null) {
            arg = resolveStringValue(namedValueInfo.defaultValue);
        }
        // <5.2> 如果是必填,則處理引數缺失的情況
        else if (namedValueInfo.required && !nestedParameter.isOptional()) {
            handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
        }
        // <5.3> 處理空值的情況
        arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
    }
    // <6> 如果 arg 為空串,則使用預設值
    else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
        arg = resolveStringValue(namedValueInfo.defaultValue);
    }

    // <7> 資料繫結相關
    if (binderFactory != null) {
        WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
        try {
            arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
        }
        catch (ConversionNotSupportedException ex) {
            throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
                    namedValueInfo.name, parameter, ex.getCause());
        }
        catch (TypeMismatchException ex) {
            throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
                    namedValueInfo.name, parameter, ex.getCause());

        }
    }

    // <8> 處理解析的值
    handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);

    return arg;
}
  1. 呼叫 getNamedValueInfo(MethodParameter parameter) 方法,獲得方法引數對應的 NamedValueInfo 物件

  2. 如果 parameter 是內嵌型別(Optional 型別)的,則獲取內嵌的引數。否則,還是使用 parameter 自身。一般情況下,parameter 引數,我們不太會使用 Optional 型別。可以暫時忽略

  3. 呼叫 resolveStringValue(String value) 方法,如果 name 是佔位符,則進行解析成對應的值,方法如下:

    @Nullable
    private Object resolveStringValue(String value) {
        // 如果 configurableBeanFactory 為空,則不進行解析
        if (this.configurableBeanFactory == null) {
            return value;
        }
        // 獲得佔位符對應的值
        String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value);
        // 獲取表示式處理器物件
        BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver();
        if (exprResolver == null || this.expressionContext == null) {
            return value;
        }
        // 計算表示式
        return exprResolver.evaluate(placeholdersResolved, this.expressionContext);
    }
    

    這種用法非常小眾,從來沒用過。示例如下:

    // Controller.java
    
    @RequestMapping("/hello3")
    public String hello3(@RequestParam(value = "${server.port}") String name) {
        return "666";
    }
    
    // application.properties
    server.port=8012
    

    此時,就可以傳送 GET /hello3?8012=xxx 請求

  4. 【重點】呼叫 resolveName(String name, MethodParameter parameter, NativeWebRequest request) 抽象方法,解析引數名 name 對應的值,交由子類去實現

  5. 如果上面解析出來的引數值 argnull ,則使用預設值

    1. 如果預設值非空,則呼叫 resolveStringValue(defaultValue) 方法,解析預設值

    2. 如果是必填,則呼叫 handleMissingValue(handleMissingValue) 方法,處理引數缺失的情況呼叫,也就是丟擲指定的異常

    3. 呼叫 handleNullValue(String name, Object value, Class<?> paramType) 方法,處理 null 值的情況,方法如下:

      @Nullable
      private Object handleNullValue(String name, @Nullable Object value, Class<?> paramType) {
          if (value == null) {
              if (Boolean.TYPE.equals(paramType)) {
                  return Boolean.FALSE;
              } else if (paramType.isPrimitive()) { // 如果是基本型別則不能為 null
                  throw new IllegalStateException("Optional " + paramType.getSimpleName() + " parameter '" + name +
                          "' is present but cannot be translated into a null value due to being declared as a " +
                          "primitive type. Consider declaring it as object wrapper for the corresponding primitive type.");
              }
          }
          return value;
      }
      
  6. 否則,如果 arg為空字串,並且存在預設值,則和上面的 5.1 相同處理方式

  7. 資料繫結相關,暫時忽略

  8. 呼叫 handleResolvedValue(Object arg, String name, MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) 方法,解析引數值的後置處理,空方法,子類可以覆蓋,子類 PathVariableMethodArgumentResolver 會重寫該方法

程式碼有點長,不過邏輯不難理解

RequestParamMethodArgumentResolver

org.springframework.web.method.annotation.RequestParamMethodArgumentResolver,實現 UriComponentsContributor 介面,繼承 AbstractNamedValueMethodArgumentResolver 抽象類,引數解析器 HandlerMethodArgumentResolver 的實現類,處理普通的請求引數

構造方法

public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver
		implements UriComponentsContributor {
	private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
	/**
	 * 是否使用預設解決
	 *
	 * 這個變數有點繞,見 {@link #supportsParameter(MethodParameter)} 方法
	 */
	private final boolean useDefaultResolution;

	public RequestParamMethodArgumentResolver(boolean useDefaultResolution) {
		this.useDefaultResolution = useDefaultResolution;
	}

	public RequestParamMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory,
			boolean useDefaultResolution) {
		super(beanFactory);
		this.useDefaultResolution = useDefaultResolution;
	}
}

supportsParameter

實現 supportsParameter(MethodParameter parameter) 方法,判斷是否支援處理該方法入參,方法如下:

@Override
public boolean supportsParameter(MethodParameter parameter) {
    // <3> 有 @RequestParam 註解的情況
    if (parameter.hasParameterAnnotation(RequestParam.class)) {
        // <3.1> 如果是 Map 型別,則 @RequestParam 註解必須要有 name 屬性
        if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
            RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
            return (requestParam != null && StringUtils.hasText(requestParam.name()));
        }
        else {
            // <3.2> 否則返回 true
            return true;
        }
    }
    else {
        // 如果有 @RequestPart 註解,返回 false 。即 @RequestPart 的優先順序 > @RequestParam
        if (parameter.hasParameterAnnotation(RequestPart.class)) {
            return false;
        }
        // 獲得引數,如果存在內嵌的情況
        parameter = parameter.nestedIfOptional();
        // <1> 如果 Multipart 引數。則返回 true ,表示支援
        if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
            return true;
        }
        // <2> 如果開啟 useDefaultResolution 功能,則判斷是否為普通型別
        else if (this.useDefaultResolution) {
            return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
        }
        // 其它,不支援
        else {
            return false;
        }
    }
}
  1. 如果 Multipart 引數。則返回 true ,表示支援呼叫 MultipartResolutionDelegate#isMultipartArgument(parameter) 方法,如果 Multipart 引數。則返回 true ,表示支援。程式碼如下:

    public static boolean isMultipartArgument(MethodParameter parameter) {
        Class<?> paramType = parameter.getNestedParameterType();
        return (MultipartFile.class == paramType ||
                isMultipartFileCollection(parameter) || isMultipartFileArray(parameter) ||
                (Part.class == paramType || isPartCollection(parameter) || isPartArray(parameter)));
    }
    

    上傳檔案相關型別

  2. 如果開啟 useDefaultResolution 功能,則呼叫 BeanUtils#isSimpleProperty(Class<?> clazz) 方法,判斷是否為普通型別,程式碼如下:

    public static boolean isSimpleProperty(Class<?> type) {
        Assert.notNull(type, "'type' must not be null");
        return isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.getComponentType()));
    }
    public static boolean isSimpleValueType(Class<?> type) {
        return (type != void.class && type != Void.class &&
                (ClassUtils.isPrimitiveOrWrapper(type) ||
                Enum.class.isAssignableFrom(type) ||
                CharSequence.class.isAssignableFrom(type) ||
                Number.class.isAssignableFrom(type) ||
                Date.class.isAssignableFrom(type) ||
                URI.class == type ||
                URL.class == type ||
                Locale.class == type ||
                Class.class == type));
    }
    

    那麼 useDefaultResolution 到底是怎麼被賦值的呢?回到 RequestMappingHandlerAdapter 的 getDefaultArgumentResolvers() 的方法,精簡程式碼如下:

    private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
        List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
    
        // Annotation-based argument resolution
        resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
        
        // ... 省略許多 HandlerMethodArgumentResolver 的新增
        
        // Catch-all
        resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
        resolvers.add(new ServletModelAttributeMethodProcessor(true));
    
        return resolvers;
    }
    

    我們可以看到有兩個 RequestParamMethodArgumentResolver 物件,前者 useDefaultResolutionfalse ,後者為 useDefaultResolutiontrue 。什麼意思呢?優先將待有 @RequestParam 註解的請求引數給第一個 RequestParamMethodArgumentResolver 物件;其次,給中間省略的一大片引數解析器試試能不能解析;最後,使用第二個 RequestParamMethodArgumentResolver 兜底,處理剩餘的情況。

  3. 如果該方法引數有 @RequestParam 註解的情況

    1. 如果是 Map 型別,則 @RequestParam 註解必須要有 name 屬性,是不是感覺有幾分靈異?答案在下面的 RequestParamMapMethodArgumentResolver 中揭曉
    2. 否則,返回 true

createNamedValueInfo

實現父類的 createNamedValueInfo(MethodParameter parameter) 方法,建立 NamedValueInfo 物件,方法如下:

@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
    RequestParam ann = parameter.getParameterAnnotation(RequestParam.class);
    return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo());
}

private static class RequestParamNamedValueInfo extends NamedValueInfo {

    public RequestParamNamedValueInfo() {
        super("", false, ValueConstants.DEFAULT_NONE);
    }

    public RequestParamNamedValueInfo(RequestParam annotation) {
        super(annotation.name(), annotation.required(), annotation.defaultValue());
    }
}
  1. 如果方法引數有 @RequestParam 註解,則根據註解建立一個 RequestParamNamedValueInfo 物件,獲取註解中的 namerequired defaultValue配置

  2. 否則,就建立一個空的 RequestParamNamedValueInfo 物件,三個屬性分別為,空字串falseValueConstants.DEFAULT_NONE

    上面的 getNamedValueInfo 方法中講述到,name空字串 沒有關係,會獲取方法的引數名

    說明:通過反射獲取方法的引數名,我們只能獲取到 arg0,arg1 的名稱,因為jdk8之後這些變數名稱沒有被編譯到class檔案中,編譯時需要指定-parameters選項,方法的引數名才會記錄到class檔案中,執行時我們就可以通過反射機制獲取到,所以我們最好還是用 @RequestParam 註解來標註

    ValueConstants.DEFAULT_NONE 則會設定為 null

resolveName

實現 #resolveName(String name, MethodParameter parameter, NativeWebRequest request) 方法,獲得引數的值,方法如下:

@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
    // 情況一,HttpServletRequest 情況下的 MultipartFile 和 Part 的情況
    HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);

    if (servletRequest != null) {
        Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
        if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
            return mpArg;
        }
    }

    // 情況二,MultipartHttpServletRequest 情況下的 MultipartFile 的情況
    Object arg = null;
    MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
    if (multipartRequest != null) {
        List<MultipartFile> files = multipartRequest.getFiles(name);
        if (!files.isEmpty()) {
            arg = (files.size() == 1 ? files.get(0) : files);
        }
    }
    // 情況三,普通引數的獲取
    if (arg == null) {
        String[] paramValues = request.getParameterValues(name);
        if (paramValues != null) {
            arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
        }
    }
    return arg;
}
  • 情況一、二,是處理引數型別為檔案 org.springframework.web.multipart.MultipartFilejavax.servlet.http.Part 的引數的獲取,例如我們常用到 MultipartFile 作為引數就是在這裡處理的
  • 情況三,是處理普通引數的獲取。就是我們常見的 String、Integer 之類的請求引數,直接從請求中獲取引數值就好了

因為在《MultipartResolver 元件》中講過了會對請求進行處理,包括解析出引數,解析成對應的 HttpServletRequest 物件

獲得到引數值後,就可以準備開始通過反射呼叫對應的方法了

RequestParamMapMethodArgumentResolver

org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver,實現 HandlerMethodArgumentResolver 介面,用於處理帶有 @RequestParam 註解,但是註解上沒有 name 屬性的 Map 型別的引數, HandlerMethodArgumentResolver 的實現類,程式碼如下:

public class RequestParamMapMethodArgumentResolver implements HandlerMethodArgumentResolver {

	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
		return (requestParam != null && Map.class.isAssignableFrom(parameter.getParameterType()) &&
				!StringUtils.hasText(requestParam.name()));
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);

		// MultiValueMap 型別的處理
		if (MultiValueMap.class.isAssignableFrom(parameter.getParameterType())) {
			Class<?> valueType = resolvableType.as(MultiValueMap.class).getGeneric(1).resolve();
			if (valueType == MultipartFile.class) { // MultipartFile 型別
				MultipartRequest multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest);
				return (multipartRequest != null ? multipartRequest.getMultiFileMap() : new LinkedMultiValueMap<>(0));
			}
			else if (valueType == Part.class) { // Part 型別
				HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
				if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
					Collection<Part> parts = servletRequest.getParts();
					LinkedMultiValueMap<String, Part> result = new LinkedMultiValueMap<>(parts.size());
					for (Part part : parts) {
						result.add(part.getName(), part);
					}
					return result;
				}
				return new LinkedMultiValueMap<>(0);
			}
			else {
				Map<String, String[]> parameterMap = webRequest.getParameterMap();
				MultiValueMap<String, String> result = new LinkedMultiValueMap<>(parameterMap.size());
				parameterMap.forEach((key, values) -> {
					for (String value : values) {
						result.add(key, value);
					}
				});
				return result;
			}
		}
		// 普通 Map 型別的處理
		else {
			Class<?> valueType = resolvableType.asMap().getGeneric(1).resolve();
			if (valueType == MultipartFile.class) { // MultipartFile 型別
				MultipartRequest multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest);
				return (multipartRequest != null ? multipartRequest.getFileMap() : new LinkedHashMap<>(0));
			}
			else if (valueType == Part.class) { // Part 型別
				HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
				if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
					Collection<Part> parts = servletRequest.getParts();
					LinkedHashMap<String, Part> result = new LinkedHashMap<>(parts.size());
					for (Part part : parts) {
						if (!result.containsKey(part.getName())) {
							result.put(part.getName(), part);
						}
					}
					return result;
				}
				return new LinkedHashMap<>(0);
			}
			else {
				Map<String, String[]> parameterMap = webRequest.getParameterMap();
				Map<String, String> result = new LinkedHashMap<>(parameterMap.size());
				parameterMap.forEach((key, values) -> {
					if (values.length > 0) {
						result.put(key, values[0]);
					}
				});
				return result;
			}
		}
	}
}

上面沒有仔細看,實際上是有點看不懂,不知道處理場景?就舉兩個例子吧

  1. 對於 RequestParamMapMethodArgumentResolver 類,它的效果是,將所有引數新增到 Map 集合中,示例如下:

    // Controller.java
    
    @RequestMapping("/hello")
    public String hello4(@RequestParam Map<String, Object> map) {
        return "666";
    }
    

    傳送請求 GET /hello?name=yyy&age=20nameage 引數,就會都新增到 map

  2. 對於 RequestParamMethodArgumentResolver 類,它的效果是,將指定名字的引數新增到 Map 集合中,示例如下:

    // Controller.java
    
    @RequestMapping("/hello")
    public String hello5(@RequestParam(name = "map") Map<String, Object> map) {
        return "666";
    }
    

    傳送請求 GET /hello4?map={"name": "yyyy", age: 20}map 引數的元素則都會新增到方法引數 map 中。當然,要注意下,實際請求要 UrlEncode 編碼下引數,所以實際請求是 GET /hello?map=%7b%22name%22%3a+%22yyyy%22%2c+age%3a+20%7d

PathVariableMethodArgumentResolver

org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver,實現 UriComponentsContributor 介面,繼承 AbstractNamedValueMethodArgumentResolver 抽象類,處理路徑引數

supportsParameter

實現 supportsParameter(MethodParameter parameter) 方法,判斷是否支援處理該方法引數,程式碼如下:

@Override
public boolean supportsParameter(MethodParameter parameter) {
    // <1> 如果無 @PathVariable 註解
    if (!parameter.hasParameterAnnotation(PathVariable.class)) {
        return false;
    }
    // <2> Map 型別,有 @PathVariable 註解,但是有 name 屬性
    if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
        PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class);
        return (pathVariable != null && StringUtils.hasText(pathVariable.value()));
    }
    return true;
}
  1. 如果沒有 @PathVariable 註解則直接返回 fasle,也就是說必須配置 @PathVariable 註解
  2. 如果還是 Map 型別,則需要 @PathVariable 註解有 name 屬性,才返回 true,檢視 org.springframework.web.servlet.mvc.method.annotation.PathVariableMapMethodArgumentResolver 就理解了,和上述的邏輯差不多
  3. 否則,直接返回 true

createNamedValueInfo

實現 createNamedValueInfo(MethodParameter parameter) 方法,方法如下:

@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
    // 獲得 @PathVariable 註解
    PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
    Assert.state(ann != null, "No PathVariable annotation");
    // 建立 PathVariableNamedValueInfo 物件
    return new PathVariableNamedValueInfo(ann);
}

private static class PathVariableNamedValueInfo extends NamedValueInfo {

    public PathVariableNamedValueInfo(PathVariable annotation) {
        super(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE);
    }
}

必須要有 @PathVariable 註解,沒有的話丟擲異常,然後根據註解建立 PathVariableNamedValueInfo 物件

resolveName

實現 resolveName(String name, MethodParameter parameter, NativeWebRequest request) 方法,從請求路徑中獲取方法引數的值,方法如下:

@Override
@SuppressWarnings("unchecked")
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
    // 獲得路徑引數
    Map<String, String> uriTemplateVars = (Map<String, String>) request.
        getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
    // 獲得引數值
    return (uriTemplateVars != null ? uriTemplateVars.get(name) : null);
}

handleResolvedValue

重寫 handleResolvedValue(Object arg, String name, MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest request) 方法,新增獲得的屬性值到請求的 View.PATH_VARIABLES 屬性種,方法如下:

@Override
@SuppressWarnings("unchecked")
protected void handleResolvedValue(@Nullable Object arg, String name, MethodParameter parameter,
        @Nullable ModelAndViewContainer mavContainer, NativeWebRequest request) {
    // 獲得 pathVars
    String key = View.PATH_VARIABLES;
    int scope = RequestAttributes.SCOPE_REQUEST;
    Map<String, Object> pathVars = (Map<String, Object>) request.getAttribute(key, scope);
    // 如果不存在 pathVars,則進行建立
    if (pathVars == null) {
        pathVars = new HashMap<>();
        request.setAttribute(key, pathVars, scope);
    }
     // 新增 name + arg 到 pathVars 中
    pathVars.put(name, arg);
}

具體用途還不清楚?

總結

HandlerAdapter 執行 HandlerMethod 處理器的過程中,會將該處理器封裝成 ServletInvocableHandlerMethod 物件,通過該物件來執行處理器。該物件通過反射機制呼叫對應的方法,在呼叫方法之前,藉助 HandlerMethodArgumentResolver 引數解析器從請求中獲取到對應的方法引數值,因為你無法確認哪個引數值對應哪個引數,所以需要先通過它從請求中解析出引數值,一一對應,然後才能呼叫該方法。

HandlerMethodArgumentResolver 引數解析器的實現類非常多,採用了組合模式來進行處理,如果有某一個引數解析器支援解析該方法引數,則使用它從請求體中獲取到該方法引數的值,注意這裡有一定的先後順序,因為是通過 LinkedList 儲存所有的實現類,排在前面的實現類則優先處理。

本文分析了我們常用的 @RequestParam@PathVariable 註解所對應的 HandlerMethodArgumentResolver 實現類,如下:

  • RequestParamMethodArgumentResolver:解析 @RequestParam 註解配置引數(名稱、是否必須、預設值),根據註解配置從請求獲取引數值
  • PathVariableMethodArgumentResolver:解析 @PathVariable 註解配置的(名稱、是否必須),根據註解配置從請求路徑中獲取引數值

注意,關於方法引數為 Map 型別,應該如何配置,可以參考上面的 RequestParamMapMethodArgumentResolver 小節中的兩個示例

關於其他的 HandlerMethodArgumentResolver 實現類,感興趣的可以去看看

在接下來的《HandlerAdapter 元件(四)之 HandlerMethodReturnValueHandler》中講到 RequestResponseBodyMethodProcessor 既是 HandlerMethodReturnValueHandler 實現類,也是 HandlerMethodArgumentResolver 實現類,用於處理器 @RequestBody 和 @ResponseBody 兩個註解

參考文章:芋道原始碼《精盡 Spring MVC 原始碼分析》

相關文章