繁体   English   中英

Java 中的类型安全方法反射

[英]Type-safe method reflection in Java

以类型安全的方式引用类上的方法有什么实用的方法吗? 一个基本示例是,如果我想创建类似以下实用程序函数的内容:

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);

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

即使在编译时无法检查,如果您希望错误代码尽快失败,那么一种方法是在类初始化时解析引用的 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 代码由于各种其他原因往往会出错,而且非常动态。 对于介于两者之间的内容,上述内容会有所帮助。

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

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

查看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

Java Person.phoneNumber.getter语法糖来做一些像Person.phoneNumber.getter一样好的Person.phoneNumber.getter 但是如果 Person 是一个接口,您可以使用动态代理记录 getter 方法。 您也可以使用 CGLib 记录非 final 类的方法,就像 Mockito 那样。

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

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

以类型安全的方式引用类上的方法有什么实用的方法吗?

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

因此,假设您想要一个静态类型的反射等价物,理论上的答案是这是不可能的。 考虑一下:

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 ...)

框架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));

使用 Manifold 的@Jailbreak进行编译时对私有字段、方法等的类型安全访问。

@Jailbreak Foo foo = new Foo();
foo.privateMethod();
foo.privateMethod("hey");
foo._privateField = 88;

public class Foo {
  private final int _privateField;

  public Foo(int value) {
    _privateField = value;
  }

  private String privateMethod() {
    return "hi";
  }

  private String privateMethod(String param) {
    return param;
  }
}

了解更多:类型安全反射

我找到了一种使用 Lambda 获取Method实例的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 来创建新的接口和方法。

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

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

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);

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM