反射构造最佳性能的替代方案:关于 Lambda、泛型、MethodHandle 、受检异常的技巧运用

918 阅读1分钟

本文环境:Java17\color{gray}{\small 本文环境:Java17}

前言

尝试一下新的文章结构,写从最基础的反射如何一步步优化演变到最佳方案,希望能够对各位有所帮助

完整代码在文末

1. 基础写法

众所周知,最基础的使用反射获取任意类的构造器的写法如下:

public static Constructor<?> getConstructor(Class<?> type,
                                            Class<?>... parameterTypes) 
                                            throws NoSuchMethodException {
    return type.getDeclaredConstructor(parameterTypes);
}

这种最普通的写法有一个很明显的缺点,每次调用getConstructor都需要强转才能使用

2. 加上泛型

所以我们需要加上方法泛型,给参数类型和返回值类型之间建立联系

public static <T> Constructor<T> getConstructor(Class<T> type,
                                                 Class<?>... parameterTypes) 
                                                 throws NoSuchMethodException {
    return type.getDeclaredConstructor(parameterTypes);
}

对于简单的类,已经可以使用了Constructor<String> constructor = getConstructor(String.class);
但是这种写法有一个很明显的缺点,对于本身带有泛型参数的类不适应:

Constructor<Map<String, String>> constructor = getConstructor(Map.class); 无法通过编译,因为Map.class 的类型是 Map 不是 Map<String,Stirng> 只能经过原始类型强转才能通过编译

而且由于Constructor本身也具备泛型参数,Constructor<Object> constructor = getConstructor(String.class); 也无法通过编译,传入参数和返回值之间既没有逆变也没有协变

3. 泛型协/逆变

由于存在了Constructor<T> Class<T> 两个泛型,往往需要泛型协变和逆变来让泛型边界扩大从而与函数语义契合,提高代码实用性,但是这里我们需要做出取舍:因为类型Map<String, String>extends Map 的,但在实例化的语义里,合适的类型关系是 result super type ,如果想要Map<String, String> 就会因为过于宽广的类型关系导致Constructor<String> constructor = getConstructor(Object.class);也通过编译,当然了,如果你觉得用起来方便更重要也可以忽略这一问题

方案1:只保留最稳妥的协变,不考虑Map<String, String>问题

@SuppressWarnings("unchecked")
public static <T> Constructor<T> getConstructor(Class<? extends T> type,
                                                 Class<?>... parameterTypes) 
                                                 throws NoSuchMethodException {
    return (Constructor<T>) type.getDeclaredConstructor(parameterTypes);
}

这样写Constructor<Object> constructor = getConstructor(String.class);就可以通过编译了

方案2:任意变,用就完事了

@SuppressWarnings("unchecked")
public static <T> Constructor<T> getConstructor(Class<?> type,
                                                 Class<?>... parameterTypes) 
                                                 throws NoSuchMethodException {
    return (Constructor<T>) type.getDeclaredConstructor(parameterTypes);
}

不过如果这么写,编译器永远默认推断类型是 Constructor<Object> 因为type类型参数T之间没有关系了,可以考虑使用如下写法使得默认类型推断是正确的但又不影响以任意类型接收

@SuppressWarnings("unchecked")
public static <T, X extends T> Constructor<X> getConstructor(Class<? extends T> type,
                                                              Class<?>... parameterTypes) 
                                                              throws NoSuchMethodException {
    return (Constructor<X>) type.getDeclaredConstructor(parameterTypes);
}

默认时会推断type类型参数=T=X,改变类型时,会把T推断为Object从而实现任意类型转换

4. 移除受检异常

应该没谁喜欢受检异常吧?作为工具方法,如果每次使用都需要 try-catch 是非常难受的,普通包装为 RuntimeException 写法就不提了,重新包装异常既会影响性能也会影响异常的处理和排查

方法1:添加 @SneakyThrows

@SneakyThrows
@SuppressWarnings("unchecked")
public static <T> Constructor<T> getConstructor(Class<? super T> type,
                                                 Class<?>... parameterTypes) {
    return (Constructor<T>) type.getDeclaredConstructor(parameterTypes);
}

Lombok的经典注解之一,因为Java只在编译层面检查受检异常的处理情况,但是对于字节码层面JVM本身并不校验,Lombok通过注解处理器在编译的最后阶段修改字节码就可以直接抛出受检异常

通过反编译,我们可以看到@SneakyThrows的方法实际上是:

public static Constructor getConstructor(Class type, Class... parameterTypes) {
   try {
      return type.getDeclaredConstructor(parameterTypes);
   } catch (Throwable var3) {
      Throwable $ex = var3;
      throw $ex;
   }
}

