簡體   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