简体   繁体   English

Java 中的类型安全方法反射

[英]Type-safe method reflection in Java

Is any practical way to reference a method on a class in a type-safe manner?以类型安全的方式引用类上的方法有什么实用的方法吗? A basic example is if I wanted to create something like the following utility function:一个基本示例是,如果我想创建类似以下实用程序函数的内容:

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

In order to call it, I would have to do:为了调用它,我必须这样做:

validateField(data, "phoneNumber", options);

Which forces me to either use a magic string, or declare a constant somewhere with that string.这迫使我要么使用魔法字符串,要么使用该字符串在某处声明一个常量。

I'm pretty sure there's no way to get around that with the stock Java language, but is there some kind of (production grade) pre-compiler or alternative compiler that may offer a work around?我很确定用现有的 Java 语言无法解决这个问题,但是是否有某种(生产级)预编译器或替代编译器可以提供解决方法? (similar to how AspectJ extends the Java language) It would be nice to do something like the following instead: (类似于 AspectJ 扩展 Java 语言的方式)最好改为执行以下操作:

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

And call it with:并调用它:

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

As others mention, there is no real way to do this... and I've not seen a precompiler that supports it.正如其他人提到的,没有真正的方法可以做到这一点……而且我还没有看到支持它的预编译器。 The syntax would be interesting, to say the least.至少可以说,语法会很有趣。 Even in your example, it could only cover a small subset of the potential reflective possibilities that a user might want to do since it won't handle non-standard accessors or methods that take arguments, etc..即使在您的示例中,它也只能涵盖用户可能想要做的潜在反射可能性的一小部分,因为它不会处理带参数的非标准访问器或方法等。

Even if it's impossible to check at compile time, if you want bad code to fail as soon as possible then one approach is to resolve referenced Method objects at class initialization time.即使在编译时无法检查,如果您希望错误代码尽快失败,那么一种方法是在类初始化时解析引用的 Method 对象。

Imagine you have a utility method for looking up Method objects that maybe throws error or runtime exception:想象一下,您有一个实用方法来查找可能会引发错误或运行时异常的 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
}

Then in your classes, have constants to preresolve the methods you will use:然后在您的类中,使用常量来预先解析您将使用的方法:

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

    ....

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

At least then it will fail as soon as MyClass is loaded the first time.至少,一旦第一次加载 MyClass,它就会失败。

I use reflection a lot, especially bean property reflection and I've just gotten used to late exceptions at runtime.我经常使用反射,尤其是 bean 属性反射,而且我刚刚习惯了运行时的后期异常。 But that style of bean code tends to error late for all kinds of other reasons, being very dynamic and all.但是这种风格的 bean 代码由于各种其他原因往往会出错,而且非常动态。 For something in between, the above would help.对于介于两者之间的内容,上述内容会有所帮助。

There isn't anything in the language yet - but part of the closures proposal for Java 7 includes method literals, I believe.语言中没有任何东西 - 但我相信 Java 7 的闭包提案的一部分包括方法文字。

I don't have any suggestions beyond that, I'm afraid.恐怕除此之外我没有任何建议。

Check out https://jodd.org/ref/methref.html .查看https://jodd.org/ref/methref.html It uses the Jodd proxy library (Proxetta) to proxy your type.它使用 Jodd 代理库 (Proxetta) 来代理您的类型。 Not sure about its performance characteristics, but it does provide type safety.不确定它的性能特征,但它确实提供了类型安全。

An example: Suppose Str.class has method .boo() , and you want to get its name as the string "boo" :一个例子:假设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"                                 

There's more to the API than the example above: https://oblac.github.io/jodd-site/javadoc/jodd/methref/Methref.html API 比上面的例子更多: https : //oblac.github.io/jodd-site/javadoc/jodd/methref/Methref.html

Java misses the syntax sugar to do something as nice as Person.phoneNumber.getter . Java Person.phoneNumber.getter语法糖来做一些像Person.phoneNumber.getter一样好的Person.phoneNumber.getter But if Person is an interface, you could record the getter method using a dynamic proxy.但是如果 Person 是一个接口,您可以使用动态代理记录 getter 方法。 You could record methods on non-final classes as well using CGLib, the same way Mockito does it.您也可以使用 CGLib 记录非 final 类的方法,就像 Mockito 那样。

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

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

Is any practical way to reference a method on a class in a type-safe manner?以类型安全的方式引用类上的方法有什么实用的方法吗?

First of all, reflection is type-safe.首先,反射类型安全的。 It is just that it is dynamically typed, not statically typed.只是它是动态类型的,而不是静态类型的。

So, assuming that you want a statically typed equivalent of reflection, the theoretical answer is that it is impossible.因此,假设您想要一个静态类型的反射等价物,理论上的答案是这是不可能的。 Consider this:考虑一下:

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