方法2:通过泛型抛出受检异常

通过在throws部分使用泛型参数,强转会让编译器误以为没有受检异常,而泛型擦除又保证了这种强转在运行时不会出错

public class Throws {
    @SuppressWarnings("unchecked")
    public static <T extends Throwable> RuntimeException sneakyThrows(Throwable throwable) throws T {
        throw (T) throwable;
    }
}

这样,就可以在不重新包装异常的情况下抛出受检异常:

@SuppressWarnings("unchecked")
public static <T> Constructor<T> getConstructor(Class<? super T> type,
                                                Class<?>... parameterTypes) {
    try {
        return (Constructor<T>) type.getDeclaredConstructor(parameterTypes);
    } catch (NoSuchMethodException e) {
        throw Throws.sneakyThrows(e);
    }
}

使用 MethodHandle 替代反射

使用反射机制调用构造函数终究是性能受限的,因为从调用反射到具体方法执行,中间会经过层层调用,这是无法跟原生的new调用构造函数相比的,想要更快的性能就必须使用 MethodHandle 这种可以直接链接到目标方法的工具,但同样,更直接的链接也意味着更少的自动转换、更少的校验,以及更严格的使用规则,接下来我们就将MethodHandle封装的比反射更快更好用吧!

5. 自定义构造器接口

既然要根据 MethodHandle 自行封装了,就没有反射的 Constructor<?> 给我们用了,第一步就是需要一个构造器接口了,为了方便调用,参数我们就用可变参数吧:

@FunctionalInterface
public interface ArgsConstructor<T> {
    T newInstance(Object... args);
}

6. 调用MethodHandle

步骤1: 获取一个 MethodHandles.Lookup 对象

MethodHandle 不同于反射的第一个点就是,反射的权限校验是在运行时进行的,而MethodHandle是在获取MethodHandle的时候进行的校验,这其中,需要获取的第一个对象就是 MethodHandles.Lookup

对于基础方案,我们可以直接使用

MethodHandles.publicLookup();

如果需要获取一些私有构造函数,可以用《摆脱 --add-opens,使用 Unsafe 突破 Java17 强封装》里的方法获取最高权限 LookUp

步骤2: 查找构造器Methodhandle

使用LookUp查找方法句柄时,需要使用MethodType作为方法的参数类型描述符来定位方法 只要注意 Java 构造函数的返回值类型是void即可

public static <T> ArgsConstructor<T> getConstructor(Class<? extends T> clazz, Class<?>... parameterTypes) {
    MethodType methodType = MethodType.methodType(void.class, parameterTypes);
    MethodHandle constructor = MethodHandles.publicLookup()
            .findConstructor(clazz, methodType);
//...
}

步骤3:转换MethodHandle到适配类型

获取到MethodHandle之后,一定要使用invokeExact来调用,否则如果存在自动类型转换性能会受到影响,但是invokeExact要求类型完全匹配,不管是参数类型还是返回值类型都必须和MethodHandle.type()完全一致,我们需要显式调整类型以匹配我们的调用

因为泛型擦除,在构造器内部调用MethodHandle时,返回值类型都是Object

修改返回值类型为 Object

.asType(methodType.changeReturnType(Object.class))

因为我们的构造器接口ArgsConstructor#newInstance(Object... args)是可变参数所以还不能直接调用

转换为数组展开方法句柄

.asSpreader(Object[].class, methodType.parameterCount())

现在我们已经得到一个可以直接调用invokeExactMethodHandle

7. 构建自定义构造器实例

直接使用Lambda方式使用MethodHandle构建ArgsConstructor是这样的:

return args -> {
    try {
        return (T) constructor.invokeExact(args);
    } catch (Throwable e) {
        throw Throws.sneakyThrows(e);
    }
};

但是显而易见由于Object invokeExact(Object... args)拥有和ArgsConstructor<Object>同样的方法签名,我们本可以直接使用性能更好的方法引用——constructor::invokeExact的,但是invokeExact抛出了受检异常,必须在Lambda内部捕获,怎么办呢?

方法1:使用 @SneakyThrows 解决方法引用形式Lambda内部受检异常

是的,虽然Java语法规定就算整体 try-catch 也必须在 Lambda 内部再次 try-catch 受检异常,但是@SneakyThrows 就是可以让方法引用形式的 Lambda 内的受检异常连同外部受检异常一起忽略并且编译通过

