読者です 読者をやめる 読者になる 読者になる

速さが足りない

プログラミングとか、設計とか、ゲームとかいろいろ雑記。

Java Reflection(メソッドを呼び出す)

Java Reflection

前回の記事はこちら。
さて、今回はメソッドの呼び出しです。
実はメソッドの呼び出しだけだといまいちぱっとしないので色々盛り込みました。
ということでまずは見てみましょう。

package jp.co.companyname.pjname.domain;

import java.util.Date;

public class DomainObject implements Serializable{
  private static final long serialVersionUID = -1L;
  protected String createdBy;
  protected String modifiedBy;
  protected Date createdDate;
  protected Date modifiedDate;
  public String getCreatedBy(){
    return this.createdBy;
  }
  public void setCreatedBy(String createdBy){
    this.createdBy = createdBy;
  }
  public String getModifiedBy(){
    return this.modifiedBy;
  }
  public void setModifiedBy(String modifiedBy){
    this.modifiedBy = modifiedBy;
  }
  public Date getCreatedDate(){
    return this.createdDate;
  }
  public void setCreatedDate(Date createdDate){
    this.createdDate = createdDate;
  }
  public Date getModifiedDate(){
    return this.modifiedDate;
  }
  public void setModifiedDate(Date modifiedDate){
    this.modifiedDate = modifiedDate;
  }
}

public class Foo extends DomainObject{
  private static final long serialVersionUID = -1L;
  protected Strgin foo;
  private List<String> fooHistory;
  public Foo(){
    fooHistory = new ArrayList<String>();
  }
  public String getFoo(){
    return this.foo;
  }
  public void setFoo(String foo){
    this.foo = foo;
    addFooHistory(foo);
  }
  public String name(){
    return foo;
  }
  protected void setDefault(){
    this.foo = "foo";
  }
  protected boolean isDefault(){
    return this.foo.equals("foo");
  }
  private void addFooHistory(String his){
    fooHistory.add(his);
  }
  public static String getDefault(){
    return "foo";
  }
  public String getHistories(){
    StringBuilder r = new StringBuilder();
    for(String each : fooHistory){
      if(r.length()>0)
        r.append(", ");
      r.append(each);
    }
    return r.toString();
  }
}

はい、単純なドメインオブジェクトがあります。メソッドとしては

があるわけですね。フィールドアクセスと内容が被るところがありますがメソッドで取り上げようと思います。
では実際にリフレクション経由でアクセスしてみましょう。

  Class<?> fooClazz = Foo.class;
  Foo foo = new Foo();
  Method setFooMethod = fooClazz.getMethod("setFoo",String.class);
  setFooMethod.invoke(foo,"hogehoge");
  System.out.println(foo.name());

  foo.setFoo("hoge");
  Method nameMethod = fooClazz.getMethod("name");
  String name = (String)nameMethod.invoke(foo);
  System.out.println(name);


実行結果:
hogehoge
hoge

上段のコードのは1のsetterにアクセスしている訳です。getter/setterは特別なメソッドではありますが、単体で見ると通常のメソッドと同じ訳ですね。
下段のコードは2の公開メソッドにアクセスしている訳ですね。今回は引数なしの戻り値ありです。
それでは次に参りましょう。

  Class<?> fooClazz = Foo.class;
  Method getDefaultMethod = fooClazz.getMethod("getDefault");
  String defaultValue = (String)getDefaultMethod.invoke(null);
  System.out.println(defaultValue);

実行結果:
foo

上記のコードは3のstaticな公開メソッドにアクセスしています。staticな場合は、Method#invoke(target,args) を呼ぶ際にtargetがない訳ですから、nullを指定しています。
さて、どんどんいきましょう。

  Class<?> fooClazz = Foo.class;
  Foo foo = new Foo();
  Method setDefaultMethod = 
    fooClazz.getDeclaredMethod("setDefault");
  setDefaultMethod.setAccessible(true);
  setDefaultMethod.invoke(foo);
  System.out.println(foo.name());
  
  Method addFooHistoryMethod = 
    fooClazz.getDeclaredMethod("addFooHistory",String.class);
  addFooHistoryMethod.setAccessible(true);
  addFooHistoryMethod.invoke(foo,"hogehoge");
  System.out.println(foo.getHistories());

実行結果:
foo
foo, hogehoge

