日々常々

ふつうのプログラマがあたりまえにしたいこと。

EC2のAmazonLinuxにPayaraを放流してみた

前置き

EC2さん。便利ですよね。使ってますか?私はあまり使ってないです。別にほかのサービス使ってるとかでもないです。(ドヤ顔で目線を逸らしながら

たまに思い出したように触っては、ある程度使えるつもりになって。 しばらくしたら使い方を綺麗さっぱり忘れて、AWS Consoleにログインするたびにいろいろ変わってて戸惑うってのを繰り返しています。 いい加減にしたい。いや、これがいい加減なのか。

ふとしたことから雑なJavaEEアプリケーションを突っ込んだPayara microをEC2で動かしたいなーと思って、また思い出したようにEC2を触るわけで、また一からあれこれやってました。

理想と現実

理想

  • EC2インスタンスを立ち上げて
  • Payaraダウンロードしてきて
  • war放り込んで
  • 起動したら完了

現実

  • EC2立ち上げるかー。microでいいか……ん、そいやnanoとかあったね。知ってる知ってる。聞いた事はある。
  • jdkのインストールとか要ったね。あったあった。
  • payaraさんダウンロード、短縮URLだけど……まあいけるか。
  • コマンドわからん。wgetわからん。yumもわからん。 ← いつものこと
  • さあ起動だ!……ん?……例外? ← new

Payaraさんを放り込んだら例外吐いて死んだ

jdkのインストール

まずはいつものアレ。

$ sudo yum install java-1.8.0-openjdk-devel
$ sudo alternatives --config java
(なんか出てくるので1.8.0を選択)

1.8.0_65だった。まあいいか。

参考: Amazon LinuxでJava8/Tomcat8の環境を構築する

payaraのインストール

次に http://www.payara.fish/downloads からリンクをコピーしてきてwgetする。

$ wget http://bit.ly/1W9d2Lb

ファイル名が 1W9d2Lb になるけど気にしない。 適当にmvした。

payaraの起動

まだwarとか放り込んでないのでそのまま起動する。

$ java -jar payara-micro-4.1.1.154.jar

……ふつうはこれでいいのだけれど、こんなエラーになっちゃって。

$ java -jar payara-micro-4.1.1.154.jar
1 24, 2016 10:33:44 午前 com.sun.enterprise.v3.server.SystemTasksImpl setSystemPropertiesFromEnv
重大: Cannot determine host name, will use localhost exclusively
java.net.UnknownHostException: ip-XXX-XXX-XXX-XXX: ip-XXX-XXX-XXX-XXX: unknown error
    at java.net.InetAddress.getLocalHost(InetAddress.java:1505)
(中略)
    at fish.payara.micro.PayaraMicro.main(PayaraMicro.java:105)
  Caused by: java.net.UnknownHostException: ip-XXX-XXX-XXX-XXX: unknown error
    at java.net.Inet4AddressImpl.lookupAllHostAddr(Native Method)
(中略)
    at java.net.InetAddress.getLocalHost(InetAddress.java:1500)
    ... 29 more

(中略)

[2016-01-24T10:33:55.956+0000] [Payara 4.1] [INFO] [NCLS-JMX-00006] [javax.enterprise.system.jmx] [tid: _ThreadID=87 _ThreadName=Thread-12] [timeMillis: 1453631635956] [levelValue: 800] JMXStartupService has disabled JMXConnector system

Exception in thread "main" java.lang.NullPointerException
  at fish.payara.micro.services.PayaraMicroInstance.setInstanceName(PayaraMicroInstance.java:95)
    at fish.payara.micro.PayaraMicroRuntime.<init>(PayaraMicroRuntime.java:64)
    at fish.payara.micro.PayaraMicro.bootStrap(PayaraMicro.java:710)
    at fish.payara.micro.PayaraMicro.main(PayaraMicro.java:105)

とても悲しい。 起動はしてるのでサーバーのエラーページを表示することはできるのだけれど、--deployでwarを放り込んでも動いてくれない状態だった。

ホスト名が見つからないって言ってて、出てるのはローカルのホスト名。なので /etc/hostslocalhostに足してあげる。

$ hostname
ip-XXX-XXX-XXX-XXX
$ sudo vi /etc/hosts
127.0.0.1   localhost localhost.localdomain ip-XXX-XXX-XXX-XXX

参考: EC2 インスタンス上に Cassandra クラスタを構成する〜 java.net.MalformedURLException の対処 〜

よくわからんけど 動くようになったからよし。

放流

  • scpとかでwarを放り込んで
  • java -jar payara-micro-4.1.1.154.jar --deploy hoge.war で起動

これで完了。やったね。

コンストラクタのメソッド利用で注意すること

Java入門ではさらっと以下のように書いた、コンストラクタインスタンスメソッドを実行することについて掘り下げてみます。

コンストラクタからインスタンスメソッドを使用することは可能ですが、避けたほうが無難です。 コンストラクタの実行中はインスタンス自体が構築中のため、初期化が完了していない状態でメソッドが実行されることになります。

Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] (Software Design plus)

Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] (Software Design plus)

