简体   繁体   English

类编译器的 Java 反射方法解析

[英]Compiler-like Java Reflection method resolution

I'm programming a method that uses Reflection to find methods the "same way" (not exactly, as you will see) the compiler does.我正在编写一种方法,该方法使用反射来查找编译器所做的“相同方式”(不完全是,正如您将看到的那样)的方法。 I left Generics out of the scope in this case, as the type parameter does not affect signature of methods (as far as I know).在这种情况下,我将泛型排除在范围之外,因为类型参数不影响方法的签名(据我所知)。

Basically I want to ask how far are my methods from accomplishing the task in a relatively acceptable way (I don't expect perfect and full resolution, I don't even need static methods).基本上我想问一下我的方法离以相对可接受的方式完成任务有多远(我不期望完美和完整的分辨率,我什至不需要静态方法)。

I could not find the exact spects about how does the compiler actually perform the task, so I made a guess that suits the needs of my environment , however, if possible and not too complex, I'd like to have it done in the proper way for possible complex future uses.我找不到关于编译器如何实际执行任务的确切方面,所以我做了一个适合我环境需要的猜测,但是,如果可能并且不太复杂,我希望它以适当的方式完成未来可能的复杂用途。

Right now, my assumption is IGNORING overloaded methods with different return types, as it is not a problem with my current problem, but that may be in the future, so I may be interested in taking it into consideration if you have ideas.现在,我的假设是忽略具有不同返回类型的重载方法,因为这不是我当前问题的问题,但将来可能会出现,因此如果您有想法,我可能有兴趣将其考虑在内。

So, as I don't have to care about return types, I start by calling a simple所以,由于我不必关心返回类型,我首先调用一个简单的

clazz.getMethods();

and then I perform a filter by name.然后我按名称执行过滤器。 I'm aware I'm totally missing the overrides mentioned abvove here.我知道我完全错过了这里提到的覆盖。 This is a first approach.这是第一种方法。

This is how I calculate the distance between a method and the desired arguments: If parent and children are the same instance, then the distance is 0. If not, this methods calls classDistance recursively on children superclass and all directly implemented interfaces.这就是我如何计算方法和所需参数之间的距离:如果父和子是同一个实例,则距离为 0。如果不是,则此方法在子超类和所有直接实现的接口上递归调用 classDistance。 The distance will be the smalles positive distance plus one.距离将是最小的正距离加一。 (ignoring incompatible ancestors). (忽略不兼容的祖先)。 This solution works for me because right now all the functions I need to call have just one parameter, and the few that have a second one, allways do a perfect match on first parameter on the desired method, so there's only one positive distance to narrow down.这个解决方案对我有用,因为现在我需要调用的所有函数只有一个参数,而少数有第二个参数,总是对所需方法的第一个参数进行完美匹配,因此只有一个正距离可以缩小下。

Actual code:实际代码:

private static Method getBestMatch(List<Method> validMethods, List<Class<?>> classes) {
    if(validMethods.size() == 1) return validMethods.get(0);
    int distance = Integer.MAX_VALUE;
    Method currMethod = null;
    outer_loop:
    for(Method method : validMethods) {
        Class<?>[] methodTypes = method.getParameterTypes();
        int methodDistance = 0;
        for(int i=0; i < methodTypes.length; i++) {
            if(!methodTypes[i].isAssignableFrom(classes.get(i))) continue outer_loop; // Incompatible. Should not happen, but just in case
            methodDistance += classDistance(methodTypes[i], classes.get(i));

        }
        if(currMethod == null || methodDistance < distance) {
            currMethod = method;
            distance = methodDistance;
        }
    }
    return currMethod;
}

Distance calculator:距离计算器:

private static int classDistance(Class<?> parent, Class<?> children) throws IllegalArgumentException{
    if(parent.equals(children)) return 0;
    if(!parent.isAssignableFrom(children)) throw new IllegalArgumentException("children is not assignable to father"); // Should do b4 equals?
    Integer minDistance = null;

    Class<?> superClass = children.getSuperclass();
    if(superClass != null && parent.isAssignableFrom(superClass)) {
        minDistance = classDistance(parent, superClass);
    }
    for(Class<?> directInterface : children.getInterfaces()) {
        if(!parent.isAssignableFrom(directInterface)) continue;
        int interfaceDistance = classDistance(parent, directInterface);
        if(interfaceDistance < minDistance)  minDistance = interfaceDistance;
    }

    if(minDistance == null) throw new IllegalArgumentException("we found no distance. this is an odd behaviour and definetly a bug, or means this method is not well-thought at all");
    return minDistance + 1;
}

