bingoohuang / blog

write blogs with issues
MIT License
176 stars 23 forks source link

SpringMVC埋点后参数名称丢失问题分析 #58

Open bingoohuang opened 5 years ago

bingoohuang commented 5 years ago
package xx;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @GetMapping("/")
    public String hello(@RequestParam String name, @RequestParam String addr, @RequestParam int age) {
        return "hello " + name + " at " + addr + " with age " + age;
    }
}
package xx;

import net.bytebuddy.build.Plugin;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.DynamicType.Builder;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import org.springframework.web.bind.annotation.GetMapping;

import java.lang.reflect.Method;
import java.util.concurrent.Callable;

import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;

public class ByteBuddyHookPlugin implements Plugin {
    @Override
    public boolean matches(TypeDescription target) {
        return true;
    }

    @Override
    public Builder<?> apply(Builder<?> builder, TypeDescription typeDescription, ClassFileLocator classFileLocator) {
        return builder.method(isAnnotatedWith(GetMapping.class))
                .intercept(MethodDelegation.to(MetricsIntercept.class));
    }

    @Override public void close() {

    }

    public static class MetricsIntercept {
        @RuntimeType
        public static Object intercept(@Origin Class obj, @AllArguments Object[] allArguments,
                                       @SuperCall Callable<?> zuper, @Origin Method method) throws Exception {
            System.out.println("start");
            return zuper.call();
        }
    }
}
package xx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.github.bingoohuang</groupId>
    <artifactId>springboot-upload-server</artifactId>
    <version>1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.4.RELEASE</version>
    </parent>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>1.9.6</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

运行mvn clean package命令,然后javap -v target/classes/HelloController.class可以看到,hello函数上有一个本地变量表LocalVariableTable

public java.lang.String hello(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: new           #2                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         7: ldc           #4                  // String hello
         9: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        12: aload_1
        13: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        16: invokevirtual #6                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        19: areturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      20     0  this   LHelloController;
            0      20     1  name   Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      name
    RuntimeVisibleAnnotations:
      0: #22(#23=[s#24])
    RuntimeVisibleParameterAnnotations:
      parameter 0:
        0: #26()

在pom.xml中添加bytebuddy插件后,再次运行mvn clean packagejavah命令后,看到LocalVariableTable没有了,导致hello方法无法正确调用。

<plugin>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy-maven-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>transform</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <transformations>
            <transformation>
                <plugin>xx.ByteBuddyHookPlugin</plugin>
            </transformation>
        </transformations>
    </configuration>
</plugin>

然后访问curl "http://127.0.0.1:8080?addr=bjca&name=bingoo"会打印出

{"timestamp":1546942729387,"status":500,"error":"Internal Server Error","exception":"java.lang.IllegalArgumentException","message":"Name for argument type [java.lang.String] not available, and parameter name information not found in class file either.","path":"/"}
bingoohuang commented 5 years ago

解决办法1,在pom.xml中添加以下编译插件。

打开java8的生成参数名的开关,参考

<parameters>    boolean 3.6.2   
Set to true to generate metadata for reflection on method parameters.
Default value is: false.
User property is: maven.compiler.parameters.
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.3</version>
    <configuration>
        <compilerArgs>
            <arg>-parameters</arg>
        </compilerArgs>
    </configuration>
</plugin>
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.0</version>
    <configuration>
        <parameters>true</parameters>
    </configuration>
</plugin>
bingoohuang commented 5 years ago

解决办法2,升级spring-boot版本

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.1.RELEASE</version>
</parent>

跟踪~/.m2/repository/org/springframework/boot/spring-boot-starter-parent/2.1.1.RELEASE/spring-boot-starter-parent-2.1.1.RELEASE.pom,发现其中maven-compiler-plugin的配置已经添加了parameters参数了。

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <parameters>true</parameters>
    </configuration>
</plugin>

再继续跟踪~/.m2/repository/org/springframework/boot/spring-boot-dependencies/2.1.1.RELEASE/spring-boot-dependencies-2.1.1.RELEASE.pom,发现compiler版本已经到了3.8.0了。

<maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
bingoohuang commented 5 years ago

在SpringMVC中,强烈建议还是给@RequestParam加上属性名称,例如@RequestParam("name"),将请求属性名称与方法参数名称进行分离。

如果不加,例如下面这样,在代码重构时,若改变name参数的名称,也会导致请求参数的名称发生变化了,例如把name改成了x,那么就不只是方法参数名称变化了而已,也会导致客户端HTTP请求参数名称也需要从name变化成x了

@RequestMapping("/hello")
public String hello(@RequestParam String name) {
    return "messages/hello" + name;
}