文章だけで伝えるのはなかなか難しいものだとも思いますし、 本に書いたのに実際にこの問題を見た時に即解決できなくて悔しかった ので、 突っ込んでしっかり書くことにしました。くそう。。。

簡単なサンプルコード

コンストラクタでのインスタンスメソッド呼び出しが問題を起こすコードは、以下のようになります。

/* このコードは動作しません */
class Hoge {
    final String str;

    Hoge () {
        invoke();
        this.str = "HOGERA";
    }

    void invoke() {
        System.out.println(str.length());
    }
}

new Hoge()とかするとNullPointerExceptionです。

Exception in thread "main" java.lang.NullPointerException
  at blog.constructor.Hoge.invoke
  at blog.constructor.Hoge.<init>

このくらい短いコードなら、流石にやらないでしょうが、 コンストラクタinvokeメソッドの間に数百行の隔たりがあればどうでしょう? フィールドはfinalだし、うっかり使ってしまうかもしれません。 作成時は問題がなかったとしても、不具合改修などでこの手の問題が起こしてしまったとしても、 その開発者を不注意だと責め立てることは、私には出来そうもありません。(自分もやっちゃうだろうし。)

少し複雑にしたサンプルコード

さらに問題を複雑にすると、冒頭のように1クラスに完結している形では問題なく動作しているにもかかわらず、 継承すると問題が起こるパターンも考えられます。擬似的なコードは次のようなものになります。

/* このコードは動作しません */
abstract class Parent {

    final String arg;

    Parent(String arg) {
        init();
        this.arg = arg;
    }

    abstract void init();
}

class Child extends Parent {

    Child(String arg) {
        super(arg);
    }

    @Override
    void init() {
        System.out.println(arg.toUpperCase(Locale.ROOT));
    }
}

new Child("xxx")とかでNullPointerExceptionになります。

Exception in thread "main" java.lang.NullPointerException
  at blog.constructor.Child.init
  at blog.constructor.Parent.<init>
  at blog.constructor.Child.<init>

少し説明落ち着いてコードを追えば、コンストラクタ引数がフィールドに格納される前に、 そのフィールドを参照しているので、フィールド型のデフォルト値(参照型なのでnullになる)となっているだけです。

しかし、このNullPointerExceptionに遭遇すると、おそらく「finalフィールドなのになぜ値が入っていないんだ?」となります。 コンストラクタの引数がnullになっていないかや、どこかで受け渡し損ねてないかなどを確認するかもしれません。 デバッガでinitメソッドにブレイクポイントを置いてみたりするかもしれません。いくら見ても、事実が示すようにフィールドはnullです。

継承階層が深くなると、メソッドの呼び出しタイミングがわからなくなっていきます。 全てのコードの詳細を把握することは困難ですし、全ての人にそれを求めるべきではありません。 上記のようにinitメソッドが拡張ポイントのように扱われていると、フレームワークコンストラクタの後に呼びだす初期化処理と想像しても、不思議ではないでしょう。