上段のコードは4のprotectedなメソッドへのアクセス、下段のコードはprivateなコードへのアクセスをしています。簡単ですね!
Method#setAccessible() については、前回の記事にも書いた通り、セキュリティマネージャの設定次第では動きません。起動オプションやポリシ設定の仕方(このリンク先)を確認しておきましょう。

さて、次はプロパティの操作についてです。java.beansパッケージにはプロパティを包括的に操作できるPropertyDescriptorというクラスがあるので、そちらを使ってみましょう。

  Class<?> fooClazz = foo.class;
  Foo foo = new Foo();
  foo.setFoo("bar");
  
  PropertyDescriptor pd = new PropertyDescriptor("foo",fooClazz);
  String getFooResult = pd.getReadMethod().invoke(foo);
  System.out.println(getFooResult);

  pd.getWriteMethod().invoke(foo,"hoge");
  System.out.println(foo.getFoo());

実行結果:
bar
hoge

こんなかんじですね。最初に指定したプロパティへのアクセサが簡単に取得できる訳です。
もちろんプロパティ「foo」であれば、「getFoo」「isFoo」「setFoo」等、java beansの命名ルールに則ってアクセサが実装されている必要があります。
メソッドがなければNoSuchMethodExceptionがスローされますので、次回のフィールドへのアクセスを組み合わせて疑似的にアクセサがあるように振る舞うこともできます。




さて、基本的なメソッドクラスは押さえましたので、もう少し先に進みましょう。

