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)有明顯的性能差異。