Things I should take into consideration:我应该考虑的事情:

  • Should I complain about ambigous methods?我应该抱怨模棱两可的方法吗? As you can see I just pick up the first正如你所看到的,我只是拿起第一个
  • That overloaded with different type issue... How can I know precedence?与不同类型的问题重载......我怎么知道优先级? If I'm not wrong, compiler allways picks the method of the closest class.如果我没记错的话,编译器总是会选择最接近类的方法。
  • That distance calculation looks too simple to be true那个距离计算看起来太简单了,不可能是真的

I could not find the exact spects about how does the compiler actually perform the task我找不到关于编译器如何实际执行任务的确切方面

They are in the Java Language Specification , section 15.12.它们位于Java Language Specification 的15.12节中 Method Invocation Expressions , more precisely in subsection 15.12.2. 方法调用表达式,在15.12.2小节中更准确 Compile-Time Step 2: Determine Method Signature . 编译时步骤 2:确定方法签名

It's quite complex, and as you can see, far more involved than what you're currently trying to do.它非常复杂,正如您所看到的,比您目前尝试做的要复杂得多。

I'll leave it up to you to read through all of sections 15.12.2 to 15.12.2.6, and then determine how much you can ignore and still "accomplishing the task in a relatively acceptable way" .我将让您通读 15.12.2 到 15.12.2.6 的所有部分,然后确定您可以忽略多少并仍然“以相对可接受的方式完成任务”

Note that compiler does not work on just classes like your current code, but it works on full types, depending what kind of code you are writing it might affect you.请注意,编译器不仅仅适用于像您当前的代码这样的类,但它适用于完整类型,这取决于您编写的代码类型可能会影响您。 Imagine this:想象一下:

void someMethod(List<? extends Number> numbers);
void someMethod(Collection<String> numbers);

Then if you will look for method that can accept ArrayList you will find both, so if you don't know type of your data, you will not be able to provide valid answer.然后,如果您要寻找可以接受ArrayList方法,您将同时找到两者,因此如果您不知道数据类型,您将无法提供有效答案。 But generic type of variables is lost anyway, so if you are trying to find matching method for some list of arguments - you can't do that in good reliable way.但是无论如何,通用类型的变量都会丢失,因此如果您试图为某些参数列表找到匹配方法 - 您不能以可靠的方式做到这一点。

Object[] args = new Object[]{List.of("String")};
Class[] argTypes = extractTypes(args); // contains [SomeListImplementation.class]
findBestMatchingMethod(SomeClass.class, argTypes); // ?? both method matches

You could try to "rebuild" generic type by analyzing what is inside object, but that is very hard and would not always work (because raw types are used or object is empty).您可以尝试通过分析对象内部的内容来“重建”泛型类型,但这非常困难并且并不总是有效(因为使用了原始类型或对象为空)。 Would only be kinda possible for collection types to some limited degree.只能在有限程度上适用于集合类型。

But if this is enough, then java have some tools for this:但是如果这足够了,那么java有一些工具:

new Statement(object, "doSomething", new Object[]{arg1, arg2, arg3}).execute();

But return value is lost.但是返回值丢失了。 But you can just copy code from jdk used to do this.但是您可以从用于执行此操作的 jdk 中复制代码。 I also once developed my own code to do something like that:我也曾经开发过自己的代码来做这样的事情:

First we need some utils we will use later, like easy way to map between primitive and object types, as java compiler handles this for us, so we should too:首先我们需要一些稍后会用到的工具,比如在原始类型和对象类型之间映射的简单方法,因为java编译器会为我们处理这个,所以我们也应该:

public final class ClassUtils {
    private static final Map<Class<?>, Class<?>> primitives = Map.of(
        Boolean.class,   boolean.class,     Byte.class,    byte.class,        Short.class, short.class,
        Character.class, char.class,        Integer.class, int.class,         Long.class,  long.class,
        Float.class,     float.class,       Double.class,  double.class,      Void.class,  void.class);
    private static final Map<Class<?>, Class<?>> wrappers   = Map.of(
        boolean.class, Boolean.class,       byte.class,   Byte.class,         short.class, Short.class,
        char.class,    Character.class,     int.class,    Integer.class,      long.class,  Long.class,
        float.class,   Float.class,         double.class, Double.class,       void.class,  Void.class);

