【问题标题】:Type-safe method reflection in JavaJava中的类型安全方法反射
【发布时间】:2021-02-04 22:43:17
【问题描述】:

是否有任何实用的方法可以以类型安全的方式引用类上的方法?一个基本的例子是,如果我想创建类似以下实用函数的东西:

public Result validateField(Object data, String fieldName, 
                            ValidationOptions options) { ... }

为了调用它,我必须这样做:

validateField(data, "phoneNumber", options);

这迫使我要么使用魔法字符串,要么使用该字符串在某处声明一个常量。

我很确定没有办法使用现有的 Java 语言来解决这个问题,但是是否有某种(生产级)预编译器或替代编译器可以解决这个问题? (类似于 AspectJ 扩展 Java 语言的方式)最好执行以下操作:

public Result validateField(Object data, Method method, 
                            ValidationOptions options) { ... }

然后调用它:

validateField(data, Person.phoneNumber.getter, options);

【问题讨论】:

  • 这是对反射的常见抱怨——也是尽可能避免反射的一个很好的理由。作为一个必须对我从事的项目之一进行大量反思的人,我感受到了你的痛苦。
  • 我认为“类型安全”这个词有点错误。使用反射@java 是类型安全的(在运行时)。类型错误只是不会在编译过程中提前弹出。
  • 为什么不将 Function 作为 lambda 传递给它?

标签: java reflection compiler-construction


【解决方案1】:

正如其他人所提到的,没有真正的方法可以做到这一点......而且我还没有看到支持它的预编译器。至少可以说,语法会很有趣。即使在您的示例中,它也只能涵盖用户可能想要做的潜在反射可能性的一小部分,因为它不会处理非标准访问器或带参数的方法等。

即使无法在编译时检查,如果您希望坏代码尽快失败,那么一种方法是在类初始化时解析引用的 Method 对象。

假设您有一个实用方法来查找可能引发错误或运行时异常的 Method 对象:

public static Method lookupMethod( Class c, String name, Class... args ) {
    // do the lookup or throw an unchecked exception of some kind with a really
    // good error message
}

然后在您的类中,使用常量来预解析您将使用的方法:

public class MyClass {
    private static final Method GET_PHONE_NUM = MyUtils.lookupMethod( PhoneNumber.class, "getPhoneNumber" );

    ....

    public void someMethod() {
        validateField(data, GET_PHONE_NUM, options);
    }
}

至少在第一次加载 MyClass 时它会失败。

我经常使用反射,尤其是 bean 属性反射,而且我刚刚习惯了运行时的延迟异常。但是由于各种其他原因,这种 bean 代码风格往往会迟到出错,而且非常动态。对于介于两者之间的东西,以上内容会有所帮助。

【讨论】:

  • 这似乎是个好主意。这比尝试使用我在维护的代码中看到的字段名称来定义字符串常量要好。
  • 是的,这是一种“充分利用它”。不知道谁/为什么我得到了某人的反对票。 :) 在没有评论的情况下投票总是很有趣。
  • 这种方法的另一个好处是当/如果语言获得方法文字,那么它可能是一个简单的转换。
【解决方案2】:

语言中还没有任何东西 - 但我相信 Java 7 的闭包提案的一部分包括方法文字。

恐怕除此之外我没有任何建议。

