spring mvc如何實(shí)現(xiàn)參數(shù)名綁定

0. 寫(xiě)在前面

一次偶然寫(xiě)代碼的時(shí)候想到這個(gè)問(wèn)題,通過(guò)反射我們是無(wú)法得到方法參數(shù)的名稱(chēng)的,那么spring mvc是如何通過(guò)參數(shù)名稱(chēng)完成請(qǐng)求參數(shù)綁定的呢?帶著這個(gè)疑問(wèn)進(jìn)行了一番調(diào)查。

1. 參數(shù)綁定的方式

參數(shù)綁定的核心在于如何確定請(qǐng)求參數(shù)與方法參數(shù)之間如何對(duì)應(yīng),大致上,spring mvc實(shí)現(xiàn)請(qǐng)求參數(shù)和方法參數(shù)的綁定有三種方式,一種是通過(guò)注解實(shí)現(xiàn),包括使用@PathVariable和@RequestParam。一個(gè)簡(jiǎn)單的實(shí)例如下,方法參數(shù)將同請(qǐng)求中參數(shù)名為注解中指定的名稱(chēng)進(jìn)行綁定。

@GetMapping("annotation")
    public void testAnnotation(
            @RequestParam("f1") int f1, @RequestParam("f2")int f2, @RequestParam("f3")int f3
            , @RequestParam("f4")int f4, @RequestParam("f5")int f5, @RequestParam("f6")int f6
            , @RequestParam("f7")int f7, @RequestParam("f8")int f8, @RequestParam("f9")int f9
            , @RequestParam("f10")int f10, @RequestParam("f11")int f11, @RequestParam("f12")int f12
            , @RequestParam("f13")int f13, @RequestParam("f14")int f14, @RequestParam("f15")int f15
            , @RequestParam("f16")int f16) {
        System.out.println("annotation:" + System.currentTimeMillis());
    }

第二種方式是不使用注解,直接通過(guò)參數(shù)名稱(chēng)與請(qǐng)求參數(shù)名稱(chēng)綁定

@GetMapping("name")
    public void testByName(int f1, int f2, int f3, int f4, int f5
            , int f6, int f7, int f8, int f9, int f10
            , int f11, int f12, int f13, int f14, int f15, int f16) {
        System.out.println("name:" + System.currentTimeMillis());
    }

第三種方式是參數(shù)對(duì)象的屬性與請(qǐng)求參數(shù)實(shí)現(xiàn)綁定

@GetMapping("domain")
    public void testByDomain(TestDomain domain){
        System.out.println("domain:" + System.currentTimeMillis());
    }

2. 參數(shù)解析的方式

spring mvc使用了多個(gè)參數(shù)解析器用于針對(duì)各類(lèi)參數(shù)的解析,這些解析器都實(shí)現(xiàn)了HandlerMethodArgumentResolver這個(gè)接口,spring mvc使用策略模式輪詢(xún)每一種參數(shù)解析器來(lái)尋找適合參數(shù)的解析器,找到過(guò)后將放入緩存之中,下一次請(qǐng)求同一個(gè)方法時(shí)將直接從緩存中拿到解析器。源代碼如下

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

解析@RequestParam注解的參數(shù)解析器是RequestParamMethodArgumentResolver,初次解析時(shí)將通過(guò)反射獲取注解中指定的參數(shù)名稱(chēng)

@Override
    protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
        RequestParam ann = parameter.getParameterAnnotation(RequestParam.class);
        return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo());
    }
--------------
public RequestParamNamedValueInfo(RequestParam annotation) {
            super(annotation.name(), annotation.required(), annotation.defaultValue());
        }
--------------
public NamedValueInfo(String name, boolean required, String defaultValue) {
            this.name = name;
            this.required = required;
            this.defaultValue = defaultValue;
        }

再次進(jìn)行解析時(shí),則通過(guò)緩存直接得到RequestParamNamedValueInfo,緩存是ConcurrentHashMap,并發(fā)讀無(wú)限制,代碼中并發(fā)寫(xiě)線程安全