@SneakyThrows
@SuppressWarnings("unchecked")
public static <T> ArgsConstructor<T> getConstructor2(Class<? extends T> type, Class<?>... parameterTypes) {
    MethodType methodType = MethodType.methodType(void.class, parameterTypes);
    MethodHandle constructor = MethodHandles.publicLookup()
            .findConstructor(type, methodType)
            .asType(methodType.changeReturnType(Object.class))
            .asSpreader(Object[].class, methodType.parameterCount());
    return (ArgsConstructor<T>) (ArgsConstructor<Object>) constructor::invokeExact;
}

方法2:使用泛型异常和继承重写解决Lambda内部受检异常

虽然Lombok的黑科技很强,但也不是任何时候都可以适用的,只能用在方法引用的Lambda,如果不能写为方法引用形式,那就还是必须在 Lambda 内部 try-catch,这样不仅写起来麻烦,try-catch 本身也会轻微的影响性能,我们可以使用泛型异常变种形式来更通用的解决这个问题

修改ArgsConstructor接口

@FunctionalInterface
public interface ArgsConstructor<T> {
    <Ex extends Throwable> T newInstance(Object... args) throws Ex;

    @FunctionalInterface
    interface Lambda<T> extends ArgsConstructor<T> {
        @Override
        T newInstance(Object... args) throws Throwable;
    }
}

newInstance上定义方法泛型,并将其作为异常抛出的泛型参数使用,然后因为方法泛型的存在,ArgsConstructor<T> 不能直接以Lambda形式构造,创建一个接口的子类,因为这里方法泛型没有在方法参数上,就可以利用Java的重写规则消除掉方法泛型

使用ArgsConstructor.Lambda<T>构造构造器

@SuppressWarnings("unchecked")
public static <T> ArgsConstructor<T> getConstructor(Class<? extends T> type, Class<?>... parameterTypes) {
    try {
        MethodType methodType = MethodType.methodType(void.class, parameterTypes);
        MethodHandle constructor = MethodHandles.publicLookup()
                .findConstructor(type, methodType)
                .asType(methodType.changeReturnType(Object.class))
                .asSpreader(Object[].class, methodType.parameterCount());
        return (ArgsConstructor<T>) (ArgsConstructor.Lambda<Object>) constructor::invokeExact;
    } catch (Throwable e) {
        throw Throws.sneakyThrows(e);
    }
}

因为泛型参数作为异常抛出的参数时会优先推断为RuntimeException,所以我们通过ArgsConstructor<T>#newInstance调用构造器时也不用捕获受检异常,这样就可以不通过lombok也能跳过受检异常了,而且这样也是兼容args->constructor.invokeExact(args)这种非方法引用的写法的

8. 处理数组的构造器

因为数组对象的构造方式和普通类的构造方式不同,JDK专门提供了数组构造器MethodHandle的获取方式,我们单独处理数组类型:

if (type.isArray()) {
    if (parameterTypes != null && parameterTypes.length == 1 
        && (parameterTypes[0] == int.class || parameterTypes[0] == Integer.class))) {
        MethodHandle constructor = MethodHandles.arrayConstructor(type)
                .asType(MethodType.genericMethodType(1))
                .asSpreader(Object[].class, 1);
        return (ArgsConstructor<T>) (ArgsConstructor.Lambda<Object>) constructor::invokeExact;
    } else {
        throw new NoSuchMethodException("No such constructor");
    }
}

(如果你还想加上原始类的构造器(转换为装箱类)支持,也可以在这里加上)

9. 构造器缓存

显然,构造器是和类对象绑定的,可以通过ClassValue来缓存,避免每次调用都重新构造:

步骤1: 创建一个用于找不到构造器的占位对象

private static final ArgsConstructor.Lambda<Object> NO_SUCH_CONSTRUCTOR = 
        args -> Throws.sneakyThrows(new NoSuchMethodException("No such constructor"));

步骤2:创建ClassValue

每个类型内通过Map缓存不同的构造器

