如何找到真正的 public 方法
发布于 8 天前 作者 yan 30 次浏览

(给ImportNew加星标,提高Java技能)

编译:ImportNew/唐尤华

www.javaspecialists.eu/archive/Issue273.html

摘要Class.getMethods() 能返回 class 及父类中所有 public方法。然而,这些方法并不一定能被调用,比如在 private inner class 或 lambda 表达式中声明的方法。这篇文章会介绍如何找到所有真正的 public 方法。

昨天,我试图找出某个未知对象的所有 public 方法。这件事应该很容易,用 getClass() 找出 class,然后调用 getMethods()。即使启用了 SecurityManager 应该也可以工作。第一个测试对象为 java.util.ArrayList,运行结果一切正常。然后把代码重构一下,使用 Arrays.asList() 创建并初始化第一步创建的列表。结果,抛出了 IllegalAccessException。

import java.lang.reflect.*;
import java.util.*;

public class ReflectionPuzzle {
 public static void main(String... args) {
   Collection<String> names = new ArrayList<>();
   Collections.addAll(names, "Goetz", "Marks", "Rose");

   printSize(names);
   printSize(Arrays.asList("Goetz", "Marks", "Rose"));
   printSize(List.of("Goetz", "Marks", "Rose"));
   printSize(Collections.unmodifiableCollection(names));
 }

 private static void printSize(Collection<?> col) {
   System.out.println("Size of " + col.getClass().getName());
   try {
     Method sizeMethod = col.getClass().getMethod("size");
     System.out.println(sizeMethod.invoke(col));
   } catch (ReflectiveOperationException e) {
     System.err.println(e);
   }
 }
} 

上面的程序运行结果如下:

Size of java.util.ArrayList
3
Size of java.util.Arrays$ArrayList
java.lang.IllegalAccessException: class ReflectionPuzzle cannot
access a member of class java.util.Arrays$ArrayList (in module
java.base) with modifiers "public"
Size of java.util.ImmutableCollections$ListN
java.lang.IllegalAccessException: class ReflectionPuzzle cannot
access a member of class java.util.ImmutableCollections$ListN (in
module java.base) with modifiers "public"
Size of java.util.Collections$UnmodifiableCollection
java.lang.IllegalAccessException: class ReflectionPuzzle cannot
access a member of class
java.util.Collections$UnmodifiableCollection (in module
java.base) with modifiers "public" 

只有ArrayList执行printSize()时能得到结果。虽然 Size() 是定义在 java.util.Collection 中的 public 方法,但是定义的 class 也必须为 public。

我把这个问题发到了 Twitter,Brian Goetz 看到后告诉我,这种做法使用的是语言级别反射(reflection),应该用 class 反射。事实也是如此。如果对所有列表对象执行 List.class.getMethod(“size”) ,那么所有列表都会自动报告它们的大小(size)。

Java 没有“private” class 或类似的东西,class 只有 public 或 package 访问权限。当在内部类定义为“private”时,class 文件并没有在内部标记为 private。相反,它的访问权限为“package 可见”,外部类(outer class)访问时会遵守相应规则。之前的版本中,所有访问都是通过 synthetic method 完成,Java 12 对此进行了改变。

让我们定义一个稍微复杂的 class,它实现了多个接口继承并且采用协变返回值类型。

首先,在“problem” package 中定义两个 interface A 和 B。每个接口都定义了方法 foo() 但是返回值类型不同。

package problem;

public interface A {
 CharSequence foo();
}
 
package problem;

public interface B {
 java.io.Serializable foo();
} 

接下来,我们在另一个 package problem.inner 中定义 class “Hidden”,包含两个工厂方法。getLambda() 实现了接口 A 返回一个 lambda。getPrivateInnerClass() 返回一个 C 的实例。请注意:C 同时实现了接口 A 和 B,并且 foo()返回类型同时实现了这两个接口。还有另外一个 public 方法 bar(),虽然为 public,但因为内部类为 private,同样也无法访问。

package problem.inner;

import problem.*;

public class Hidden {
 public static A getPrivateInnerClass() {
   return new C();
 }

 private static class C implements A, B {
   public String foo() {
     return "Hello World";
   }

   public String bar() {
     return "Should not be visible";
   }
 }

 public static A getMethodClass() {
   class D implements A {
     public CharSequence foo() {
       return "inside method";
     }
   }
   return new D();
 }

 public static A getLambda() {
   return () -> "Hello Lambert";
 }
} 

下面这个示例展示了如何用普通的反射调用 foo():

import problem.*;
import problem.inner.*;

