【问题标题】:Call method by its name stored in string without reflection API?通过存储在字符串中的名称调用方法而不使用反射 API?
【发布时间】:2014-05-28 17:04:15
【问题描述】:

我知道,使用反射 API,我们可以通过存储在字符串中的名称来调用方法。

但是,反射 API 不能用于高性能应用程序。 在我的应用程序中,方法将以非常高的速率被调用。所以,我不能使用反射 API。

那么,反射 API 有哪些替代方案?

我做了研究,发现可以使用 cglib 和其他代码生成库。

但是,我没有找到任何通过存储在字符串中的名称来调用方法的示例。

使用反射替代方案也可以提供一个很好的示例。

更新: 实际上我正在实现一些主从通信 API。从属将远程调用主方法。而且,方法调用的速度非常快(每秒大约 50 个方法调用)。因为,主机不断地轮询从机以获取任何响应。那么,我应该在如此高的调用率下进行反射吗?

【问题讨论】:

  • 解析和处理字符串比使用反射要昂贵得多。反射确实需要成本,但不如查找字符串多。您对性能有什么要求?
  • 您的问题是,通过使用 String 选择方法来调用方法本质上会非常慢。
  • 每秒调用 50 次,任何技术都可以。通过反射,您每秒最多可以调用 50 百万 个方法。
  • 你使用的是java 7+吗?
  • 是的,我使用的是 Java 7。

标签: java reflection cglib


【解决方案1】:

这就是反射的作用。在排除它之前,我建议尝试一下,看看在过去几年的任何 JVM 上,您是否真的看到任何与之相关的性能问题。我怀疑你不会。