private static final ClassValue<Map<Class<?>[], ArgsConstructor<?>>> CONSTRUCTOR_MAP = new ClassValue<>() {
    @Override
    @SuppressWarnings({"unchecked", "rawtypes"})
    protected Map<Class<?>[], ArgsConstructor<?>> computeValue(Class<?> clazz) {
        return new HashMap() {
            @Override
            public ArgsConstructor<?> get(Object key) {
                var parameterTypes = (Class<?>[]) key;
                if (parameterTypes == null) {
                    parameterTypes = new Class[0];
                }
                MethodType methodType = MethodType.methodType(void.class, parameterTypes);
                var mapKey = methodType.descriptorString();
                ArgsConstructor.Lambda<?> constructorLambda = (ArgsConstructor.Lambda<?>) super.get(mapKey);

                if (constructorLambda != null) {
                    return constructorLambda;
                }
                synchronized (this) {
                    constructorLambda = (ArgsConstructor.Lambda<?>) super.get(mapKey);
                    if (constructorLambda != null) {
                        return constructorLambda;
                    }
                    // 添加原始类支持的话可以在这里添加
                    //Class<?> targetClass = clazz.isPrimitive() ? primitiveToWrapper(clazz) : clazz;
                    Class<?> targetClass = clazz;

                    if (targetClass.isArray()) {
                        if (parameterTypes.length == 1 && (parameterTypes[0] == int.class || parameterTypes[0] == Integer.class)) {
                            MethodHandle methodHandle = MethodHandles.arrayConstructor(targetClass)
                                    .asType(MethodType.genericMethodType(1))
                                    .asSpreader(Object[].class, 1);
                            constructorLambda = methodHandle::invokeExact;
                        } else {
                            constructorLambda = NO_SUCH_CONSTRUCTOR;
                        }
                    } else {
                        try {
                            var constructor = MethodHandles.publicLookup()
                                    .findConstructor(targetClass, methodType)
                                    .asType(methodType.changeReturnType(Object.class))
                                    .asSpreader(Object[].class, methodType.parameterCount());
                            constructorLambda = constructor::invokeExact;
                        } catch (NoSuchMethodException | IllegalAccessException e) {
                            constructorLambda = NO_SUCH_CONSTRUCTOR;
                        }
                    }
                    put(mapKey, constructorLambda);
                    return constructorLambda;
                }
            }
        };
    }
};

然后工具方法这样写就足够了(你也可以增加一个getConstructorOrNull的方法)

@SuppressWarnings("unchecked")
public static <T> ArgsConstructor<T> getConstructor(Class<? extends T> clazz, Class<?>... parameters) {
    var argsConstructor = (ArgsConstructor<T>) CONSTRUCTOR_MAP.get(clazz).get(parameters);
    if (argsConstructor == NO_SUCH_CONSTRUCTOR) {
        throw Throws.sneakyThrows(new NoSuchMethodException("No such constructor"));
    }
    return argsConstructor;
}

10. 利用LambdaMetafactory.metafactory创建更快的构造器

什么样的构造器比MethodHandle#invokeExact更快呢?当然是new了,众所周知Java Lambda是利用INVOKEDYNAMIC调用LambdaMetafactory.metafactory来实现的,会在第一次运行时使用字节码生成一个class作为我们的Lambda对象的实际类

所以我们也可以利用LambdaMetafactory.metafactory帮助我们生成字节码,实现和原生new一样的性能,并且比利用直接用ASM等工具字节码生成来的更方便和依赖更少

步骤1:创建工具类的MethodHandles.Lookup对象

private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();

如果用《摆脱 --add-opens,使用 Unsafe 突破 Java17 强封装》里的方法的话可以用JUnsafe.getLookUp(/*你的工具类class*/InstanceUtil.class)替代

步骤2:展平可变参数

LambdaMetafactory.metafactory是存在很多限制的,比如说,因为它会校验参数数量是否对齐,可变参数就无法使用了,我们需要创建一些子类来展平可变参数

利用如下方法可以生成任意数量的展平类

public static void main(String[] args) {
    // Example usage: generate interfaces
    System.out.println(generateLambdaInterfaces(10));
}

@SuppressWarnings("SameParameterValue")
private static String generateLambdaInterfaces(int n) {
    return IntStream.rangeClosed(0, n)
            .mapToObj(i -> String.format(
                    """
                            @FunctionalInterface
                            interface $Lambda$%d<T> extends ArgsConstructor.Lambda<T> {
                                @Override
                                default T newInstance(Object... args) throws Throwable {
                                    return this.newInstance$(%s);
                                }

                                T newInstance$(%s) throws Throwable;
                            }
                            """,
                    i,
                    generateArguments(i, "args[%d]"),
                    generateArguments(i, "Object arg%d")
            ))
            .collect(Collectors.joining());
}

private static String generateArguments(int n, String format) {
    return IntStream.range(0, n)
            .mapToObj(i -> String.format(format, i))
            .collect(Collectors.joining(", "));
}

一般来说0-10个参数的类就够用了,将这些类放置在工具类内部使用,不需要外部可见

步骤3:检索和存储展平类

将类型存储为Map,方便我们后面调用

private static final Map<Integer, Class<?>> LAMBDAS;

