开发者

Type-safe method reflection in Java

开发者 https://www.devze.com 2022-12-15 08:15 出处:网络
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:

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 (produ开发者_运维百科ction grade) pre-compiler or alternative compiler that may offer a work around? (similar to how AspectJ extends the Java language) It would be nice to do something like the following instead:

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.

Imagine you have a utility method for looking up Method objects that maybe throws error or runtime exception:

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.

I use reflection a lot, especially bean property reflection and I've just gotten used to late exceptions at runtime. But that style of bean code tends to error late for all kinds of other reasons, being very dynamic and all. 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.

I don't have any suggestions beyond that, I'm afraid.


Check out https://proxetta.jodd.org/refs/methref. It uses the Jodd proxy library (Proxetta) to proxy your type. 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":

String methodName = Methref.of(Str.class).name(Str::boo);                              

There's more to the API than the example above: https://oblac.github.io/jodd-site/javadoc/jodd/methref/Methref.html


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)) {
    m = obj.getClass().getDeclaredMethod("foo", ...);
} else {
    m = 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. (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.)

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.

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. 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. I.E. Person.class.getMethod("getPhoneNumber", null) would always return the same method and it's entirely possible to resolve it at compile time.

What happens if after compiling the class containing this code, you change Person to remove the getPhoneNumber method?

The only way you can be sure that you can resolve getPhoneNumber reflectively is if you can somehow prevent Person from being changed. But you can't do that in 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 ...)


It has been pointed out that in Java 8 and later you could declare your validator something like this:

public Result validateField(Object data, 
                            SomeFunctionalInterface function, 
                            ValidationOptions options) { ... }

where SomeFunctionalInterface corresponds to the (loosely speaking) common signature of the methods you are validating.

Then you can call it with a method reference; e.g.

validateField(data, SomeClass::someMethod, options)

This is approach is statically type-safe. You will get a compilation error if SomeClass doesn't have someMethod or if it doesn't conform to SomeFunctionalInterface.

But you can't use a string to denote the method name. Looking up a method by name would entail either reflection ... or something else that side-steps static (i.e. compile time / load time) type safety.


Java misses the syntax sugar to do something as nice as Person.phoneNumber.getter. But if Person is an interface, you could record the getter method using a dynamic proxy. You could record methods on non-final classes as well using CGLib, the same way Mockito does it.

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


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.

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.

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.


The framework picklock lets you do the following:

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


I found a way to get the Method instance using Lambdas. It works only on interface methods though currently.

It works using net.jodah:typetools which is a very lightweight library. 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.

However, if you have a somewhat fixed/limited method signature/parameter types, then this becomes quite handy.

0

精彩评论

暂无评论...
验证码 换一张
取 消