    public static Class<?> getPrimitive(Class<?> clazz) {
        if (clazz.isPrimitive()) {
            return clazz;
        }
        return primitives.getOrDefault(clazz, clazz);
    }

    public static Class<?> getWrapperClass(Class<?> clazz) {
        if (! clazz.isPrimitive()) {
            return clazz;
        }
        return wrappers.getOrDefault(clazz, clazz);
    }
}

And simple util to check if object of typeA could be assigned to typeB but handling primitive types (as in java you can pass int to long , or int to Integer ) too:以及检查 typeA 的对象是否可以分配给 typeB 但处理原始类型的简单实用程序(如在 java 中,您可以将int传递给long ,或将int传递给Integer ):

public final class TypeUtils {
    private TypeUtils() {}

    /**
     * Checks if given objectType can be assigned to variable of variableType type.
     *
     * @param objectType type of object that you want to assign.
     * @param variableType type of variable where object will be assigned.
     * @param autoboxing whether to use implicit autoboxing/unboxing between primitives and wrappers
     *
     * @return {@code true} if assignment possible
     */
    public static boolean isAssignable(@Nullable Class<?> objectType, final Class<?> variableType, final boolean autoboxing) {
        if (objectType == null) {
            return ! variableType.isPrimitive();
        }
        if (objectType == variableType) {
            return true;
        }

        if (autoboxing) {
            if (objectType.isPrimitive() && ! variableType.isPrimitive()) {
                objectType = ClassUtils.getWrapperClass(objectType);
            }
            if (variableType.isPrimitive() && ! objectType.isPrimitive()) {
                objectType = ClassUtils.getPrimitive(objectType);
                if (! objectType.isPrimitive()) {
                    return false;
                }
            }
        }

        if (objectType == variableType) {
            return true;
        }

        if (objectType.isPrimitive()) {
            if (! variableType.isPrimitive()) {
                return false;
            }
            if (Integer.TYPE.equals(objectType)) {
                return Long.TYPE.equals(variableType) || Float.TYPE.equals(variableType) || Double.TYPE.equals(variableType);
            }
            if (Long.TYPE.equals(objectType)) {
                return Float.TYPE.equals(variableType) || Double.TYPE.equals(variableType);
            }
            if (Boolean.TYPE.equals(objectType)) {
                return false;
            }
            if (Double.TYPE.equals(objectType)) {
                return false;
            }
            if (Float.TYPE.equals(objectType)) {
                return Double.TYPE.equals(variableType);
            }
            if (Character.TYPE.equals(objectType)) {
                return Integer.TYPE.equals(variableType)
                               || Long.TYPE.equals(variableType)
                               || Float.TYPE.equals(variableType)
                               || Double.TYPE.equals(variableType);
            }
            if (Short.TYPE.equals(objectType)) {
                return Integer.TYPE.equals(variableType)
                               || Long.TYPE.equals(variableType)
                               || Float.TYPE.equals(variableType)
                               || Double.TYPE.equals(variableType);
            }
            if (Byte.TYPE.equals(objectType)) {
                return Short.TYPE.equals(variableType)
                               || Integer.TYPE.equals(variableType)
                               || Long.TYPE.equals(variableType)
                               || Float.TYPE.equals(variableType)
                               || Double.TYPE.equals(variableType);
            }
            return false;
        }
        return variableType.isAssignableFrom(objectType);
    }
}

Note: if you will be trying to make version that works with generics, apache commons have method to check if 2 generic types are assignable to each other, as I would not suggest writing it alone, its a lot of complicated code to handle all possible nested generic types.注意:如果您要尝试制作与泛型一起使用的版本,apache commons 有方法来检查 2 个泛型类型是否可以相互分配,因为我不建议单独编写它,它有很多复杂的代码来处理所有可能的嵌套泛型类型。

And having these 2 simple utils we can start creating code to find this matching executable, first I made small enum to represent 3 possible states of method:有了这两个简单的工具,我们就可以开始创建代码来找到这个匹配的可执行文件,首先我做了一个小的枚举来表示方法的 3 种可能状态:

enum CompatibleExecutableResult {
    EXACT,
    COMPATIBLE,
    INVALID
}

It can either be perfect match or be compatible or no match, we have separate exact match because javac would then fail to compile due to ambiguous definitions, so we probably want to do the same.它可以是完美匹配,也可以是兼容或不匹配,我们有单独的完全匹配,因为 javac 会由于定义不明确而无法编译,所以我们可能想要做同样的事情。