import java.lang.reflect.*;
import java.util.stream.*;

public class TestPlainReflection {
 public static void main(String... args) {
   System.out.println("Testing private inner class");
   test(Hidden.getPrivateInnerClass());
   System.out.println();

   System.out.println("Testing method inner class");
   test(Hidden.getMethodClass());
   System.out.println();

   System.out.println("Testing lambda");
   test(Hidden.getLambda());
 }

 private static void test(A a) {
   Stream.of(a.getClass().getMethods())
       .forEach(System.out::println);
   printMethodResult(a, "foo");
   printMethodResult(a, "bar");
 }

 private static void printMethodResult(Object o, String name) {
   try {
     Method method = o.getClass().getMethod(name);
     System.out.println(method.invoke(o));
   } catch (NoSuchMethodException e) {
     System.out.println("Method " + name + "() not found");
   } catch (IllegalAccessException e) {
     System.out.println("Illegal to call " + name + "()");
   } catch (InvocationTargetException e) {
     throw new IllegalStateException(e.getCause());
   }
 }
} 

结果同样不理想。通过 getMethod() 找到 foo(),由于属于 lambda 和私有内部类,同样也无法调用。而且,这些方法掩盖(shadow)了A 和 B 中的方法。在私有内部类 C 上调用 getMethods() 会返回三个名为“foo”的方法,这些方法参数列表都为空,只有返回类型不同。String foo() 在 Hidden$C 中定义,另外两个是合成(synthetic)方法。它们由编译器生成,用来产生协变返回类型。

Testing private inner class
public CharSequence problem.inner.Hidden$C.foo()
public java.io.Serializable problem.inner.Hidden$C.foo()
public String problem.inner.Hidden$C.foo()
public String problem.inner.Hidden$C.bar()
public Class Object.getClass()
public boolean Object.equals(Object)
public int Object.hashCode()
public String Object.toString()
public void Object.wait() throws InterruptedException
public void Object.wait(long) throws InterruptedException
public void Object.wait(long,int) throws InterruptedException
public void Object.notify()
public void Object.notifyAll()
Illegal to call foo()
Illegal to call bar()

Testing method inner class
public CharSequence problem.inner.Hidden$1D.foo()
public Class Object.getClass()
public boolean Object.equals(Object)
public int Object.hashCode()
public String Object.toString()
public void Object.wait() throws InterruptedException
public void Object.wait(long) throws InterruptedException
public void Object.wait(long,int) throws InterruptedException
public void Object.notify()
public void Object.notifyAll()
Illegal to call foo()
Method bar() not found

Testing lambda
public CharSequence problem.inner.Hidden$$Lambda$23/0x67840.foo()
public Class Object.getClass()
public boolean Object.equals(Object)
public int Object.hashCode()
public String Object.toString()
public void Object.wait() throws InterruptedException
public void Object.wait(long) throws InterruptedException
public void Object.wait(long,int) throws InterruptedException
public void Object.notify()
public void Object.notifyAll()
Illegal to call foo()
Method bar() not found 

注意:我们无法通过这些对象调用 foo()。

在 Reflections 类中,我们尝试找到 public 方法且方法所在的 class 也是 public。为了确认这一点,把方法以及包含方法的类使用的修饰符进行“与(AND)”操作。如果结果仍然是 public,那么就能知道这是一个真正的 public 方法。在旧的反射机制中,当在指定的类中找不到方法时,会抛出 NoSuchMethodException。这里会返回  Optional<Method>。

getTrulyPublicMethods() 会递归找到 class 层次结构中所有真正的 public 方法。为了模拟掩盖(shadow)效果,只有当层次结构下游没有相同的方法签名时,才会加入结果集合。在例子中,方法签名由返回类型、方法名和参数类型组成。集合的 key 是一个字符串,由这三个元素组成。

import java.lang.reflect.*;
import java.util.*;
import java.util.stream.*;

public class Reflections {
 public static Optional<Method> getTrulyPublicMethod(
     Class<?> clazz, String name, Class<?>... paramTypes) {
   return getTrulyPublicMethods(clazz)
       .stream()
       .filter(method -> matches(method, name, paramTypes))
       .reduce((m1, m2) -> {
         Class<?> r1 = m1.getReturnType();
         Class<?> r2 = m2.getReturnType();
         return r1 != r2 && r1.isAssignableFrom(r2) ? m2 : m1;
       });
 }

 public static Collection<Method> getTrulyPublicMethods(
     Class<?> clazz) {
   Map<String, Method> result = new HashMap<>();
   findTrulyPublicMethods(clazz, result);
   return List.copyOf(result.values());
 }

