利用lambda优化反射功能实现方法调用

最近在思考lambda相关的问题,简单记录下做的相关反射替代和函数映射的尝试。

原理分析

lambda是jdk8才提供的,原理其实就是动态生成内部类来执行函数映射的方法。也就是说一段lambda表达式会对应特定的类方法,之后调用。底层是通过LambdaMetaFactory实现的函数映射,利用了jdk7给出的MethodHandler之类的函数式编程相关类来实现对函数的映射,MethodType用于函数签名。
反射的话基本接触过java的都听说过,底层实现是利用了native的invoke方法,因此说大量使用反射会影响性能,毕竟调用native方法开销会更大一点。
那么,只要利用LambdaMetaFactory或者函数式编程语法糖来获取对应的方法,并在类初始化的情况下进行构建,那么之后利用该对象调用对应方法,就会和直接调用原始方法一样快,因为本质上是在调用生成的内部类的方法。

简单尝试

首先是直接使用反射的方法,对一个User类对象获取其中的Method,invoke直接调用。
反射大家都会,直接给出实现。
用户类,包括函数编程接口User和实现类

@FunctionalInterface
public interface User {
    String getId();
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SimpleUser implements User{
    String id;
    String name;
}

反射类实现

// 简单反射调用user中方法
public class UserReflectAdaptor {
    public static String reflectUserId(SimpleUser user) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class<? extends SimpleUser> c = user.getClass();
        Method method = c.getMethod("getId");
        return (String) method.invoke(user);
    }