private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
        NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
        if (namedValueInfo == null) {
            namedValueInfo = createNamedValueInfo(parameter);
            namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
            this.namedValueInfoCache.put(parameter, namedValueInfo);
        }
        return namedValueInfo;
    }

不使用注解直接通過(guò)方法參數(shù)名綁定時(shí)對(duì)應(yīng)的解析器仍然是RequestParamMethodArgumentResolver,在spring mvc提供的參數(shù)解析器之中包含兩個(gè)該實(shí)例,能對(duì)方法參數(shù)名解析的實(shí)例中useDefaultResolution為true,也就是說(shuō)作為默認(rèn)解析了。該實(shí)例順序在倒數(shù)第二,也就是說(shuō)此前的策略都無(wú)法解析的情況下,使用該策略

else if (this.useDefaultResolution) {
                return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
            }

我們知道通過(guò)反射是無(wú)法獲取方法參數(shù)名的,那么spring mvc是如何做到的呢?
spring mvc在java1.8之前使用LocalVariableTableParameterNameDiscoverer來(lái)獲取方法參數(shù)名稱(chēng),關(guān)鍵步驟如下,首先獲取該方法對(duì)應(yīng)的類(lèi),嘗試從緩存獲取參數(shù)名信息

public String[] getParameterNames(Method method) {
        Method originalMethod = BridgeMethodResolver.findBridgedMethod(method);
        Class<?> declaringClass = originalMethod.getDeclaringClass();
        Map<Member, String[]> map = this.parameterNamesCache.get(declaringClass);
        if (map == null) {
            map = inspectClass(declaringClass);
            this.parameterNamesCache.put(declaringClass, map);
        }
        if (map != NO_DEBUG_INFO_MAP) {
            return map.get(originalMethod);
        }
        return null;
    }

緩存無(wú)法獲取參數(shù)名信息的情況下,spring mvc將調(diào)用inspectClass方法區(qū)獲取參數(shù)名信息,如果仍然無(wú)法獲取,即map == NO_DEBUG_INFO_MAP則代表在類(lèi)編譯階段沒(méi)有生成調(diào)試信息,根本不可能獲取到參數(shù)名信息。
這里有兩個(gè)關(guān)鍵點(diǎn),一個(gè)是inspectClass如何解析參數(shù)名信息,第二個(gè)是什么事參數(shù)名信息。先來(lái)看第一個(gè),繼續(xù)往下看代碼,實(shí)質(zhì)上獲取參數(shù)名信息是通過(guò)spring自己實(shí)現(xiàn)的asm對(duì)類(lèi)文件進(jìn)行解析,asm也是cglib底層使用的工具,相關(guān)內(nèi)容可以百度哈。這里也就是說(shuō)spring mvc獲取參數(shù)名信息是通過(guò)asm解析類(lèi)文件拿到調(diào)試信息,而調(diào)試信息里面有參數(shù)名信息。

private Map<Member, String[]> inspectClass(Class<?> clazz) {
        InputStream is = clazz.getResourceAsStream(ClassUtils.getClassFileName(clazz));
        if (is == null) {
            // We couldn't load the class file, which is not fatal as it
            // simply means this method of discovering parameter names won't work.
            if (logger.isDebugEnabled()) {
                logger.debug("Cannot find '.class' file for class [" + clazz +
                        "] - unable to determine constructor/method parameter names");
            }
            return NO_DEBUG_INFO_MAP;
        }
        try {
            ClassReader classReader = new ClassReader(is);
            Map<Member, String[]> map = new ConcurrentHashMap<Member, String[]>(32);
            classReader.accept(new ParameterNameDiscoveringVisitor(clazz, map), 0);
            return map;
        }
        catch (IOException ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("Exception thrown while reading '.class' file for class [" + clazz +
                        "] - unable to determine constructor/method parameter names", ex);
            }
        }
        catch (IllegalArgumentException ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("ASM ClassReader failed to parse class file [" + clazz +
                        "], probably due to a new Java class file version that isn't supported yet " +
                        "- unable to determine constructor/method parameter names", ex);
            }
        }
        finally {
            try {
                is.close();
            }
            catch (IOException ex) {
                // ignore
            }
        }
        return NO_DEBUG_INFO_MAP;
    }

