如何找到真正的 public 方法

專注的阿熊發表於2019-11-04

摘要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
function(){   // 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() 方法。  


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946337/viewspace-2662477/,如需轉載,請註明出處,否則將追究法律責任。

相關文章