    public static void main(String[] args){
        SimpleUser user = new SimpleUser("123", "mage");
        try {
            System.out.println(reflectUserId(user));
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

简单获取Method对象,并使用invoke方法实现代理调用。

lambda表达式实现

lambda表达式进行函数映射基本有数种方法,比较简单的是直接利用::获取函数对象

static User user;
@Benchmark
public static String lambdaUserId() {
    Supplier<String> idGetter = user::getId;
    return idGetter.get();
}

@Benchmark是用来做jmh test的,SupplierFunction的一种实现类,用来表示只有输出没有输入的方法对象。直接使用get()即可调用对应的方法获取结果。

已有对象,需要对特定对象进行bind的情况

需要调用的是public方法,同时有已存在的对象的情况下可使用该方式。
使用返回方法对象的方式:

    @Benchmark
    public static String lambdaFactoryInstanceUserId(){
        User sampleUser;
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        try {
            Method method = User.class.getMethod("getId");
            // lookup对应的函数,非反射直接映射
            MethodHandle methodHandle = lookup.unreflect(method);
            // 构建函数映射,invokeType为callSite的期望输出,必须为接口类型
            // 增加参数用来输入实现类
            MethodType invokedType = MethodType.methodType(User.class, User.class);
            // 方法对应Type,参数为输出和输入
            MethodType returnType = MethodType.methodType(String.class);
            // 句柄指向真正的方法
            CallSite callSite = LambdaMetafactory.metafactory(lookup, "getId",
                    invokedType,
                    returnType,
                    methodHandle,
                    returnType);
            // 参数表示实现类对象
            sampleUser = (User) callSite.getTarget().invokeExact(user);
            return sampleUser.getId() + " lambda user interface";
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return null;
    }

需要注意的是,示例里名为user的对象为已有的对象,要对其进行绑定并调用getId方法获取对应的结果。
首先和反射相似,获取对应的method对象。lookup.unreflect方法将对应method设为不可反射,以此获取方法签名和控制MethodHandler。之后使用LambdaMetafactory.metafactory工厂方法来构建对应的lambda对象生成类。方法参数可解释为:

  1. lookup,即为获取调用method对象的lookup类,在之前已初始化。
  2. 方法名,即为之后要调用的方法的名称,在这个示例生成的是User类,所以直接写对应的要调用方法的名称即可。
  3. invokeType,这个参数要注意的是,这个Type对应的是factory生成的对象类型,比如这个示例里生成User接口对应的对象。这里第二个参数指的是callSite.getTarget().invokeExact(user);中绑定的User对象参数。
  4. returnType这个Type对应的是字节码bytecode层面的调用方法的对应,也就是getId方法对应的MethodType。因为是字节码层面的对应,这里也可写成MethodType.methodType(Object.class)
  5. MethodHandle前面已经初始化,也是用来确定要调用的方法。
  6. 最后的returnType为真正调用时需要的,所以是String.class,也就是getId方法,只有返回值String而没有参数。
    最后,在invokeType中我们增加了User.class参数,因此在callSite.getTarget().invokeExact(user);中可以进行对象的绑定,实现lambda优化的方法调用。

不需要与特定对象进行bind的情况

比较灵活的方法是生成Function函数接口相关的对象,如BiCustom,supplier之类的,这个示例中直接生成Function。
设定一个简单工具类:

public class SimpleUserGetter implements UserGetter{
    @Override
    public String getId(User user) {
        return user.getId();
    }
    private String privateMethodGetter(){
        return "this is private";
    }
    public String publicMethodGetter(){
        return "this is public";
    }
}

对public方法publicMethodGetter方法进行lambda优化:

public static String lambdaFactoryFunction(){
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        try {
            Method method = SimpleUserGetter.class.getMethod("publicMethodGetter");
            // lookup对应的函数,非反射直接映射
            MethodHandle methodHandle = lookup.unreflect(method);
            // 构建函数映射,invokeType为callSite的期望输出,必须为接口类型
            MethodType invokedType = MethodType.methodType(Function.class);
            // 方法对应Type,参数为输出和输入
            MethodType returnType = MethodType.methodType(String.class, SimpleUserGetter.class);
            // 这边的apply对应的是Function的方法apply
            CallSite callSite = LambdaMetafactory.metafactory(lookup, "apply",
                    invokedType,
                    // byteCode level
                    MethodType.methodType(Object.class, Object.class),
                    methodHandle,
                    returnType);
            Function sp = (Function) callSite.getTarget().invokeExact();
            return (String)sp.apply(simpleUserGetter);
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return "error";
    }

这边需要注意的变化是invokedType变化了,参数为Function.class,并只有一个,因为并不需要绑定特定的对象即可生成Function接口实现类对象。
另一点是方法名,这边是apply,因为这个方法名参数对应的是最终调用的方法,也就是Function接口中apply方法,而不是getId,这是非常容易错的。
因此最后的returnType也进行了变化,需要贴合apply方法进行设置。

获取private方法的情况

调用privateMethodGetter方法:

    public static String lambdaFactoryPrivateFunction(){
        try {
            MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(SimpleUserGetter.class, MethodHandles.lookup());
            Method method = SimpleUserGetter.class.getDeclaredMethod("privateMethodGetter");
            method.setAccessible(true);
            // lookup对应的函数,非反射直接映射
            MethodHandle methodHandle = lookup.unreflect(method);
            // 构建函数映射,invokeType为callSite的期望输出,必须为接口类型
            MethodType invokedType = MethodType.methodType(Function.class);
            // 方法对应Type,参数为输出和输入
            MethodType returnType = MethodType.methodType(String.class, SimpleUserGetter.class);
            CallSite callSite = LambdaMetafactory.metafactory(lookup, "apply",
                    invokedType,
                    MethodType.methodType(Object.class, Object.class),
                    methodHandle,
                    returnType);
            Function sp = (Function) callSite.getTarget().invokeExact();
            return (String)sp.apply(simpleUserGetter);
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return "error";
    }

最重要的是lookup的初始化,需要直接给予private方法调用的权限,使用MethodHandles.privateLookupIn来获取对SimpleUserGetter的private lookup权限。
Method的accessible权限获取大家都懂,不赘述了。

时间测试

简单对使用lambda优化对象进行调用和每次都反射调用的方法进行测试。
对三个方法进行测试:

    @Benchmark
    public static String SimpleUserId() {
        return user.getId();
    }

    // static方法,利用额外的类进行user处理
    @Benchmark
    public static String reflectUserId() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class<? extends SimpleUser> c = (Class<? extends SimpleUser>) user.getClass();
        Method method = c.getMethod("getId");
        return (String) method.invoke(user);
}

    @Benchmark
    public static String lambdaFactoryUserId(){
        return simpleUser.getId();
    }

分别为直接调用,反射方法和创建lambda对象后进行调用,其中最后一个方法的simpleUser是在类的static块中进行初始化的,实现过程和上文示例中相同。
利用lambda优化反射功能实现方法调用_第1张图片

方法 时间
直接调用 1 0 − 9 10^{-9} 109
利用反射 1 0 − 7 10^{-7} 107
利用lambda 1 0 − 9 10^{-9} 109

最后的时间结果可以看出,lambda优化的方法代理调用不需要额外的开销。

总结

简单写了下lambda代替反射的demo,之前几个项目里面有用到使用代理类来实现项目中的SPI实现,这种大量调用的情况如果在初始化过程中使用lambdaFactory,之后之间使用函数对象调用,能快不少,有空改写下试试。

你可能感兴趣的:(java,java,开发语言)