那么調(diào)試信息是如何生成的呢?這實(shí)際是在編譯器決定的,對(duì)于java1.8之前,我們可以使用-g來(lái)生成調(diào)試信息,-g的具體使用可自行百度,我們直接看效果

D:\java\mushroomplay\pmp\pmp-core\project\src\main\java\com\jd\pmp\project\dao>javac Main.java

D:\java\mushroomplay\pmp\pmp-core\project\src\main\java\com\jd\pmp\project\dao>javap -l Main.class
Compiled from "Main.java"
public class Main {
  public Main();
    LineNumberTable:
      line 8: 0

  public void f1(java.lang.String, int);
    LineNumberTable:
      line 13: 0

  public java.lang.String f2(java.util.Map);
    LineNumberTable:
      line 16: 0
}
D:\java\mushroomplay\pmp\pmp-core\project\src\main\java\com\jd\pmp\project\dao>javac -g Main.java

D:\java\mushroomplay\pmp\pmp-core\project\src\main\java\com\jd\pmp\project\dao>javap -l Main.class
Compiled from "Main.java"
public class Main {
  public Main();
    LineNumberTable:
      line 8: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   LMain;

  public void f1(java.lang.String, int);
    LineNumberTable:
      line 13: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       1     0  this   LMain;
          0       1     1  name   Ljava/lang/String;
          0       1     2   age   I

  public java.lang.String f2(java.util.Map);
    LineNumberTable:
      line 16: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       2     0  this   LMain;
          0       2     1   map   Ljava/util/Map;
}

可以看出當(dāng)編譯時(shí)使用-g時(shí)我們可以看到生成了LocalVariableTable,里面保存了該類(lèi)中所有方法的參數(shù)名信息。-g默認(rèn)是不打開(kāi)的,不過(guò)maven編譯時(shí)默認(rèn)是打開(kāi)的,也就是說(shuō)如果我們不特別指定,maven會(huì)自動(dòng)生成調(diào)試信息,asm也就能夠解析出參數(shù)名信息供spring mvc進(jìn)行參數(shù)綁定。這是maven官方對(duì)應(yīng)的文檔:http://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html


針對(duì)java1.8,我們可以在編譯期使用-parameter參數(shù),這樣我們可以通過(guò)反射獲取參數(shù)名信息,1.8jdk在反射包增加了Parameter這個(gè)類(lèi),該類(lèi)可以在使用了-paramter的情況下得到參數(shù)名信息。針對(duì)這個(gè)情況,spring mvc提供了如下這個(gè)類(lèi)獲取參數(shù)名信息

@UsesJava8
public class StandardReflectionParameterNameDiscoverer implements ParameterNameDiscoverer

最后是通過(guò)對(duì)象實(shí)現(xiàn)綁定,這種方式使用ServletModelAttributeMethodProcessor進(jìn)行綁定,就不多說(shuō)了

3. 不同解析方式的性能

通過(guò)簡(jiǎn)單的測(cè)試發(fā)現(xiàn),如果是啟動(dòng)后第一次訪問(wèn)三種方式的速度都會(huì)比再次訪問(wèn)更慢,可以很容易的理解這是有緩存的緣故。三種方式對(duì)比則沒(méi)有明顯的性能差異。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,715評(píng)論 19 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,290評(píng)論 6 342
  • 一,登陸www.github.com 二,搜索zxing 三,選擇zxing源碼,下載源碼壓縮包。注意,下載源碼的...
    珍珠熊騎士閱讀 5,915評(píng)論 0 4
  • 我要批評(píng)你 是什么理由讓輪印深邃到臉上 是什么理由讓血管像死去的蚯蚓躺在手臂上 是什么理由 原本茂密的森林 如今退...
    零溫度閱讀 219評(píng)論 0 0
  • 有些花在那里,以自己的姿態(tài)盛開(kāi) 紫花地丁,從小喜歡的花草 ,一到春天便會(huì)開(kāi)在田野上。野地的花草除了大片的盛開(kāi)從來(lái)不...
    花開(kāi)無(wú)它閱讀 743評(píng)論 0 3

友情鏈接更多精彩內(nèi)容