(注:このブログはもう更新していません)この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

[クラウド帳票エンジンDocurain]

Eclipse3.5のコンパイラに致命的なバグ有り


昔も書いたが、EclipseJavaコンパイラJDKjavaコンパイラは別物。


昨日、原因究明に到るまで半日要したのだが、
Eclipse3.5のJavaコンパイラ(ecj)に致命的なバグがあることを発見した。


簡単に説明すると、
『非publicな親クラスから引き継いだpublicメンバーを、異なるパッケージにおいて子クラス経由でリフレクションで呼び出すとエラーになる』
というもの。まあコードを見たほうが早いので、下のコードを見るべし。

サンプルコード

NonPublicParent.java

package foo;

class NonPublicParent {

    public void aMethod() {
        System.out.println("method1");
    }

}


PublicChild.java

package foo;

public class PublicChild extends NonPublicParent {

}


FooMain.java

package foo;

import java.lang.reflect.Method;

public class FooMain {

    public static void main(String[] args) throws Exception {

        final PublicChild pc = new PublicChild();

        // --------------------------------------
        // 普通に呼び出す。当然ながら正常動作する
        // --------------------------------------
        pc.aMethod();

        // --------------------------------------
        // リフレクションで呼び出す。こちらも正常動作する。
        // --------------------------------------
        Method aMethod = PublicChild.class.getMethod("aMethod", new Class[]{});

        aMethod.invoke(pc, new Object[]{});
    }

}


BarMain.java(パッケージが異なることに注意)

package bar;

import java.lang.reflect.Method;

import foo.PublicChild;

public class BarMain {

    public static void main(String[] args) throws Exception {

        final PublicChild pc = new PublicChild();

        // --------------------------------------
        // 普通に呼び出す。当然ながら正常動作する
        // --------------------------------------
        pc.aMethod();

        // --------------------------------------
        // リフレクションで呼び出す。動作しない。
        // --------------------------------------
        Method aMethod = PublicChild.class.getMethod("aMethod", new Class[]{});

        aMethod.invoke(pc, new Object[]{}); // ←ここで例外
    }
}


このとき、FooMainの実行結果は以下のとおり。まあごく当たり前の結果。

method1
method1


しかし、BarMainの実行結果はこの通りだ。

method1
Exception in thread "main" java.lang.IllegalAccessException: Class bar.BarMain can not access a member of class foo.NonPublicParent with modifiers "public"
	at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:65)
	at java.lang.reflect.Method.invoke(Method.java:588)
	at bar.BarMain.main(BarMain.java:23)


何故かIllegalAccessExceptionが発生する。
ひとつ上で、実際にコンパイル可能なコードの実行が成功している(当たり前だが)のにもかかわらず。

原因考察

Java言語仕様的には、非publicクラスから継承したpublicメンバーは、子クラスのpublicメンバーとして扱われる。

だから、

pc.method1();

は何の問題もなくコンパイルが成功する。


最初は、通常のメソッド呼び出し"pc.aMethod"()はコンパイルもでき、もちろん正常に動作するのに、
リフレクションだと失敗するというのが信じられなかった。
自分が何か、致命的な勘違いをしているのではないか、と。


しかし試行錯誤の末、javacでコンパイルした場合は正常に動作することを発見し、それが突破口になった。

javapの比較

ecjでコンパイルしたPublicChild

Compiled from "PublicChild.java"
public class foo.PublicChild extends foo.NonPublicParent{
public foo.PublicChild();
  Code:
   0:	aload_0
   1:	invokespecial	#8; //Method foo/NonPublicParent."<init>":()V
   4:	return

}


javacでコンパイルしたPublicChild
(JDk1.6.0_u20)

Compiled from "PublicChild.java"
public class foo.PublicChild extends foo.NonPublicParent{
public foo.PublicChild();
  Code:
   0:	aload_0
   1:	invokespecial	#1; //Method foo/NonPublicParent."<init>":()V
   4:	return

public void aMethod();
  Code:
   0:	aload_0
   1:	invokespecial	#2; //Method foo/NonPublicParent.aMethod:()V
   4:	return

}


この通り、違いが明らかだ。
ecjのコンパイル結果にはaMethodが存在しない。

どうやらこの問題、

かなり昔にSunのメーリングリストでバグレポートが上がっていた模様。
例えばコレとか。


SunJDKでは当然ながら修正済みなんだが、ecjでは発生しているということは
もしかしてecjとjavacはコードベースが共通なのか?
歴史的にはそんなことはないハズなんだが。。。

この問題のソリューション


選択肢1: Eclipseコンパイラを使わず、antやmavenなどを使いjavacでコンパイルする
選択肢2: さっさとEclipse3.6へ乗り換える (Eclipse3.6ではこの問題が解消済みであることを確認)
選択肢3: setAccessible(true)を実行する
選択肢4: 非publicクラスにはpublicメンバーを宣言しない


正直、選択肢4は制約がきつすぎる。選択肢3はうっかりミスが怖い。

個人的には選択肢2をオススメするが、選択肢1もアリだろう。

ある意味、幸運かも

今回は、たまたま開発中のシステムでコレを見つけられたから良かったものの、
商用環境でこんなバグが出たらと思うとゾッとするよ、マジで。


20年前ならいざ知らず、現代のJavaシステムにおけるトラブルシューティングでは、
コンパイラが怪しい』なんて疑うことはまずない。そして、そんなこと言っても誰も信じてくれないだろう。
もし僕がそういう報告を受けたなら、まず先に担当者のトラブルシューティング能力の方を疑う。


ヤレヤレ。