Can we do this so that that runtime type exceptions cannot happen?我们可以这样做,以便不会发生运行时类型异常吗? In general NO, since this would entail proving that arbitraryFunction(obj) terminates.一般不会,因为这需要证明arbitraryFunction(obj)终止。 (This is equivalent to the Halting Problem, which is proven to be unsolvable in general, and is intractable using state-of-the-art theorem proving technology ... AFAIK.) (这相当于停机问题,它被证明通常无法解决,并且使用最先进的定理证明技术难以解决......AFAIK。)

And I think that this road-block would apply to any approach where you could inject arbitrary Java code into the logic that is used to reflectively select a method from an object's class.而且我认为这个障碍适用于任何可以将任意 Java 代码注入到用于从对象的类中反射选择方法的逻辑中的方法。

To my mind, the only moderately practical approach at the moment would be to replace the reflective code with something that generates and compiles Java source code.在我看来,目前唯一比较实用的方法是用生成和编译 Java 源代码的东西替换反射代码。 If this process occurs before you "run" the application, you've satisfied the requirement for static type-safety.如果这个过程发生在你“运行”应用程序之前,你就满足了静态类型安全的要求。


I was more asking about reflection in which the result is always the same.我更多地询问结果总是相同的反射。 IE Person.class.getMethod("getPhoneNumber", null) would always return the same method and it's entirely possible to resolve it at compile time. IE Person.class.getMethod("getPhoneNumber", null)将始终返回相同的方法,并且完全有可能在编译时解析它。

What happens if after compiling the class containing this code, you change Person to remove the getPhoneNumber method?如果在编译包含此代码的类后,更改Person以删除getPhoneNumber方法,会发生什么情况?

The only way you can be sure that you can resolve getPhoneNumber reflectively is if you can somehow prevent Person from being changed.您可以确保可以反射性地解析getPhoneNumber的唯一方法是,您是否可以以某种方式防止Person被更改。 But you can't do that in Java.但是你不能在 Java 中做到这一点。 Runtime binding of classes is a fundamental part of the language.类的运行时绑定是该语言的基本部分。

(For record, if you did that for a method that you called non-reflectively, you would get an IncompatibleClassChangeError of some kind when the two classes were loaded ...) (作为记录,如果您对非反射调用的方法执行此操作,则在加载两个类时会得到某种类型的IncompatibleClassChangeError ...)

The framework picklock lets you do the following:框架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();

This works in a similar way setters and private methods.这与 setter 和私有方法的工作方式类似。 Of course, this is only a wrapper for reflection, but the exception does not occur at unlocking time not at call time.当然,这只是反射的一个包装器,但异常不会发生在解锁时而不是调用时。 If you need it at build time, you could write a unit test with:如果您在构建时需要它,您可以编写一个单元测试:

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

Use Manifold's @Jailbreak for compile-time type-safe access to private fields, methods, etc.使用 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;
  }
}

Learn more: type-safe-reflection了解更多:类型安全反射

I found a way to get the Method instance using Lambdas.我找到了一种使用 Lambda 获取Method实例的Method It works only on interface methods though currently.尽管目前它仅适用于接口方法。

It works using net.jodah:typetools which is a very lightweight library.它使用net.jodah:typetools ,这是一个非常轻量级的库。 https://github.com/jhalterman/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);
                });
    }

}

Usage:用法:

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)

Unfortunately you have to create a new interface and method based on the parameter count and whether the method returns void or not.不幸的是,您必须根据参数计数以及该方法是否返回 void 来创建新的接口和方法。

However, if you have a somewhat fixed/limited method signature/parameter types, then this becomes quite handy.但是,如果您有一些固定/有限的方法签名/参数类型,那么这将变得非常方便。

Inspired by mocking frameworks, we could dream up the following syntax:受模拟框架的启发,我们可以想出以下语法:

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

The trick is the generic declaration:诀窍是通用声明:

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

Now the return type of the method is the same as your data object's type and you can use code completion (and static checking) to access all the methods, including the getter methods.现在该方法的返回类型与您的数据对象的类型相同,您可以使用代码完成(和静态检查)来访问所有方法,包括 getter 方法。

As a downside, the code isn't quite intuitive to read, since the call to the getter doesn't actually get anything, but instead instructs the validator to validate the field.不利的一面是,代码读起来不是很直观,因为对 getter 的调用实际上并没有得到任何东西,而是指示验证器验证该字段。

Another possible option would be to annotate the fields in your data class:另一种可能的选择是注释数据类中的字段:

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

And then just call:然后只需调用:

FooData data;
validator.validate(data);

to validate all fields according to the annotated options.根据注释选项验证所有字段。

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

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