主頁 > 後端開發 > 精盡Spring MVC原始碼分析 - HandlerAdapter 組件(三)之 HandlerMethodArgumentResolver

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

2020-12-19 06:38:45 後端開發

該系列檔案是本人在學習 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 組件(一)之 HandlerAdapter》
  • 《HandlerAdapter 組件(二)之 ServletInvocableHandlerMethod》
  • 《HandlerAdapter 組件(三)之 HandlerMethodArgumentResolver》
  • 《HandlerAdapter 組件(四)之 HandlerMethodReturnValueHandler》
  • 《HandlerAdapter 組件(五)之 HttpMessageConverter》

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;
}

類圖

因為請求入參的場景非常多,所以 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 = "https://www.cnblogs.com/lifullmoon/p/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 = https://www.cnblogs.com/lifullmoon/p/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 = https://www.cnblogs.com/lifullmoon/p/(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 = "https://www.cnblogs.com/lifullmoon/p/${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 =https://www.cnblogs.com/lifullmoon/p/= 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 原始碼分析》

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/236909.html

標籤:Java

上一篇:一文搞懂什么是事務

下一篇:java~jar防止反編譯

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more