【讨论】:

    【解决方案3】:

    查看https://jodd.org/ref/methref.html。它使用 Jodd 代理库 (Proxetta) 来代理您的类型。不确定它的性能特征,但它确实提供了类型安全性。

    举个例子:假设Str.class有方法.boo(),你想得到它的名字作为字符串"boo"

    Methref<Str> m = Methref.on(Str.class);
    
    // `.to()` returns a proxied instance of `Str` upon which you
    // can call `.boo()` Methods on this proxy are empty except when
    // you call them, the proxy stores the method's name. So doing this
    // gets the proxy to store the name `"boo"`.
    
    m.to().boo();
    
    // You can get the name of the method you called by using `.ref()`:
    
    m.ref();   // returns "boo"                                 
    

    除了上面的示例之外,API 还包含更多内容:https://oblac.github.io/jodd-site/javadoc/jodd/methref/Methref.html

    【讨论】:

      【解决方案4】:

      Java 缺少语法糖来做像Person.phoneNumber.getter 这样好的事情。但是如果 Person 是一个接口,您可以使用动态代理记录 getter 方法。您也可以使用 CGLib 在非最终类上记录方法,就像 Mockito 一样。

      MethodSelector<Person> selector = new MethodSelector<Person>(Person.class);
      selector.select().getPhoneNumber();
      validateField(data, selector.getMethod(), options);
      

      MethodSelector 的代码:https://gist.github.com/stijnvanbael/5965609

      【讨论】:

        【解决方案5】:

        是否有任何实用的方法可以以类型安全的方式引用类上的方法?

        首先,反射类型安全的。只是它是动态类型的,不是静态类型的。

        所以,假设您想要一个 静态类型 等效于反射,理论上的答案是这是不可能的。考虑一下:

        Method m;
        if (arbitraryFunction(obj)) {
            obj.getClass().getDeclaredMethod("foo", ...);
        } else {
            obj.getClass().getDeclaredMethod("bar", ...);
        }
        

        我们可以这样做以使运行时类型异常不会发生吗?一般不会,因为这需要证明arbitraryFunction(obj) 终止。 (这相当于停止问题,它被证明通常是无法解决的,并且使用最先进的定理证明技术是难以解决的...... AFAIK。)

        而且我认为这个障碍适用于任何可以将任意 Java 代码注入到用于从对象的类中反射性地选择方法的逻辑的方法。

        在我看来,目前唯一适度实用的方法是将反射代码替换为生成和编译 Java 源代码的东西。如果此过程发生在您“运行”应用程序之前,您就满足了静态类型安全的要求。


        我更多地询问结果总是相同的反射。 IE。 Person.class.getMethod("getPhoneNumber", null) 总是会返回相同的方法,并且完全可以在编译时解决它。

        如果在编译包含此代码的类之后,您更改 Person 以删除getPhoneNumber 方法会怎样?

        您可以确定您可以反思地解决getPhoneNumber 的唯一方法是您是否可以以某种方式防止 Person 被更改。但是你不能在 Java 中做到这一点。类的运行时绑定是该语言的基本部分。

        (作为记录,如果您为非反射调用的方法执行此操作,则在加载两个类时会得到某种IncompatibleClassChangeError ...)

        【讨论】:

        • 感谢您对术语的更正。我不确定您的其余答案与我的问题有何关系。如果您在运行时动态地进行反射(即反射的结果可能会因输入或其他运行时状态而异),那么是的,您可能无法确保代码不会抛出异常或它会停止。
        • 我更多的是询问结果始终相同的反射。 IE。 Person.class.getMethod("getPhoneNumber", null) 将始终返回相同的方法,并且完全有可能在编译时解决它。就像你可以通过 Person.class 来获取 Class 对象一样,能够通过 Person.getPhoneNumber.method 来获取 Method 对象会很有帮助。
        【解决方案6】:

        受模拟框架的启发,我们可以构想出以下语法:

        validator.validateField(data, options).getPhoneNumber();
        Result validationResult = validator.getResult();
        

        诀窍是泛型声明:

        class Validator {
            public <T> T validateField(T data, options) {...}
        }
        

        现在方法的返回类型与数据对象的类型相同,您可以使用代码完成(和静态检查)来访问所有方法,包括 getter 方法。

        不利的一面是,代码读起来不太直观,因为对 getter 的调用实际上并没有得到任何东西,而是指示验证器验证字段。

        另一种可能的选择是注释数据类中的字段:

        class FooData {
            @Validate(new ValidationOptions(...))
            private PhoneNumber phoneNumber;
        }
        

        然后只需调用:

        FooData data;
        validator.validate(data);
        

        根据注释选项验证所有字段。

        【讨论】:

          【解决方案7】:

          框架picklock 允许您执行以下操作:

          class Data {
            private PhoneNumber phoneNumber;
          }
          
          interface OpenData {
            PhoneNumber getPhoneNumber(); //is mapped to the field phoneNumber
          }
          
          Object data = new Data();
          PhoneNumber number = ObjectAccess
            .unlock(data)
            .features(OpenData.class)
            .getPhoneNumber();
          

          这与 setter 和私有方法的工作方式类似。当然,这只是对反射的一个包装器,但是在解锁时不会发生异常而不是在调用时发生异常。如果您在构建时需要它,您可以编写一个单元测试:

           assertThat(Data.class, providesFeaturesOf(OpenData.class));
          

          【讨论】:

            【解决方案8】:

            我找到了一种使用 Lambda 获取 Method 实例的方法。它目前仅适用于接口方法。

            它使用net.jodah:typetools 工作,这是一个非常轻量级的库。 https://github.com/jhalterman/typetools

            public final class MethodResolver {
            
                private interface Invocable<I> {
            
                    void invokeWithParams(I instance, Class<?>[] parameterTypes) throws Throwable;
            
                }
            
                interface ZeroParameters<I, R> extends Invocable<I> {
            
                    R invoke(I instance) throws Throwable;
            
                    @Override
                    default void invokeWithParams(I instance, Class<?>[] parameterTypes) throws Throwable {
                        invoke(instance);
                    }
            
                }
            
                public static <I, R> Method toMethod0(ZeroParameters<I, R> call) {
                    return toMethod(ZeroParameters.class, call, 1);
                }
            
                interface OneParameters<I, P1, R> extends Invocable<I> {
            
                    R invoke(I instance, P1 p1) throws Throwable;
            
                    @Override
                    default void invokeWithParams(I instance, Class<?>[] parameterTypes) throws Throwable {
                        invoke(instance, param(parameterTypes[1]));
                    }
            
                }
            
                public static <I, P1, R> Method toMethod1(OneParameters<I, P1, R> call) {
                    return toMethod(OneParameters.class, call, 2);
                }
            
                interface TwoParameters<I, P1, P2, R> extends Invocable<I> {
            
                    R invoke(I instance, P1 p1, P2 p2) throws Throwable;
            
                    @Override
                    default void invokeWithParams(I instance, Class<?>[] parameterTypes) throws Throwable {
                        invoke(instance, param(parameterTypes[1]), param(parameterTypes[2]));
                    }
            
                }
            
                public static <I, P1, P2, R> Method toMethod2(TwoParameters<I, P1, P2, R> call) {
                    return toMethod(TwoParameters.class, call, 3);
                }
            
                private static final Map<Class<?>, Object> parameterMap = new HashMap<>();
            
                static {
                    parameterMap.put(Boolean.class, false);
                    parameterMap.put(Byte.class, (byte) 0);
                    parameterMap.put(Short.class, (short) 0);
                    parameterMap.put(Integer.class, 0);
                    parameterMap.put(Long.class, (long) 0);
                    parameterMap.put(Float.class, (float) 0);
                    parameterMap.put(Double.class, (double) 0);
                }
            
                @SuppressWarnings("unchecked")
                private static <T> T param(Class<?> type) {
                    return (T) parameterMap.get(type);
                }
            
                private static <I> Method toMethod(Class<?> callType, Invocable<I> call, int responseTypeIndex) {
                    Class<?>[] typeData = TypeResolver.resolveRawArguments(callType, call.getClass());
                    Class<?> instanceClass = typeData[0];
                    Class<?> responseType = responseTypeIndex != -1 ? typeData[responseTypeIndex] : Void.class;
            
                    AtomicReference<Method> ref = new AtomicReference<>();
            
                    I instance = createProxy(instanceClass, responseType, ref);
            
                    try {
                        call.invokeWithParams(instance, typeData);
                    } catch (final Throwable e) {
                        throw new IllegalStateException("Failed to call no-op proxy", e);
                    }
            
                    return ref.get();
                }
            
                @SuppressWarnings("unchecked")
                private static <I> I createProxy(Class<?> instanceClass, Class<?> responseType,
                        AtomicReference<Method> ref) {
                    return (I) Proxy.newProxyInstance(MethodResolver.class.getClassLoader(),
                            new Class[] {instanceClass},
                            (proxy, method, args) -> {
                                ref.set(method);
                                return parameterMap.get(responseType);
                            });
                }
            
            }
            

            用法:

            Method method = MethodResolver.toMethod2(SomeIFace::foobar);
            System.out.println(method); // public abstract example.Result example.SomeIFace.foobar(java.lang.String,boolean)
            
            Method get = MethodResolver.<Supplier, Object>toMethod0(Supplier::get);
            System.out.println(get); // public abstract java.lang.Object java.util.function.Supplier.get()
            
            Method accept = MethodResolver.<IntFunction, Integer, Object>toMethod1(IntFunction::apply);
            System.out.println(accept); // public abstract java.lang.Object java.util.function.IntFunction.apply(int)
            
            Method apply = MethodResolver.<BiFunction, Object, Object, Object>toMethod2(BiFunction::apply);
            System.out.println(apply); // public abstract java.lang.Object java.util.function.BiFunction.apply(java.lang.Object,java.lang.Object)
            

            不幸的是,您必须根据参数计数以及方法是否返回 void 来创建新的接口和方法。

            但是,如果您有一些固定/有限的方法签名/参数类型,那么这将变得非常方便。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2013-01-02
              • 1970-01-01
              • 2021-06-24
              • 2023-03-27
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多