public class Bar extends Foo{
  private static final long serialVersionUID = -1L;
  private String bar;
  public String getBar(){
    return this.bar;
  }
  public void setBar(String bar){
    this.bar = bar;
  }
  @Override
  @ThreadUnsafe
  @Transactional("hoge-transaction");
  public String name(){
    return super.name() + this.bar;
  }
  public static Bar newInstance(){
    return new Bar();
  }
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ThreadSafe{
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ThreadUnsafe{
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Transactional{
  public String value();
}

はい、ではn階層の継承構造を持つドメインオブジェクトについて考察してみましょう。
継承構造はBar←Foo←DomainObjectになってますね?FooやDomainObjectのプロパティにアクセスする際は、getter/setterはpublicであるため、どの継承階層に所属しているかは普段あまり気にしないと思います。(特に継承構造が深いと)
勿論、今回のお題はリフレクションですから、リフレクションで操作することもあると思います。例えば

  <bean id="barInstance" class="jp.co.companyname.pjname.domain.Bar">
    <property name="bar" value="hogehoge" />
    <property name="createdBy" value="administrator" />
  </bean>

こんなような設定ファイルがあった場合に、Barが持っているプロパティ「bar」と継承階層の誰かが持っている「createdBy」が混在することになります。
この操作が上手くいく方法を考えてみましょう。

  Bar barInstance = new Bar();
  String fieldName = "createdBy";
  String getMethodName = 
    "get" + 
    Character.valueOf(fieldName.charAt(0)).toString().toUpperCase() + 
    fieldName.substring(1,fieldName.length());
  Class<?> barClazz = Bar.class;
  Method getCreatedByMethod = barClazz.getMethod(getMethodName);
  String createdByValue = getCreatedByMethod.invoke(barInstance);

はい、動きそうな感じがしますが、残念ながらClass#getMethod(getMethodName) でNoSuchMethodExceptionが飛びます。
Barクラスは該当のプロパティを定義していないですし、getter/setterも持っていないためです。
メタプログラミング的には、Barの継承構造を知っている必要はないですし、把握もしてないですし、
ここではどうやってcreatedByを操作するか、のみ解決すればよいわけですね。
ということで書いてみましょう。

  Bar barInstance = new Bar();
  String fieldName = "createdBy";
  String getMethodName = 
    "get" + 
    Character.valueOf(fieldName.charAt(0))
      .toString().toUpperCase() + 
    fieldName.substring(1,fieldName.length());
  Class<?> barClazz = Bar.class;
  Method getCreatedByMethod = null;
  do{
    try{
      getCreatedByMethod = barClazz.getMethod(getMethodName);
    }
    catch(NoSuchMethodException nsme){
      try{
        getCreatedByMethod  = 
          barClazz.getDeclaredMethod(getMethodName);
      }
      catch(NoSuchMethodException nsme2){
        barClazz = barClazz.getSuperclass();
      }
    }
  }while(barClazz != null && getCreatedByMethod == null);
  
  if(getCreatedByMethod == null)
    throw new NoSuchMethodException();
  getCreatedByMethod.invoke(barInstance);

はい、こんなかんじですかね。Class#getMethod / Class#getDeclaredMethodに存在しないメソッド名・引数の組み合わせを渡すと、NoSuchMethodExceptionが飛ぶことを踏まえて、Exception飛ぶ=見つからない、扱いとしてpublic / private・protectedのどちらにもメソッドがなかったら、継承構造を1つ遡るようにしているわけです。
それと、うーんExceptionがガシガシ飛ぶコードはちょっと・・・って思いません?ということでもう少し綺麗に書き直しましょう。

  Bar barInstance = new Bar();
  Object[] parameters = new Object[]{};
  Class<?>[] parameterTypes = new Class<?>[]{];
  String fieldName = "createdBy";
  String getMethodName = 
    "get" + 
    Character.valueOf(fieldName.charAt(0))
      .toString().toUpperCase() + 
    fieldName.substring(1,fieldName.length());
  Class<?> barClazz = Bar.class;
  Method getCreatedByMethod = null;
  
  Comparator<Class<?>[]> comparator = (left, right) -> {
    if(left.length > right.length)
      return 1;
    if(left.length < right.length)
      return -1;
    for(int i = 0; i < left.length; i++){
      if(!left[i].equals(right[i]))
        return 1;
    }
    return 0;
  };
  Function<Method[],List<Method>> converter = (arg) -> {
    return Arrays.asList(arg);
  };
  Consumer<Method> consumer = (each) -> {
    if(getCreatedByMethod != null)
      return;
    if(
      each.getName().equals(getMethodName) &&
      comparator.compare(
        each.getParameterTypes(),
        parameterTypes()
      ) == 0
    ){
       getCreatedByMethod = each;
    }
  };
  
  while(getCreatedByMethod == null){
    converter.apply(barClazz.getMethods()).forEach(consumer);
    if(getCreatedMethod == null)
      converter.apply(barClazz.getDeclaredMethod()).forEach(consumer);
    barClazz = barClazz.getSuperclass();
    if(barClazz == null)
      break;
  }

  if(getCreatedByMethod == null)
    throw new NoSuchMethodException();
  getCreatedByMethod.invoke(barInstance);  

はい、こんなかんじですかね。幾つかあるラムダはユーティリティとしてどこかに纏めると更にシンプルになると思います。


さて、メソッドの呼び出しからはちょっと外れますが・・・メソッドの修飾子が何であるか判定する必要が出てくることがあるかも知れません。特にユーティリティを作成したときのメソッド呼出しで、static / instance の判定なんかは必要そうですね。
ということで、修飾子の判定をするのに必要な機能を見ていきましょう。
なお、Java言語のシンタクスとしては、メソッドに以下の修飾子を適用できます。
public, protected, private, abstract, static, final, synchronized, native, strictfp

  Class<?> fooClazz = Foo.class;
  Foo foo = new Foo();
  Method setFooMethod = fooClazz.getMethod("setFoo",String.class);
  //false
  Modifier.isAbstract(setFooMethod.getModifiers());
  //false
  Modifier.isFinal(setFooMethod.getModifiers());
  //true
  Modifier.isPublic(setFooMethod.getModifiers());
  //false
  Modifier.isPrivate(setFooMethod.getModifiers());
  ...

java.lang.reflect.Modifierには、intを受け取り、修飾子を判定するメソッドがあるのでそちらを利用しましょう。もちろん上記のコードの例以外にもたくさんのメソッドが用意されています。
前回の記事では紹介しませんでしたが、もちろんClass#getModifiers() もありますので、クラスに適用されている修飾子はそれを使って判定することができるわけですね。


さて、最後になりましたがアノテーションについて見ていきましょう。アノテーションについては纏めて記事にしますので、今回はさわりだけ。

  Class<?> barClazz = Bar.class;
  Method method = barClass.getMethod("name");
  Transactional[] trans = method.getAnnotationsByType(Transactional.class);
  System.out.println(trans[0].value());

実行結果:
hoge-transaction

はい、簡単ですね?AnnotatedElement#getAnnotationsByType() を呼ぶとそれに注釈されているアノテーションが取得できるわけです。これを使って各種frameworkは注釈されているアノテーションから設定値を取得しているわけですね。
AnnotatedElementインターフェースの実装は、AccessibleObject, Class, Constructor, Executable, Field, Method, Package, Parameterですので、これらでアノテーションを取得することができます。


ということで、今回はここまでとします。次回はフィールド(プロパティ)への操作です。