So now a simple function that calculates that:所以现在有一个简单的函数来计算:

private static CompatibleExecutableResult isCompatibleExecutable(Method method, Class[] providedTypes) {
    Class<?>[] constructorParameterTypes = method.getParameterTypes();
    CompatibleExecutableResult current = CompatibleExecutableResult.EXACT;
    for (int i = 0; i < constructorParameterTypes.length; i++) {
        Class<?> providedType = providedTypes[i];
        Class<?> parameterType = constructorParameterTypes[i];

        // null can't be used as primitive
        if ((providedType == null) && parameterType.isPrimitive()) {
            return CompatibleExecutableResult.INVALID;
        }

        // handle primitives correctly by using our special util function
        if ((providedType != null) && !TypeUtils.isAssignable(parameterType, providedType, true)) {
            return CompatibleExecutableResult.INVALID;
        }

        // this code support skipping some types assuming that you will use null value for it, so you can look for any method with 3 arguments where you only know 2 first types and last one will be always null.
        if ((providedType == null)) {
            current = CompatibleExecutableResult.COMPATIBLE;
            continue;
        }

        if (parameterType.equals(providedType)) {
            continue; // sill exact match
        }
        // it was not an exact match as types of this argument were not equals, so thats our current max possible score for this method
        current = CompatibleExecutableResult.COMPATIBLE;
    }
    return current;
}

But now we could find few exact matches, or few compatible methods, so we also need a function that will tell us which method is more specialized:但是现在我们几乎找不到完全匹配或兼容的方法,因此我们还需要一个函数来告诉我们哪个方法更专业:

static Method getMoreSpecialized(Method a, Method b) {
    if (a == null) return b;
    if (b == null) return a;
    Class<?>[] aTypes = a.getParameterTypes();
    Class<?>[] bTypes = b.getParameterTypes();
    int result = 0;
    for (int i = 0; i < aTypes.length; i++) {
        Class<?> aType = aTypes[i];
        Class<?> bType = bTypes[i];

        // same type, no differences so far
        if (aType.equals(bType)) {
            continue;
        }

        // if aType is less specialized than bType
        if ((aType.isPrimitive() && !bType.isPrimitive()) || TypeUtils.isAssignable(aType, bType, true)) {
            // one of prev types was less specialized, javac fails to find such constructor, we should too
            if (result < 0) {
                throw new IllegalStateException("Ambiguous executables found for: " + Arrays.toString(aTypes) + " and " + Arrays.toString(bTypes));
            }
            result += 1;
        } else {
            if (result > 0) {
                throw new IllegalStateException("Ambiguous executables found for: " + Arrays.toString(aTypes) + " and " + Arrays.toString(bTypes));
            }
            result -= 1;
        }
    }
    if (result == 0) {
        throw new IllegalStateException("Ambiguous executables found for: " + Arrays.toString(aTypes) + " and " + Arrays.toString(bTypes));
    }
    return result < 0 ? a : b;
}

And then we can write our simple loop that will find best match, so loop over methods, check if compatible and check if more specialized than last one + be sure to not allow more than 1 exact match:然后我们可以编写我们的简单循环来找到最佳匹配,因此循环方法,检查是否兼容并检查是否比上一个更专业+确保不允许超过 1 个完全匹配:

static <T> Method findBest(Collection<? extends Method> methods, Class[] paramTypes) {
    int exactMatches = 0;
    Method bestMatch = null;
    for (Method executable : methods) {
        CompatibleExecutableResult compatibleConstructor = isCompatibleExecutable(executable, paramTypes);
        if (compatibleConstructor == CompatibleExecutableResult.EXACT) {
            if (exactMatches >= 1) {
                throw new IllegalStateException("Ambiguous executables found " + Arrays.toString(paramTypes));
            }
            exactMatches += 1;
        }
        if (compatibleConstructor != CompatibleExecutableResult.INVALID) {
            bestMatch = getMoreSpecialized(bestMatch, executable);
        }
    }
    if (bestMatch == null) {
        throw new IllegalStateException("Can't find matching executable for: " + Arrays.toString(paramTypes));
    }
    return bestMatch;
}

Code supporting generic types should look similar, just use Type[] and that apache commons method to check if objects are assignable with a small wrapper to also handle primitive types correctly like here.支持泛型类型的代码应该看起来相似,只需使用Type[]和 apache commons 方法来检查对象是否可以用一个小包装器分配,以便像这里一样正确处理原始类型。

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

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