获取Java方法参数名的原理与实践

渗透技巧 1年前 (2023) admin
260 0 0

前言

你是否曾经好奇过 SpringMVC 是如何获取方法参数名实现请求参数映射的呢?是反射还是字节码技术?最近群友在知乎回答了该问题,苦于没有博客,特此转载分享 Java 中获取方法参数名的原理。

ps. 该群友单身优质男青年,95后,在线找女票ing,有意者mm

原文出处:https://zhuanlan.zhihu.com/p/610288146

作者:xinxi

javac 命令

Java里面获取方法的参数名大概有两种方法,对应的javac的两个选项如下

获取Java方法参数名的原理与实践

-g 选项

生成调试用的东西,它有三个,lines、vars、source,也就是调试的时候用的行号、参数名和源文件。直接使用 -g 的话会把这三个信息都生成。

编译时使用 -g 选项,然后使用 javap 可以看到会有一个 LocalVariableTable 块,里面有方法的参数的名字,如下图所示

获取Java方法参数名的原理与实践

-parameters 选项

直接看效果吧,它有个 MethodParameters 块,如下图

获取Java方法参数名的原理与实践

使用代码获取 LocalVariableTable 块

我们自己去读取 class 文件貌似有点难度,借助一些处理字节码的框架会比较ok

使用ASM

import org.springframework.asm.*;

import static org.springframework.asm.Opcodes.*;

public class Main {
    public static void main(String[] args) throws Exception {
        ClassPrinter cp = new ClassPrinter();
        ClassReader cr = new ClassReader("com.example.test.Dog");
        cr.accept(cp, 0);
    }
}

class ClassPrinter extends ClassVisitor {
    public ClassPrinter() {
        super(ASM9);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        System.out.println("---------------------------------------");
        System.out.println(name + " | " + desc);
        return new MethodVisitor(ASM9) {
            @Override
            public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) {
                System.out.println(name + " t " + descriptor);
                super.visitLocalVariable(name, descriptor, signature, start, end, index);
            }
        };
    }
}

这里用的是 Spring ASM,与原生的差不太多,运行效果如下

获取Java方法参数名的原理与实践

使用javassist

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;

import java.lang.reflect.Modifier;

public class Main {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.get("com.example.test.Dog");
        CtMethod ctMethod = ctClass.getDeclaredMethod("func");
        MethodInfo methodInfo = ctMethod.getMethodInfo();
        CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
        LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
        if (attr != null) {
            int len = ctMethod.getParameterTypes().length;
            int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;
            for (int i = 0; i < len; i++) {
                System.out.print(attr.variableName(i + pos) + ' ');
            }
            System.out.println();
        }
    }
}

运行效果如下

获取Java方法参数名的原理与实践

使用代码获取 MethodParameters 块

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class Main {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Dog.class;
        Method method = clazz.getDeclaredMethod("func", String.classInteger.class);
        Parameter[] parameters = method.getParameters();
        for (final Parameter parameter : parameters) {
            if (parameter.isNamePresent()) {
                System.out.print(parameter.getName() + ' ');
            }
        }
    }
}

运行效果如下

获取Java方法参数名的原理与实践

构建工具

写Java的应该很少有手动 javac 的吧?所以看看构建工具是很有必要的。

Maven

Maven编译代码使用的是 maven-compiler-plugin 插件,看看它是怎么玩的

amaven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html

获取Java方法参数名的原理与实践


获取Java方法参数名的原理与实践


可以看到 debug 默认 true,parameters 默认 false。

Gradle

参考

adocs.gradle.org/current/dsl/org.gradle.api.tasks.compile.CompileOptions.html

获取Java方法参数名的原理与实践


可以看到 debug 默认是 true。

没找到 parameters…..你可以自己指定这个选项,默认应该是没有开启这个。

SpringBoot 项目

  • Maven

如果你是下面这样写的话

<parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.7.7</version>
      <relativePath/> <!-- lookup parent from repository -->
</parent>

那么可以看到编译插件被动了点手脚,如下

获取Java方法参数名的原理与实践

还记得么,Maven的编译插件 debug 默认 true,parameters 默认 false,而SpringBoot把parameters也打开了。

  • Gradle

我们直接查看SpringBoot的Gradle插件源码如下

获取Java方法参数名的原理与实践

还记得么, Gradle编译时debug 默认是 true,parameters 默认 false。而SpringBoot插件会检查如果没有 -parameters 的话,就加上去。

Spring 框架

spring-core 模块中有个 ParameterNameDiscoverer 接口,专门用来获取参数的名字。比较重要的实现是如下两个

  • StandardReflectionParameterNameDiscoverer 类

使用JDK 8的反射设施来反省参数名称(编译时需指定 -parameters 参数)

  • LocalVariableTableParameterNameDiscoverer 类

使用 ASM 库来分析类文件,使用方法属性中的 LocalVariableTable 信息来发现参数名称(编译时需指定 -g 参数生成调试信息)

但是实际上使用的类是 DefaultParameterNameDiscoverer,源代码如下

获取Java方法参数名的原理与实践

一看就应该知道是怎么工作的,把能用的手段都用上对吧。

测试代码如下

import com.google.common.collect.Lists;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.StandardReflectionParameterNameDiscoverer;

public class Main {
    public static void main(String[] args) {
        Lists.newArrayList(
                new LocalVariableTableParameterNameDiscoverer(),
                new StandardReflectionParameterNameDiscoverer(),
                new DefaultParameterNameDiscoverer()
        ).forEach(parameterNameDiscoverer -> {
            try {
                String[] parameterNames = parameterNameDiscoverer
                        .getParameterNames(Dog.class.getDeclaredMethod("func", String.classInteger.class));
                for (String parameterName : parameterNames) {
                    System.out.print(parameterName + ' ');
                }
                System.out.println();
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        });
    }
}

运行效果如下

获取Java方法参数名的原理与实践

既没有-g也没有-parameters还能抢救一下吗?

能的。用注解,不过这已经不算是获取方法的参数名了,但也能用不是。。。

获取Java方法参数名的原理与实践


原文始发于微信公众号(Kirito的技术分享):获取Java方法参数名的原理与实践

版权声明:admin 发表于 2023年3月1日 上午1:00。
转载请注明:获取Java方法参数名的原理与实践 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...