さらに複雑にした(略

もっと複雑に、もっとわかりにくくすることは可能です。もっともっと。 でも、書いても楽しくないので箇条書きで許してください。

  • コンストラクタで呼ばれるメソッド(上記だとinit)で例外が発生せず、そこから呼び出したメソッドで発生する。
  • コンストラクタで呼ばれるメソッドで生成されたインスタンスにフィールドのデフォルト値が設定される。
    • nullで即例外が発生すればいいのですが、生成されたインスタンスに意図せぬ値が入ったまましばらく経ち、別のタイミングで不正な状態で実行されることになります。
    • このパターンでさらに問題を起こすクラスのインスタンス生成箇所が多かったりすると、原因の特定難度はさらに上がります。
  • 対象がプリミティブ型
    • 参照型であればNullPointerExceptionが発生することで助かります(そう、例外が発生すればまだ助かるのです。少なくとも不正な更新を行ったりしてデータを壊す可能性は下がります。ぬるぽはいい子です。)が、プリミティブ型ならおそらくデフォルト値でそのまますんなり動いてしまいます。
  • フィールド参照ではなく、そのフィールドを使用する別のメソッドを使用する。
  • フィールドがfinalでなく、頻繁に書きかわる。

……こんなのデバッグしたくないです。

どうすれば?

Java入門ではこんな感じで書きました。

もしインスタンスメソッドを実行する必要があるのならば、privateメソッドやfinalメソッドのような 拡張されないメソッドにできないかを検討し、対象のメソッド内で他のメソッドを呼び出さないように気をつけましょう。

書いたことは書いたままなのですが、まだ他にも考えられます。

提供側として、どうすればよいか

もし実装クラスで拡張することを想定したインスタンスメソッドを、コンストラクタでどうしても実行したいのならば、どうすれば良いかを考えてみます。 まず、そのメソッドが何をするための拡張ポイントであり、どういうタイミングで実行されるかなど ドキュメントをしっかり書いてください 。 そして、一切のメンバを使用禁止にします。これは三階層以上の継承がある場合、二階層目のコンストラクタで初期化されるフィールドやそれを使用するメソッドが正常に動作しないタイミングで呼ばれてしまうためであり、そのことを具象化しようとしているクラスでの検知が困難だからです。 その上で、メソッドで使用する値は 全て メソッドの引数で渡しましょう。

これでようやく、気をつけて使っていれば大丈夫な可能性のある物体になるかもしれません。 しかし、間違った使い方のできるものは、間違った使い方をされるものです。 要約すると「やらせるな」ということです。考え直しましょう、真面目に。真剣に。悪いこと言わないから。

利用側として、どうすればよいか

さて、利用側の視点でどうすればいいかには触れてきませんでした。

これは いくら利用者が気をつけても、安全に設計できていないものを、安全に使用することはできない からです。 単純なケースであればテストコードなどで比較的容易に検出はできるのですが、「さらに複雑にした(略」あたりで書いているものは辛いです。

フレームワークやライブラリのコードを読むことは推奨したいのですが、必須ではありません。 特に仕事ではコストは確保されないでしょうし、そういうスキルも求められないでしょう。 なので、不具合に気づいたら修正依頼をするか、迂回するか、パッチを当てるくらいでしょうかね。

あとがき

Java入門 第2章「クラスを理解する」の最後の方に書いた コンストラクタでは注意してメソッドを使用する を掘り下げてみました。 コードある方がわかりやすいかな。どうだろう。どっちでもわかりにくいよ、と言われたらごめんなさいとしか言いようがない。 しかし全部このノリで書いたらページいくらあっても……難しい。

出版されてから1年経ちますが、内容はまだ古くなってません。 そのなかで、こんな感じの内容を入門と言い張ってます。入門だよ、うん。

バージョンアップ時に互換性を損ねてしまう変更

あるいは、利用者にバージョンアップをためらわせる変更の手引き。

前置き

うっかり互換性を損なってしまう変更について書いてみます。

知ってたらやらかさないし、なんてことのない事なのだけれど、 知らなかったら何が駄目なのかわかりづらいし、 原因を突き止めるのとかも難しいのかもしれない。と思ったので。

互換性の話なので、ライブラリやフレームワークなどの提供側としての変更になります。

動かない、けど再コンパイルすれば動く

こんなコードがあって、

interface Hoge<T extends java.io.Serializable> {
    T method();
}

これを使っている、こんなコードがある。

class Hogera {

  public static void main(String... args) {
    Hoge hoge = new Hoge<String>() {
      @Override
      public String method() { return "HOGE"; }
    };

    System.out.println(hoge.method());
  }
}

コンパイルして実行すれば、まあ普通に動くよね。

% javac Hoge.java Hogera.java
% java Hogera
HOGE

ここで、元のコードをこう変更しちゃう。

interface Hoge<T> {
    T method();
}

Hoge.java だけ再コンパイルして実行すると、動かない。

% javac Hoge.java
% java Hogera
Exception in thread "main" java.lang.NoSuchMethodError: Hoge.method()Ljava/io/Serializable;
  at Hogera.main(Hogera.java:9)

使う方のコードも再コンパイルするだけで動く。

% javac Hoge.java Hogera.java
% java Hogera
HOGE

説明

Javaは戻り値の型までを含めてメソッドは識別されます。 で、ジェネリクスで境界を使用していると、境界の型で扱われます。何もしていしてないと java.lang.Object ですね。 こういう時に型引数は考慮されません。つまり、 method()LT とかにはならないの。

分かりづらくするためにジェネリクス使ってるけど、単に戻り型を拡げるだけでも十分だったりする。

String method(); // 変更前
Object method(); // 変更後(違うメソッドになる)

昔、似たような事を書いた(しかしタイトル微妙だな……)のだけれど、 正直なところ当時の私の見通しは甘くて、単にビルドツールを使えば解決するって問題ではありません。 今回もjavaファイルを直接扱っているから分かりづらいのだけれど、たとえば Hoge がjarで、 知らない誰かが作っているものだったら、一緒にコンパイルーなんてしませんよね。

こういう変更が入っているかも、と思うとバージョンアップできなくなります。 たまに、依存ライブラリ間でこういうことが起こったりします。 その瞬間のソースを眺めて脳内コンパイルしてると普通に動いちゃうので、知らないと原因特定が難しかったりする困った話。 クラスファイルを読んでたらわかるだろうけど、普通は添付されてるソース読むよね?

動く、けど再コンパイルできない

こんなコードがあって、

class Fuga {

  static void method(String str) {
    System.out.println("FUGA");
  }
}

これを使っている、こんなコードがある。

class Fugera {

  public static void main(String... args) {
    Fuga.method(null);
  }
}

コンパイルして実行すれば、まあ普通に動くよね。

% javac Fuga.java Fugera.java
% java Fugera
FUGA

ここで、元のコードをこう変更しちゃう。

class Fuga {

  static void method(String str) {
    System.out.println("FUGA");
  }

  static void method(Number str) {
    System.out.println("FUGAGA");
  }
}

Fuga.java だけ再コンパイルして実行すると、これは普通に動く。

% javac Fuga.java
% java Fugera
FUGA

使う方のコードも再コンパイル……できない。

% javac Fugera.java
Fugera.java:4: エラー: methodの参照はあいまいです
    Fuga.method(null);
        ^
  Fugaのメソッド method(String)とFugaのメソッド method(Number)の両方が一致します
エラー1個

仕方ないので修正する。

class Fugera {

  public static void main(String... args) {
    Fuga.method((String)null);
  }
}

ダサい。。。 けれど、これでコンパイルできるようになるし、コンパイル後は元通り動作する。うっかりNumberにキャストしたりすると動作が変わるから、「とにかくコンパイルが通るようにすればいい」みたいな対応をすると二次災害になります。 バグ修正時のバグ混入率は通常よりも高いって言いますし、慌てずに落ち着いて対応しましょう。

説明

nullなんて嫌いだ! 最初のと同じ感じだったりします。Javaコンパイル時に呼び出す先のメソッドをコードよりも厳密に決定します。 メソッドオーバーロードしたとしても別のメソッドとして扱うため、コンパイルできないようなオーバーロードを行っても、 コンパイル後のclassが実行される際は問題となりません。

既存のメソッドを修正するときは気をつけられるけれど、 メソッドを追加して今までコンパイルできていたものができなくなるってのは、油断しがちかもしれない。 そしてまだ同じクラスなら話は簡単なのだけれど、たとえばインタフェースに同じ名前のdefaultメソッドが足されたら同じことが起こる。 メソッド足すときも、やっぱり注意しなきゃです。

動作するかと、コンパイルできるかについて書いた。 2軸があるなら、4パターン出るから、あと2パターンも当然あります。 同じように書こう……と思ったのだけれど……

動かないし、再コンパイルもできない

何の互換も残ってないような。変更というより、破壊?

動くし、再コンパイルしても動く

普通に互換性保ててるから問題ないよね?

互換性、互換性、、、

現実で見たことのあるものから二つを挙げてみたけれど、もちろん互換性を損ねる変更なんてのはこれだけではありません。 というか、変更は必ず何かしらの互換性を損ねるでしょう。 先に挙げた通り、具象メソッドの追加も安全ではありません。 可視性を緩めることで損なわれる互換性もあります。

こういう変更をしてしまうと、一番最初に書いたように、利用者はバージョンアップをしたくなくなります。 すなわち、動いているものを変更したくなくなります。 なので提供側ならば全力で避けねばなりません。

だからといって、ありとあらゆる互換性を維持するなんて愚かな目標を立てるべきだと言いたいわけではありません。 それは変更を、つまり成長を阻害することとイコールになります。

そんな役に立たない妄想よりも、 変更がどのような互換性を損ねるかを把握し、 問題があった時にどう対応すれば良いか、その対応にどのくらいかかりそうか とかを 考えといた方がいいんじゃないかな、と思うわけです。