 private static void findTrulyPublicMethods(
     Class<?> clazz, Map<String, Method> result) {
   if (clazz == null) return;
   Method[] methods = clazz.getMethods();
   for (Method method : methods) {
     if (isTrulyPublic(method))
       result.putIfAbsent(toString(method), method);
   }
   for (Class<?> intf : clazz.getInterfaces()) {
     findTrulyPublicMethods(intf, result);
   }
   findTrulyPublicMethods(clazz.getSuperclass(), result);
 }

 private static boolean isTrulyPublic(Method method) {
   return Modifier.isPublic(method.getModifiers()
       & method.getDeclaringClass().getModifiers());
 }

 private static String toString(Method method) {
   String prefix = method.getReturnType().getCanonicalName() +
       method.getName() + " (";
   return Stream.of(method.getParameterTypes())
       .map(Class::getCanonicalName)
       .collect(Collectors.joining(", ",
           prefix, ")"));
 }

 private static boolean matches(
     Method method, String name, Class<?>... paramTypes) {
   return method.getName().equals(name)
       && Arrays.equals(method.getParameterTypes(), paramTypes);
 }
} 

可以确定的是,这里没有考虑所有返回类型和一些可能出错的 class 层次结构。但是,这段代码的确通过了我的测试。另外,使用 Optional 要比捕捉异常好。下面是 TestTrulyPublic 实现:

import problem.*;
import problem.inner.*;

import java.lang.reflect.*;
import java.util.*;

public class TestTrulyPublic {
 public static void main(String... args) throws Exception {
   System.out.println("Testing private inner class");
   test(Hidden.getPrivateInnerClass());
   System.out.println();

   System.out.println("Testing method inner class");
   test(Hidden.getMethodClass());
   System.out.println();

   System.out.println("Testing lambda");
   test(Hidden.getLambda());
 }

 private static void test(A a) {
   Reflections.getTrulyPublicMethods(a.getClass()).forEach(
       System.out::println);
   printMethodResult(a, "foo");
   printMethodResult(a, "bar");
 }

 private static void printMethodResult(
     Object o, String methodName) {
   Optional<Method> method = Reflections.getTrulyPublicMethod(
       o.getClass(), methodName);
   method.map(m -> {
     try {
       System.out.println("m = " + m);
       return m.invoke(o);
     } catch (IllegalAccessException e) {
       throw new IllegalStateException(e);
     } catch (InvocationTargetException e) {
       throw new IllegalStateException(e.getCause());
     }
   }).ifPresentOrElse(System.out::println,
       () -> System.out.println("Method " +
           methodName + "() not found"));
 }
} 

运行第二个测试,得到以下输出结果:

Testing private inner class
public abstract java.io.Serializable problem.B.foo()
public abstract CharSequence problem.A.foo()
public Class Object.getClass()
public boolean Object.equals(Object)
public int Object.hashCode()
public String Object.toString()
public void Object.wait() throws InterruptedException
public void Object.wait(long) throws InterruptedException
public void Object.wait(long,int) throws InterruptedException
public void Object.notify()
public void Object.notifyAll()
m = public abstract java.io.Serializable problem.B.foo()
Hello World
Method bar() not found

Testing method inner class
public abstract CharSequence problem.A.foo()
public Class Object.getClass()
public boolean Object.equals(Object)
public int Object.hashCode()
public String Object.toString()
public void Object.wait() throws InterruptedException
public void Object.wait(long) throws InterruptedException
public void Object.wait(long,int) throws InterruptedException
public void Object.notify()
public void Object.notifyAll()
m = public abstract CharSequence problem.A.foo()
inside method
Method bar() not found

Testing lambda
public abstract CharSequence problem.A.foo()
public Class Object.getClass()
public boolean Object.equals(Object)
public int Object.hashCode()
public String Object.toString()
public void Object.wait() throws InterruptedException
public void Object.wait(long) throws InterruptedException
public void Object.wait(long,int) throws InterruptedException
public void Object.notify()
public void Object.notifyAll()
m = public abstract CharSequence problem.A.foo()
Hello Lambert
Method bar() not found 

可以看到,现在所有 foo() 调用都成功了,还可以看到这里没有找到 bar() 方法。

致礼!

Heinz

推荐阅读

(点击标题可跳转阅读)

Java 8 注解探秘

不要在 Docker 镜像中使用 Fat Jar

为什么 main 方法是 public static void ?

看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

好文章,我在看❤️

回到顶部