你唯一真正的选择 (其实还有cglib;see this other answer for more, and why you may not want to use it是你让别人调用的方法,传入方法的名字调用,然后分派到该方法(例如,使用大的switch,或分派表,或类似的)。例如:

public Object callMethod(String methodName, Object[] args) {
    switch (methodName) { // Using strings in `switch` requires a recent version of Java
        case "foo":
            return this.foo(args[0]);
        case "bar":
            this.bar(args[0], args[1]);
            return null;
        // ...and so on...
        default:
            throw new AppropriateException();
    }
}

【讨论】:

  • 那么,这不能用cglib来实现吗?
  • @Akshat:看起来可以,我发现 raphw 的回答很有趣。不过,当您遇到实际的、可测量的反射性能问题时,我会再次担心反射的性能问题,而不是之前。
  • 这是我的第一个想法,但过去曾在具有庞大案例语句的系统上工作,并花时间用迭代 CSV 的循环替换它们,我有兴趣避免它。这是另一种选择。我想访问文件系统的开销会比仅使用反射更加密集,但我不确定。我想这取决于被调用的类文件的大小。打算尝试一下反射,看看它是怎么做的。好建议。我想不出另一种方法。
【解决方案2】:

Cglib 带有一个名为FastMethod 的类。此类的目的是通过使用非反射接口调用给定方法。为此,FastMethod 实现了接口并生成用于调用指定方法的字节码,从而避免了所谓的昂贵的反射调用。

但是,您很可能不应该使用这个类,这有两个原因。 Cglib 是很久以前写的。在这些日子里,反射仍然比今天更昂贵。但是,现代 JVM know a concept called inflation。默认情况下,JVM 将在第 15 次反射调用后生成用于调用方法的字节码。这正是 cglib 为您提供的明确功能。

此外,最昂贵的不是反射调用,而是查找。您仍然需要将方法命名为FastMethod。因此,即使使用 cglib,您也无法避免这些成本。

因此,我建议您依靠反思,直到您真正确定这是性能瓶颈。至少,使用像JMH 这样的工具来证明这样的实现是合理的。另外,请考虑类消耗 perm gen / 元空间,这可能会导致 trouble for your users

【讨论】:

    【解决方案3】:

    反射很慢是一个常见的误解。那是在 Java 1.3 左右的时代。

    但在现代 Java 中,反射确实得到了很好的优化。它在后台使用动态字节码生成。此外,JVM 可以将此类调用直接内联到调用者中,因此反射方法的调用几乎与直接调用一样快。

    这里的其他 cmets 建议使用 cglib 的 FastMethod。事实上,它并不比反射快得多。这是一个用著名的JMH 框架编写的基准测试:

    @State(Scope.Benchmark)
    public class MethodInvoke {
        Method method;
        FastMethod fastMethod;
    
        @Setup
        public void init() throws NoSuchMethodException {
            method = getClass().getMethod("foo", int.class, long.class);
            method.setAccessible(true);
            fastMethod = FastClass.create(getClass()).getMethod("foo", new Class[] { int.class, long.class });
        }
    
        @GenerateMicroBenchmark
        public long fastMethod() throws Exception {
            return (Long) fastMethod.invoke(this, new Object[] {2, 3L});
        }
    
        @GenerateMicroBenchmark
        public long reflection() throws Exception {
            return (Long) method.invoke(this, 2, 3L);
        }
    
        public long foo(int a, long b) {
            return a + b;
        }
    }
    

    Java 7u51(64 位)上的结果:

    Benchmark                     Mode   Samples         Mean   Mean error    Units
    b.MethodInvoke.fastMethod    thrpt         5    79248,583     3978,941   ops/ms
    b.MethodInvoke.reflection    thrpt         5    76975,414     2844,730   ops/ms
    

    你看,FastMethod.invoke 只比Method.invoke 快 3%,但与反射相比,FastMethod 不执行正确的参数验证等。

    【讨论】:

    • 不是更快,顺便说一句,平均差异没有统计学意义。
    【解决方案4】:

    通常,对方法的内省调用需要两个阶段:首先,您需要找到要调用的目标方法,然后在目标实例上调用该方法,并为其提供参数。

    在这个过程中,最昂贵的操作是在一个类中定位目标方法(例如Method method = TargetClass.getMethod(Class[] signature ...)一旦你抓住了Method对象,在一个对象上调用方法,比如:method.invoke(targetObj, param,...)是一个轻量级操作只是比直接方法调用稍微贵一点。

    为了证明这一点,我只是对这三种方法进行了快速而肮脏的比较,结果如下(您需要将它们相互比较):

    • 每次反射方法+调用:167ms
    • 类+调用的第一个缓存方法:36 ms
    • 直接调用:17ms

    请注意,自省具有固定的性能成本,因此该方法执行的计算越多,这些数字就越接近。

    我在以前非常重视性能的项目中使用过这种方法缓存方法。在实践中,您观察到实际的方法执行时间使得自省的成本可以忽略不计。 (cfr。Amdahal's law

    测试代码(承受快速和肮脏)

    import java.lang.reflect.Method;
    import java.util.Random;
    
    /**
     * Created by maasg on 5/10/14.
     */
    public class Instrospection {
    
        public static void main(String [] params) throws Exception {
            Random ran = new Random();
            String[] methods = new String[] {"method1", "method2", "method3"};
            Target target = new Target();
    
            // Warmup
            for (int i=0; i<1000; i++) {
                String methodName = methods[ran.nextInt(3)];
                String param = new Integer(ran.nextInt()).toString();
                Method method = Target.class.getMethod(methodName, String.class);
            }
    
            StringBuilder builder = new StringBuilder();
            long t0 = System.currentTimeMillis();
            for (int i=0; i<100000; i++) {
                String methodName = methods[ran.nextInt(3)];
                String param = new Integer(ran.nextInt()).toString();
                Method method = Target.class.getMethod(methodName, String.class);
                Object result = method.invoke(target, "param");
                builder.append(result.toString());
            }
            System.out.println("Elapsed 1: "+(System.currentTimeMillis()-t0));
    
            Method[] invokeMethods = new Method[] {
                Target.class.getMethod(methods[0], String.class),
                Target.class.getMethod(methods[1], String.class),
                Target.class.getMethod(methods[2], String.class),
            };
    
            builder = new StringBuilder();
            long t1 = System.currentTimeMillis();
            for (int i=0; i<100000; i++) {
                String param = new Integer(ran.nextInt()).toString();
                Method method = invokeMethods[ran.nextInt(3)];
                Object result = method.invoke(target, "param");
                builder.append(result.toString());
            }
            System.out.println("Elapsed 2: "+(System.currentTimeMillis()-t1));
    
            builder = new StringBuilder();
            long t2 = System.currentTimeMillis();
            for (int i=0; i<100000; i++) {
                Object result = null;
                String param = new Integer(ran.nextInt()).toString();
                switch (ran.nextInt(3)) {
                    case 0: result = target.method1(param);
                    case 1: result = target.method2(param);
                    case 2: result = target.method3(param);
                }
    
                builder.append(result.toString());
            }
            System.out.println("Elapsed 3: "+(System.currentTimeMillis()-t2));
    
        }
    
    }
    

    【讨论】:

    • 您陷入了编写错误基准的常见错误。将您的第三个测试移到第一个测试之前,直接调用将变得比缓存的反射方法“神奇地”慢。编写公平基准的问题在这里讨论了数千次,我不会复述,而是留下一个指向good benchmarking framework 的链接。
    • @apangin 我想用一些具体的数字来维持我对方法查找与调用的解释,正如你所说,我掉进了微基准测试陷阱。很难“从头开始”制作这些。我应该知道得更好:-)
    【解决方案5】:

    从 Java 1.7 开始,有一种通过 MethodHandle 从名称调用方法的新方法,它比反射快得多。

    我在一台糟糕的笔记本电脑上做了一些基准测试:

    1. 反射每秒可以执行大约 400 万次调用
    2. MethodHandle 每秒可以执行大约 1.4 亿次调用

    两者的性能都可以接受,但平均调用时间仅为 7 纳秒,使用 MethodHandle 非常好。

    如果您在 HashMap 中针对名称存储了对 MethodHandle 的引用,则可以将其重用于后续调用。

    【讨论】:

    • 你在这个基准测试中使用了线束吗?差异不应该存在。
    • @raphw 我编写了自己的基准测试。我重用了对每种类型的引用,所以我只测试了调用的速度。速度差异是因为 MethodHandle 避免了重新执行虚拟查找步骤。预计会有显着的速度差异。
    • 问题是这是否是您通常希望使用反射实现的目标。例如,用户可能会将一个实例交给某个框架,该框架的类被另一个框架动态子类化。如果您在这种情况下以非虚拟方式调用方法,这可能会相当混乱。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-11-04
    • 2023-03-15
    • 2011-07-18
    • 1970-01-01
    • 1970-01-01
    • 2016-08-09
    • 1970-01-01
    相关资源
    最近更新 更多