Spring mvc的参数究竟是如何绑定的

在使用Spring mvc的时候我们会被他优雅的参数绑定所吸引,再也不用写那么多getParameter也不用像struts2中写一堆get、set给开发者带来了很大的便利。

/**
 *注册
 */
@RequestMapping(value = "/register", method = RequestMethod.POST)
public Object register(String userName, String pwd, String rePwd) {}

其中userNamepwdrePwd均来自页面input表单。但是这些参数究竟是怎么注入的呢?小伙伴肯定都想到了方法参数名。下面我们来看一个字节码实验!

我们来看看2份javap的到的字节码。

首先看java源码:

public class Test {

	public void action(String userName, int age) {
		System.out.println(userName);
		System.out.println(age);
	}
}

注:javap -v Test

eclipse下编译得到的字节码:

public class Test
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             // Test
   #2 = Utf8               Test
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8                  #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          // java/lang/Object."":()V
   #9 = NameAndType        #5:#6          // "":()V
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LTest;
  #14 = Utf8               action
  #15 = Utf8               (Ljava/lang/String;I)V
  #16 = Fieldref           #17.#19        // java/lang/System.out:Ljava/io/PrintStream;
  #17 = Class              #18            // java/lang/System
  #18 = Utf8               java/lang/System
  #19 = NameAndType        #20:#21        // out:Ljava/io/PrintStream;
  #20 = Utf8               out
  #21 = Utf8               Ljava/io/PrintStream;
  #22 = Methodref          #23.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #23 = Class              #24            // java/io/PrintStream
  #24 = Utf8               java/io/PrintStream
  #25 = NameAndType        #26:#27        // println:(Ljava/lang/String;)V
  #26 = Utf8               println
  #27 = Utf8               (Ljava/lang/String;)V
  #28 = Methodref          #23.#29        // java/io/PrintStream.println:(I)V
  #29 = NameAndType        #26:#30        // println:(I)V
  #30 = Utf8               (I)V
  #31 = Utf8               userName
  #32 = Utf8               Ljava/lang/String;
  #33 = Utf8               age
  #34 = Utf8               I
  #35 = Utf8               SourceFile
  #36 = Utf8               Test.java
{
  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 2: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTest;
  public void action(java.lang.String, int);
    descriptor: (Ljava/lang/String;I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_1
         4: invokevirtual #22                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         7: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_2
        11: invokevirtual #28                 // Method java/io/PrintStream.println:(I)V
        14: return
      LineNumberTable:
        line 5: 0
        line 6: 7
        line 7: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   LTest;
            0      15     1 userName   Ljava/lang/String;
            0      15     2   age   I
}

用javac编译

public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #18.#19        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #4 = Methodref          #18.#20        // java/io/PrintStream.println:(I)V
   #5 = Class              #21            // Test
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8                  #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               action
  #12 = Utf8               (Ljava/lang/String;I)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Test.java
  #15 = NameAndType        #7:#8          // "":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Class              #26            // java/io/PrintStream
  #19 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #20 = NameAndType        #27:#29        // println:(I)V
  #21 = Utf8               Test
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
  #29 = Utf8               (I)V
{
  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 2: 0
  public void action(java.lang.String, int);
    descriptor: (Ljava/lang/String;I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_1
         4: invokevirtual #3                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_2
        11: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        14: return
      LineNumberTable:
        line 5: 0
        line 6: 7
        line 7: 14
}

可以看出eclipse下多了LocalVariableTable这样一段,这一段就是记录的我们方法的参数名。

Start  Length  Slot  Name   Signature
0      15     0  this   LTest;
0      15     1 userName   Ljava/lang/String;
0      15     2   age   I

LocalVariableTable属性建立了方法中的局部变量与源代码中的局部变量之间的对应关系。这个属性存在于Code属性中。这个属性是可选的,编译器可以选择不生成这个属性。

这个参数是为了让编译器在代码提示时,可以提示出友好的参数名。

##我们再来翻翻Spring的源代码

参数绑定的代码位于Spring-web org.springframework.web.method.support.InvocableHandlerMethod.java中,有兴趣的同学请查看invokeForRequestgetMethodArgumentValuesdoInvoke三个方法!

我们继续追踪,追踪到了DefaultParameterNameDiscoverer这个类!

/**
 * Default implementation of the {@link ParameterNameDiscoverer} strategy interface,
 * using the Java 8 standard reflection mechanism (if available), and falling back
 * to the ASM-based {@link LocalVariableTableParameterNameDiscoverer} for checking
 * debug information in the class file.
 *
 * Further discoverers may be added through {@link #addDiscoverer(ParameterNameDiscoverer)}.
 *
 * @author Juergen Hoeller
 * @since 4.0
 * @see StandardReflectionParameterNameDiscoverer
 * @see LocalVariableTableParameterNameDiscoverer
 */
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {

   private static final boolean standardReflectionAvailable = ClassUtils.isPresent(
         "java.lang.reflect.Executable", DefaultParameterNameDiscoverer.class.getClassLoader());


   public DefaultParameterNameDiscoverer() {
      if (standardReflectionAvailable) {
         addDiscoverer(new StandardReflectionParameterNameDiscoverer());
      }
      addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
   }

}

英语好的同学可以翻译一下类上面的注释,大致的意思是默认优先尝试判断是否是java8,java8则使用java8提供的方法获取否则使用Asm从字节码LocalVariableTable中读取。

有兴趣的朋友可以读读LocalVariableTableParameterNameDiscoverer的代码,它里面就是采用Asm读取的字节码。spring-core中是包含了Asm和CGlib的代码的,Spring中并不需要手动导入AsmCGlib的包。

最后我们来思考一下:

如果我们的Spring mvc项目,非java8也不编译LocalVariableTable信息会出现什么情况?

如下图,在eclipseidea中取消LocalVariableTable信息的生成。

重新编译运行我们的示例项目spring-shiro-traininghttp://git.oschina.net/wangzhixuan/spring-shiro-training

启动完之后报异常了:

java.lang.IllegalArgumentException: Name for argument type [java.lang.String] not available, and parameter name information not found in class file either.
	at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.updateNamedValueInfo(AbstractNamedValueMethodArgumentResolver.java:154) ~[spring-web-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.getNamedValueInfo(AbstractNamedValueMethodArgumentResolver.java:132) ~[spring-web-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.resolveArgument(AbstractNamedValueMethodArgumentResolver.java:88) ~[spring-web-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:99) ~[spring-web-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:161) ~[spring-web-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:128) ~[spring-web-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:832) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:743) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:961) [spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:895) [spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:967) [spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:869) [spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:647) [tomcat-embed-core-7.0.47.jar:7.0.47]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:843) [spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	......

注:省略了部分异常。

总结:

Java8之前由于没有提供获取方法入参的参数名的Api,故Spring采用从字节码中获取。Spring mvc中的控制器的参数绑定也并非完美,可以说是投机取巧,因为编辑器都会默认生成LocalVariableTable信息。

捐助共勉
版权声明:若无特殊注明,本文皆为原创,转载请保留文章出处。