[英]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.