static {
    Map<Integer, Class<?>> lambdas = new HashMap<>();
    Class<?>[] declaredClasses = LOOKUP.lookupClass().getDeclaredClasses();
    for (Class<?> declaredClass : declaredClasses) {
        if (declaredClass.isInterface() && ArgsConstructor.class.isAssignableFrom(declaredClass)) {
            lambdas.put(Integer.parseInt(declaredClass.getSimpleName().split("\\$")[2]), declaredClass);
        }
    }
    LAMBDAS = Collections.unmodifiableMap(lambdas);
}

步骤4:调用LambdaMetafactory.metafactory

// 利用 Lambda 机制构建构造器(本质上是字节码生成)
Class<?> lambdaClass = LAMBDAS.get(parameterTypes.length);
if (lambdaClass != null) {
    //noinspection SpellCheckingInspection
    try {
        MethodType boxedMethodType = methodType.wrap();//装箱原始类
        //此处的LOOKUP 可以替换为 JUnsafe.IMPL_LOOKUP
        var constructor = LOOKUP.findConstructor(targetClass, methodType);
        constructorLambda = (ArgsConstructor.Lambda<?>) LambdaMetafactory.metafactory(
                LOOKUP,//此处的 LOOKUP 需要同时能查找到目标类和展平的接口类
                "newInstance$",
                MethodType.methodType(lambdaClass),// factory method type (return type(args?) )
                boxedMethodType.generic(),//functionalInterfaceMethodType (in LAMBDAS)
                constructor,//lambdaBody
                boxedMethodType//lambdaBodyMethodType
        ).getTarget().invoke();//只调用一次,就不需要asType了
    } catch (NoSuchMethodException | IllegalAccessException e) {
        constructorLambda = NO_SUCH_CONSTRUCTOR;
    } catch (Throwable ignore) {
        //可能是目标类和接口类不是一个类加载器, LambdaMetafactory 要求传递的 Lookup 的 lookupClass 的 ClassLoader 必须能同时查找到目标类和接口类
    }
}

这里有几个注意点,由于LambdaMetafactory.metafactory不会像MethodHandle那样自动装箱,所以需要执行.wrap();因为LambdaMetafactory.metafactory要求caller(第一个参数的LookUp)能够同时加载目标类和实现的接口类,如果ClassLoader不兼容,我们就回退到MethodHandle

11. 接口增加便利方法:

@FunctionalInterface
public interface ArgsConstructor<T> {
    /**
     * 此方法含有方法泛型不能直接Lambda构造,调用时异常类型默认推断为 RuntimeException 所以不需要try-catch
     *
     * @param args 构造函数参数
     * @return 新实例
     */
    <Ex extends Throwable> T newInstance(Object... args) throws Ex;

    /**
     * 便利方法,减少类型转换警告
     * <pre>{@code
     *      ArgsConstructor<Map<String, String>> constructor = InstanceUtil.getConstructor(LinkedHashMap.class, int.class).as();//没有警告
     * }</pre>
     */
    @SuppressWarnings("unchecked")
    default <X> ArgsConstructor<X> as() {
        return (ArgsConstructor<X>) this;
    }

    /**
     * 便利方法,减少类型转换警告
     * <pre>{@code
     *      Map<String, Object> map = InstanceUtil.getConstructor(LinkedHashMap.class, int.class).newInstanceAs(12);//没有警告
     * }</pre>
     */
    @SuppressWarnings("unchecked")
    default <X> X newInstanceAs(Object... args) {
        return (X) newInstance(args);
    }

    @FunctionalInterface
    interface Lambda<T> extends ArgsConstructor<T> {
        @Override
        T newInstance(Object... args) throws Throwable;
    }
}

观察生成的代码:

我们可以通过设置-Djdk.internal.lambda.dumpProxyClasses=临时文件路径来观察Lambda类,将class文件反编译

我们可以看到如下构造器:

ArgsConstructor<Map<String, String>> constructor = InstanceUtil.getConstructor(LinkedHashMap.class, int.class).as();
ArgsConstructor<Object> constructor2 = InstanceUtil.getConstructor(String.class);

生成的代码为:

final class InstanceUtil$$Lambda$512 implements InstanceUtil..Lambda.1 {
   private InstanceUtil$$Lambda$512() {
   }

   public Object newInstance$(Object var1) {
      return new LinkedHashMap((Integer)var1);
   }
}
final class InstanceUtil$$Lambda$513 implements InstanceUtil..Lambda.0 {
   private InstanceUtil$$Lambda$513() {
   }

   public Object newInstance$() {
      return new String();
   }
}

这样我们就获得一个运行应该是速度最快一级别的构造器工具了!🎉🎉🎉

完整代码:

Github : JUnsafe
Github : InstanceUtil