Asakusa Direct I/O formatted textの紹介

はじめに

このエントリは Asakusa Framework Advent Calendar 2017 の20日目のエントリです。実は 2 年ぶりくらいにお仕事で Asakusa Framework を使った開発をしているので、今年は参加することにしました。

Asakusa Framework は 6 年前の初期リリースから継続して機能の追加を積極的に行っています。先ほど 2 年ぶりに Asakusa で開発していると書きましたが、その間にも色々進化していて、確実に便利になっていると感じました。

今回はその中の Direct I/O formatted text について紹介したいと思います。これはバージョン 0.9.1 になって追加された、かなり新しい機能です。

Direct I/O は HDFSAWS S3、GCS といった分散ファイルシステム上に保存されたファイルを透過的に読み書きする機能ですが、これまで次のようなファイルの種類に応じてデータモデルへのマッピングを行う機能が用意されていました。

  • Direct I/O CSV
  • Direct I/O TSV
  • Direct I/O line

DirectI/O formatted text はこれまでファイルの種類に応じて別々の API を用いていたところに統一的な機能を提供し、さらに機能追加も行われた強力なライブラリになっています。次のような強みがあります。

  • より多様なデータ形式に対応できる
  • 不整合データに対して柔軟な操作設定ができる
  • 読み込み時にファイルについてのメタ情報を入れられる

詳細についてはドキュメントをじっくり読んでもらうとして、ここでは個人的に嬉しかった点を挙げていきたいと思います。

嬉しかった点その1

「きちんとしていないデータに対してかなり無理が利く」という点です。

例えば、複数のシステム間で連携をするとします。各システムにサービスインターフェースが用意されていればいいですが、そうも行かず、いわゆるファイル連携をすることも多いと思います。

このファイル連携、とにかく泥臭い対応が必要になることが多いですよね。連携先の各システムに「CSV ファイルで連携しましょう」って声を掛けても次のように綺麗なデータが来るとは限らないことが多かったりしますよね。

  • 改行コード、エンコーディングがばらばら
  • 同じデータに対応したものなのにヘッダがあったりなかったり
  • 固定長ファイルを単純に CSV 化したのか、各項目が空白埋めでご丁寧に桁を揃えられているとか
  • 行によってカラム数が異なる CSV 的なものが出てくるとか
    • 本体のデータ部分以外に見出しや合計行なんかも 1 つのファイルに混じっている例を見たことがあります...

Direct I/O formatted text はかなり柔軟な読み込みオプションがあり、このような汚いファイルに対してもかなり無理が利くようになっています。

Direct I/O formatted text を使うためには DMDL のモデル定義に @directio.text.tabular (CSV以外の形式のファイルを扱う場合に使い、区切り文字を指定する) もしくは @directio.text.csv 属性を指定します。

@directio.text.csv(
    charset = "Windows-31J",
    header = skip,
    trim_input = true,
    true_format = "1",
    false_format = "0",
    date_format = "yyyy/MM/dd",
    datetime_format = "yyyy/MM/dd HH:mm:ss",
    on_more_input = report,
    on_less_input = ignore
)
data_model = {

};

属性に指定しているオプションに注目してください。 charset 指定があるのは当然のこととして、どの値を真偽値としてマッピングするか日付時刻のフォーマットも指定可能です。

注目してほしいのは header 指定で、読み込みや書き込みにおいてのヘッダ指定を次のようにかなり柔軟に指定可能です。

指定値 内容
nothing 何もしない (ヘッダ行が無いものとして読み込み、ヘッダを出力しない)
force ヘッダが存在しているものとして読み込み (1行スキップする) 、ヘッダを出力する
skip 読み込み時にヘッダが あれば スキップし、ヘッダは出力しない
auto 読み込み時にヘッダが あれば スキップし、ヘッダを出力する

skipauto といった、ヘッダのあるなしをよしなに扱ってくれます。こんな気配りができるライブラリは割と珍しいと思いませんか。

trim_input オプションは先頭や末尾に空白文字が入っていると除去してくれます。これでわざわざ @Update 演算子を使わなくてもいいですね。

on_more_inputon_less_input なんてオプションもあります。前者は読み込み時にレコードに余計なフィールドがあった場合の動作を、後者はレコードのフィールドが不足している場合の動作を指定できるようになっており、少々変なデータでも対処できるようになっています。この手のライブラリは普通例外を飛ばして終わりということが多いですよね。

on_more_input の場合の指定内容は次の通りです。

指定値 内容
error エラーログを出して異常終了する
report 警告ログを出力した上で無視する
ignore 単に余剰フィールドを無視する

on_less_input の場合の指定内容は次の通りです。

指定値 内容
error エラーログを出して異常終了する
report 警告ログを出力した上で不足フィールドに NULL を入れる
ignore 不足フィールドに NULL を入れる

これで行によってカラム数が異なる CSV 的なもへの対処とかもできそうですよね。

嬉しかった点その2

「読み込み時にファイルのメタ情報をデータモデルに追加できる」という点です。

データフィールド属性に次のような読み込んだファイルに関する情報を埋め込むためのものが用意されています。

属性 内容
@directio.text.file_name 属性を指定されたフィールドにファイルパスを設定する
@directio.text.line_number 属性を指定されたフィールドに当該データのファイルの行番号を設定する (物理的な行番号であるため、途中で改行の入っているレコードが存在した場合、次のレコードは番号がスキップすることになる)
@directio.text.record_number 属性を指定されたフィールドに当該データのファイルのレコード番号を設定する (こちらは論理的な行番号)

でファイルのデータチェックで、ファイル名や行番号の情報も出力したいような場合でも Asakusa 上で実行できるようになりますね。

終わりに

とりあえずはこんなところです。他にも多彩な機能が用意されているので、詳しくはドキュメントを読んでください。

Asakusa Framework は開発の現場で遭遇する「これがあったらいいな」的な機能を結構貪欲に取り込み続けています。取り込んで欲しい機能があったら MLGitHub (日本語でも OK ですよ) などで積極的に提案してみることをお勧めします。

Bean ValidationのJavaFX対応

このエントリは JavaFX Advent Calendar 2017 の 19 日目のエントリです。前日は id:planet-az さんによる「 簡単なミュージックプレーヤーをさらにいじってみた 」でした。

はじめに

今回取り上げるのは、 JSR 380 Bean Validation 2.0JavaFX 対応についてです。そうです、Java EE に含まれる Bean Validation が JavaFX に対応したのです! まさか EE の仕様が JavaFX のことを考慮するとは思わなかったので、これは非常に驚きました。

具体的には JavaFX のプロパティ API に対して Bean Validation が対応しました。このエントリではその使い方について紹介したいと思います。

Bean Validation 2.0 について

Bean Validation 2.0 は Java EE 8 に含まれる仕様です。以下の Web サイトに Specification がまとめられています。

http://beanvalidation.org/2.0/spec/

新機能の 1 つとして次のようなものが挙げられています。

  • Support for validating container elements by annotating type arguments of parameterized types, e.g. List<@Positive Integer> positiveNumbers (see Container element constraints); this also includes:
    • More flexible cascaded validation of collection types; e.g. values and keys of maps can be validated now: Map<@Valid CustomerType, @Valid Customer> customersByType
    • Support for java.util.Optional
    • Support for the property types declared by JavaFX
    • Support for custom container types by plugging in additional value extractors (see Value extractor definition)

新たにコンテナ要素のバリデーションに対応しているとあります。コレクションや Optional オブジェクトなどの中身に対してバリデーションを掛けられるようになったのです。そしてこの対応の一環として、 JavaFX のプロパティもデフォルトで対応するようになったのです。 *1

JavaFX アプリケーションでの Bean Validation の使い方

それでは早速使ってみます。折角 JavaFX を使っているので、バインドを活用してみることにします。

作成するサンプルアプリケーション

作ってみるのは次のようなユーザ登録フォームを想定したものです。

f:id:aoe-tk:20171218231023p:plain

テキストフィールドに文字を打ち込むたびにバリデーションを行い、正しい文字列が入力されたタイミングでエラーメッセージが消えるようになります。E-mail、パスワードどちらも OK になれば登録ボタンがクリック可能になります。

f:id:aoe-tk:20171218231509g:plain

E-mail 欄のバリデーションには Bean Validation 2.0 で新たに入った @Email を使ってみることにします。

依存性の設定

まず、JavaFX アプリケーションで Bean Validation を使えるようにします。参照実装である Hibernate Validator を使います。Maven だと pom.xml に次のように依存設定を記述します。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

<%-- Snip --%>

    <dependencies>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.4.Final</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.el</artifactId>
            <version>3.0.0</version>
        </dependency>
    </dependencies>

<%-- Snip --%>

</project>

Model、FXML の実装

Model のコードは次のようになります。

package aoetk.sample.beanvalidation;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

public class Model {
    @NotEmpty
    @Email
    private StringProperty email = new SimpleStringProperty("");

    @Size(min = 8)
    private StringProperty password = new SimpleStringProperty("");

    public String getEmail() {
        return email.get();
    }

    public StringProperty emailProperty() {
        return email;
    }

    public void setEmail(String email) {
        this.email.set(email);
    }

    public String getPassword() {
        return password.get();
    }

    public StringProperty passwordProperty() {
        return password;
    }

    public void setPassword(String password) {
        this.password.set(password);
    }
}

フィールドに着目してください。このように JavaFX プロパティに対して直接 Bean Validation のアノテーションを指定可能になっています。

次に View としての FXML のコードを示します。

<?xml version="1.0" encoding="UTF-8"?>

<%-- import略 --%>
<GridPane hgap="10.0" vgap="10.0" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml/1" fx:controller="aoetk.sample.beanvalidation.Controller">
   <rowConstraints>
      <RowConstraints minHeight="10.0" />
      <RowConstraints minHeight="10.0" />
      <RowConstraints minHeight="10.0" />
   </rowConstraints>
   <columnConstraints>
      <ColumnConstraints hgrow="NEVER" minWidth="10.0" />
      <ColumnConstraints hgrow="ALWAYS" minWidth="10.0" />
   </columnConstraints>
   <children>
      <Label text="E-mail" />
      <Label text="Password" GridPane.rowIndex="1" />
      <Button fx:id="registerButton" mnemonicParsing="false" text="Register" GridPane.columnSpan="2" GridPane.halignment="CENTER" GridPane.rowIndex="2" />
      <VBox GridPane.columnIndex="1">
         <children>
            <TextField fx:id="emailField" prefHeight="27.0" prefWidth="350.0" />
            <Label fx:id="emailErrorLabel" text="Label" textFill="red" />
         </children>
      </VBox>
      <VBox GridPane.columnIndex="1" GridPane.rowIndex="1">
         <children>
            <PasswordField fx:id="passwordField" />
            <Label fx:id="passwordErrorLabel" text="Label" textFill="red" />
         </children>
      </VBox>
   </children>
   <padding>
      <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
   </padding>
</GridPane>

Scene Builder で見ないとちょっと分かりにくいかも知れませんね。入力フィールドに加え、エラーメッセージについても fx:id 属性を与えて Controller 側でアクセスできるようにしている点に注目してください。

Controller でのバインド、バリデーション実装

そして、この Model と View を Controller でバインドし、さらにフィールドに対して入力が発生した場合にバリデーションが実行されるようにします。

// (import略)
public class Controller implements Initializable {
    @FXML
    Label emailErrorLabel;

    @FXML
    Label passwordErrorLabel;

    @FXML
    Button registerButton;

    @FXML
    TextField emailField;

    @FXML
    PasswordField passwordField;

    private StringProperty emailMessage = new SimpleStringProperty("");

    private StringProperty passwordMessage = new SimpleStringProperty("");

    private Model model = new Model();

    private Validator validator;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        validator = validatorFactory.getValidator();
        validateModel();

        emailErrorLabel.textProperty().bind(emailMessage);
        passwordErrorLabel.textProperty().bind(passwordMessage);
        registerButton.disableProperty().bind(emailMessage.isNotEmpty().and(passwordMessage.isNotEmpty()));
        emailErrorLabel.managedProperty().bind(emailMessage.isNotEmpty());
        passwordErrorLabel.managedProperty().bind(passwordMessage.isNotEmpty());
        model.emailProperty().bind(emailField.textProperty());
        model.passwordProperty().bind(passwordField.textProperty());
        emailField.textProperty().addListener(observable -> validateModel());
        passwordField.textProperty().addListener(observable -> validateModel());
    }

    private void validateModel() {
        final Set<ConstraintViolation<Model>> violations = validator.validate(model);
        emailMessage.set("");
        passwordMessage.set("");
        for (ConstraintViolation<Model> violation : violations) {
            if (violation.getPropertyPath().toString().equals("email")) {
                emailMessage.set(violation.getMessage());
            } else if (violation.getPropertyPath().toString().equals("password")) {
                passwordMessage.set(violation.getMessage());
            }
        }
    }
}

initialize() メソッドの最初で Validator オブジェクトを取得している点については特に説明不要ですね。その後すぐに validateMode() メソッドをコールして初期状態でのバリデーションを実行しています。

エラーメッセージは Validator によるバリデーションの結果出力されたメッセージをそのまま使うようにしました。メッセージを格納する StringProperty オブジェクトを Controller のフィールドとして持たせ、それをエラーメッセージの Label とバインドさせています。

mailErrorLabel.textProperty().bind(emailMessage);
PasswordErrorLabel.textProperty().bind(passwordMessage);

また、このメッセージが空文字の場合はボタンをクリック可能にし、エラーメッセージラベルもシーングラフから取り除くようにしました。これも次のようにバインドを活用しています。
(managed プロパティを false 設定すると一時的にシーングラフから取り除かれます)

registerButton.disableProperty().bind(emailMessage.isNotEmpty().and(passwordMessage.isNotEmpty()));
emailErrorLabel.managedProperty().bind(emailMessage.isNotEmpty());
passwordErrorLabel.managedProperty().bind(passwordMessage.isNotEmpty());

そして、テキストボックスやパスワードフィールドの入力内容を Model のプロパティに反映させるためにバインドします。

model.emailProperty().bind(emailField.textProperty());
model.passwordProperty().bind(passwordField.textProperty());

テキストボックスやパスワードフィールドの textProperty をリッスンし、文字列が入力されるたびにバリデーションを発火します。

emailField.textProperty().addListener(observable -> validateModel());
passwordField.textProperty().addListener(observable -> validateModel());

最後にバリデーションを実行する validateModel() の実装を見てみます。モデルに対してバリデーションを実行し、結果として返ってきた ConstraintViolation オブジェクトのプロパティ名をチェックし、対応するプロパティ用のエラーメッセージ StringProperty オブジェクトにメッセージをセットしています。 後はバインドの仕組みで自動的にメッセージ表示/非表示やボタンのクリック可/不可が制御されます。

private void validateModel() {
    final Set<ConstraintViolation<Model>> violations = validator.validate(model);
    emailMessage.set("");
    passwordMessage.set("");
    for (ConstraintViolation<Model> violation : violations) {
        if (violation.getPropertyPath().toString().equals("email")) {
            emailMessage.set(violation.getMessage());
        } else if (violation.getPropertyPath().toString().equals("password")) {
            passwordMessage.set(violation.getMessage());
        }
    }
}

JavaFX のバインドの仕組みを活用することで、プレゼンテーションサイドのロジックがかなり宣言的になっていることが分かると思います。

このようにして先のスクリーンショットに示したような動作を実現しました。

コードの全体は GitHub にアップしています。

https://github.com/aoetk/javafx-beanvalidation-sample

まとめ

以上に示したように、Bean Validation 2.0 では JavaFX のプロパティを対象としたバリデーションが可能になりました。バインドなど JavaFX の機構を活用して、ユーザに対して応答性の良いバリデーションの仕組みを実装できるようになります。

今回はキー入力のタイミングでバリデーションを行ってみましたが、もちろん他にもフォーカスアウトで実行する、(多くの Web アプリのように) ボタンクリックのタイミングで実行する、あるいはバリデーションエラーを受け取ったときは入力をキャンセルしてそもそも入力できない文字は入れられないようにする...などなど色んな実装方法が考えられます。皆さんなりのやり方で色々試してみるといいでしょう。

明日は...まだ埋まっていないな。みんな書いて書いて!

*1:Value extractor を実装することでカスタムのコンテナに対するバリデーションを行うことも可能になります

JavaFXアプリケーションでJava 9のモジュールを使うときの注意点

このエントリは JavaFX Advent Calendar 2017 の 1 日目のエントリです。最初を飾るのは初めてです。まだ参加者が少ないので、みんな参加してね!

いよいよJava SE 9がリリースされました。やはり9の注目はJigsawことモジュールシステムですね。特にJavaFXにとってはjavapackagerを用いてパッケージングした際の配布サイズを小さくすることができるので、特に重要ですね。

aoe-tk.hatenablog.com

ですが、JavaFXアプリケーションでモジュールシステムを利用すると早速引っ掛かるポイントがあります。それはFXMLです。

次のような FXML を使った簡単なアプリケーションを見てみましょう。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/9" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
   <center>
      <StackPane prefHeight="150.0" prefWidth="200.0" BorderPane.alignment="CENTER">
         <children>
            <Label text="This is Label." fx:id="label" />
         </children>
      </StackPane>
   </center>
   <bottom>
      <HBox alignment="CENTER" BorderPane.alignment="CENTER">
         <children>
            <Button alignment="CENTER" mnemonicParsing="false" text="Button" onAction="#click" />
         </children>
         <BorderPane.margin>
            <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
         </BorderPane.margin>
      </HBox>
   </bottom>
</BorderPane>
package sample;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class Controller {
    @FXML
    private Label label;

    @FXML
    void click(ActionEvent event) {
        label.setText("You clicked!");
    }
}

FXMLのいいところとして、ビューXMLとペアになるControllerクラスに対して、XML側に記述したコンポーネントへのアクセスをDIを使って提供している点ですね。publicなフィールドだけでなく、package privateやprivateなフィールドとしての宣言も可能で、その場合は @FXML アノテーションを付与すると、インスタンスがインジェクトされるようになります。イベント実行メソッドも同様に @FXML アノテーションでバインドできます。

ですが、この @FXML アノテーションを使う場合は注意が必要です。次のように module-info.java を作ってみましょう。

module fxml9sample {
    requires javafx.controls;
    requires javafx.fxml;
    exports sample;
}

すると次のような例外が飛びます。 javafx.fxml モジュールに属するクラスがこのControllerクラスに対してリフレクションによるアクセスを試みるためです。

Exception in Application start method
java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
(中略)
Caused by: javafx.fxml.LoadException: 
fxml9sample/out/production/fxml9sample/sample/sample.fxml:15

    at javafx.fxml/javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2625)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2603)
(中略)
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private javafx.scene.control.Label sample.Controller.label accessible: module fxml9sample does not "opens sample" to module javafx.fxml
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:337)
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:281)
    at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:176)
    at java.base/java.lang.reflect.Field.setAccessible(Field.java:170)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.addAccessibleFields(FXMLLoader.java:3495)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.access$3900(FXMLLoader.java:3344)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor$1.run(FXMLLoader.java:3460)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor$1.run(FXMLLoader.java:3456)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.addAccessibleMembers(FXMLLoader.java:3455)
    at javafx.fxml/javafx.fxml.FXMLLoader$ControllerAccessor.getControllerFields(FXMLLoader.java:3394)
    at javafx.fxml/javafx.fxml.FXMLLoader.injectFields(FXMLLoader.java:1170)
    at javafx.fxml/javafx.fxml.FXMLLoader.access$1600(FXMLLoader.java:105)
    at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processValue(FXMLLoader.java:865)
    at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:759)
    at javafx.fxml/javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2722)
    at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2552)
    ... 17 more
Exception running application sample.Main

これを防ぐためには、FXMLと対になるControllerクラスが属しているパッケージに対して javafx.fxml モジュールからのリフレクションによるアクセスを許可する必要があります。そのために次のように opens 文を使って javafx.fxml モジュールに対してアクセスを許可する必要があります。

module fxml9sample {
    requires javafx.controls;
    requires javafx.fxml;
    exports sample;
    opens sample to javafx.fxml; // <- これを追加
}

既存のJavaFXアプリケーションをJava 9に移行する際に意外と引っ掛かるポイントなので注意してください。

というわけで、1 日目のエントリは小ネタでした。19 日にも書く予定ですが、そちらはもう少し大きなネタにする予定です。

明日は...執筆時点でまだいない! 誰か書いてー。

JDK9でのjavapackagerについて

はじめに

以前自分の blog にて JDK に付属しているツールである javapackager について紹介したことがあります。このツールは主にクライアントサイド Java アプリケーションを配布可能な形態でパッケージングするためのツールです。ネイティブインストーラも生成することができます。

aoe-tk.hatenablog.com

このエントリではネイティブパッケージに含まれるランタイムについて、次のようなことを述べていました。

昔は JDK を丸ごと放り込むという豪快な感じになっていましたが、最近は結構スリムアップしました。JDK9 の Jigsaw が入るともっと効率よくなるでしょう。

そして遂に Java9 がリリースされました。JDK9 の javapackager のマニュアル には次のような記載があります。

For self-contained applications, the Java Packager for JDK 9 packages applications with a JDK 9 runtime image generated by the jlink tool.

確かに jlink と連動してランタイムイメージを作ると記載されています。つまり、module-info.java を作っていれば、必要なモジュールだけを含んだランタイムイメージを作成することになり、アプリケーションの配布サイズが小さくなることが期待されます。というわけで早速 JDK9 の javapackager を試してみることにしました。

パッケージ対象となるアプリケーション

まず、パッケージ対象となるアプリケーションを作ります。とてもシンプルな JavaFX アプリケーションとして作ります。

package aoetk.sample;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class SampleApp extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception{
        primaryStage.setTitle("Packager Sample");
        StackPane stackPane = new StackPane();
        stackPane.getChildren().add(new Text("Packager Sample"));
        primaryStage.setScene(new Scene(stackPane, 300, 275));
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

ウィンドウの真ん中にテキストを表示するだけのとてもシンプルな JavaFX アプリケーションです。これを対象にパッケージングを行ってみます。

JDK8でのパッケージング

まず、JDK8 でコンパイルし、パッケージングしてみます。パスを JDK8 に通します。

>set PATH=C:\Program Files\Java\jdk1.8.0_144\bin;%PATH%

>javac -version
javac 1.8.0_144

>where javapackager
C:\Program Files\Java\jdk1.8.0_144\bin\javapackager.exe

JDK8 の javac でコンパイルし、JAR を作ります。

>javac -d bin src\aoetk\sample\SampleApp.java

>javapackager -createjar -nocss2bin -appclass aoetk.sample.SampleApp -srcdir bin -outdir artifact -outfile packager-sample.jar

>dir artifact
 ドライブ C のボリューム ラベルは Windows です
 ボリューム シリアル番号は ACFE-5623 です

 C:\Users\aoe\develop\packager-sample\artifact のディレクトリ

2017/10/08  00:32    <DIR>          .
2017/10/08  00:32    <DIR>          ..
2017/10/08  00:32             1,354 packager-sample.jar
               1 個のファイル               1,354 バイト
               2 個のディレクトリ  366,187,679,744 バイトの空き領域

この JAR に対し、ネイティブインストールイメージを作ります。今回はインストール後のイメージサイズを知りたいので、 -native の引数に image を渡してインストールイメージのみを作ります。

>javapackager -deploy -native image -outdir package -outfile packager-sample -srcdir artifact -srcfiles packager-sample.jar -appclass aoetk.sample.SampleApp -name "jdk8-sample" -title "JDK8Sample" -BappVersion=1.0 -Bwin.menuGroup="JDK8Sample"
アプリケーション・バンドルを作成しています: C:\Users\aoe\develop\packager-sample\package内のjdk8-sample
"モジュール: [java.rmi, java.sql, javafx.web, jdk.charsets, java.logging, java.xml.crypto, java.xml, jdk.xml.dom, jdk.jfr, java.datatransfer, jdk.packager.services, jdk.httpserver, javafx.base, jdk.net, java.desktop, java.naming, javafx.controls, java.prefs, java.security.sasl, jdk.naming.rmi, jdk.zipfs, java.base, jdk.crypto.ec, jdk.management.agent, java.management, java.sql.rowset, javafx.swing, jdk.jsobject, jdk.sctp, java.smartcardio, jdk.unsupported, jdk.jdwp.agent, jdk.scripting.nashorn, java.instrument, java.security.jgss, jdk.management, java.compiler, javafx.graphics, jdk.security.auth, java.scripting, javafx.fxml, jdk.dynalink, javafx.media, jdk.accessibility, java.management.rmi, jdk.naming.dns, jdk.security.jgss, jdk.localedata]をランタイム・イメージに追加しています。"
警告: Windows Defenderが原因でJavaパッケージャが機能しないことがあります。問題が発生した場合は、リアルタイム・モニタリン グを無効にするか、ディレクトリ"C:\Users\aoe\AppData\Local\Temp\"の除外を追加することにより、問題に対処できます。
結果のアプリケーション・バンドル: C:\Users\aoe\develop\packager-sample\package

これで jdk8-sample ディレクトリの下にインストールイメージが作られます。これはインストール後にインストールディレクトリに展開される構成そのものです。

f:id:aoe-tk:20171009000534p:plain

ディレクトリサイズは次のように 167MB と、単純なアプリケーションにしては随分大きなサイズになっていることが分かります。

f:id:aoe-tk:20171009000554p:plain

JDK9でのパッケージング

では Java9 で導入されたモジュールシステムを利用してパッケージングしてみることにしましょう。環境を JDK9 に変更します。

>set PATH=C:\Program Files\Java\jdk-9\bin;%PATH%

>javac -version
javac 9

>where javapackager
C:\Program Files\Java\jdk-9\bin\javapackager.exe

モジュールの設定を行います。まずはこのアプリケーションがどのモジュールを利用しているかを調べてみましょう。

>jdeps -s artifact\packager-sample.jar
packager-sample.jar -> java.base
packager-sample.jar -> javafx.base
packager-sample.jar -> javafx.graphics

java.base モジュールの他に javafx.base モジュール、 javafx.graphics モジュールに依存していることが分かります。 javafx.graphics モジュールは javafx.base モジュールに依存しているので module-info.java は次のように javafx.graphics モジュールへの依存を記載すれば OK です。

module aoetk.sample.packager {
    requires javafx.graphics;
    exports aoetk.sample;
}

これを JDK9 のコンパイラを使ってコンパイルし、JAR を作ります。

>javac -d bin src\module-info.java src\aoetk\sample\SampleApp.java

>javapackager -createjar -nocss2bin -appclass aoetk.sample.SampleApp -srcdir bin -outdir artifact -outfile packager-sample.jar

>dir artifact
 ドライブ C のボリューム ラベルは Windows です
 ボリューム シリアル番号は ACFE-5623 です

 C:\Users\aoe\develop\packager-sample\artifact のディレクトリ

2017/10/08  00:52    <DIR>          .
2017/10/08  00:52    <DIR>          ..
2017/10/08  00:52             1,642 packager-sample.jar
               1 個のファイル               1,642 バイト
               2 個のディレクトリ  366,004,936,704 バイトの空き領域

これを同じように javapackager を用いてインストールイメージを作るのですが、モジュールを使ったアプリケーションをパッケージングする場合は指定する引数が異なります。ですが、現在の javapackager のマニュアル はこの変更に追いついていません...。執筆時点で javapackager のコマンドラインオプションについて正確な記述があったのは JEP 275: Modular Java Application Packaging のみでした。

具体的には -srcdir-srcfiles-appclass の指定が無くなり、モジュールを使った Java アプリケーションを java コマンドで実行するときと同じようにモジュールパス (対象モジュールの JAR が置かれているディレクトリ) を -p (もしくは --module-path) で、実行クラスを -m (もしくは --module) でモジュール名とクラス名を組み合わせて指定します。

>javapackager -deploy -native image -outdir package -outfile packager-sample -p artifact -m aoetk.sample.packager/aoetk.sample.SampleApp -name "jdk9-sample" -title "JDK9Sample" -BappVersion=1.0 -Bwin.menuGroup="JDK9Sample"
アプリケーション・バンドルを作成しています: C:\Users\aoe\develop\packager-sample\package内のjdk9-sample
モジュールaoetk.sample.packagerは存在しません。
"モジュール: [aoetk.sample.packager]をランタイム・イメージに追加しています。"
モジュールaoetk.sample.packagerは存在しません。
警告: Windows Defenderが原因でJavaパッケージャが機能しないことがあります。問題が発生した場合は、リアルタイム・モニタリン グを無効にするか、ディレクトリ"C:\Users\aoe\AppData\Local\Temp\"の除外を追加することにより、問題に対処できます。
結果のアプリケーション・バンドル: C:\Users\aoe\develop\packager-sample\package

これで jdk9-sample ディレクトリの下にインストールイメージが作られます。

f:id:aoe-tk:20171009000635p:plain

ディレクトリサイズを見ると 84.9MB と JDK8 の場合に比べて半分程度になっていることが分かります。確かに Jigsaw の効果が出ていますね!

f:id:aoe-tk:20171009000656p:plain

インストールイメージの中身について

なお、インストールイメージの中身を調べてみると、こちらも興味深いものがありました。 アプリケーションや JRE の JAR が見当たらない のです! runtime ディレクトリの下を覗いてみると、何やら modules というそれっぽいファイルがあります。

f:id:aoe-tk:20171009000713p:plain

このファイル、JAR か JMOD ファイルかと思いきや、特に ZIP 圧縮されていません。バイナリエディタで覗いてみると時々 CAFEBABE が登場しており、単にクラスファイルを 1 つのファイルにまとめたもののように見受けられます。色々調べてみましたが、このファイルが何であるかの解説を見つけられませんでした。誰か知っている人いますか?

(2017/10/10) 追記

id:MATSUZAKI 様よりコメントで情報を頂きました。jimage 形式のファイルであるとのことです。jimage については Java Magazine の Vol.25 に説明がありました。

jimage形式は、モジュール化されたランタイムに必要なクラスやリソースを管理するコンテナの形式です。 jimageファイルは、従来のようなzipベースの圧縮ではなく、クラスやリソースを高速に検索できるようにインデックスが付けられています。jimageのコンテンツ領域には、そのイメージのすべてのクラスとリソースが含まれており、位置情報にひも付けて管理されています。

JDK には jimage というコマンドがあったので (なお、現時点での JDK9 のドキュメントにはこのコマンドについての説明は見当たらず...) 、このコマンドで中身を閲覧してみました。

>jimage list modules
jimage: modules

Module: aoetk.sample.packager
    META-INF/MANIFEST.MF
    aoetk/sample/SampleApp.class
    module-info.class

Module: java.base
    META-INF/services/java.nio.file.spi.FileSystemProvider
    com/sun/crypto/provider/AESCipher$AES128_CBC_NoPadding.class
    com/sun/crypto/provider/AESCipher$AES128_CFB_NoPadding.class
    com/sun/crypto/provider/AESCipher$AES128_ECB_NoPadding.class
    ...

確かにクラスファイルやリソースファイルが含められていますね。

まとめ

ということで、Project Jigsaw の恩恵で、Java SE 9 からはアプリケーションのインストールイメージをより絞って配布が可能であることが分かりました。OracleOracle Java SEサポート・ロードマップ において、Java アプリケーションの配布は (予め配布先に Java をインストールさせるのではなく) JRE も一緒にバンドルした自己完結型パッケージングでの配布を推奨しています。jlink と javapackager を最大限に活用してきましょう。

Bash on Windows上のファイルをうっかりWindows側でいじってしまったときの対処

Windows10 になってから Linux サブシステムが搭載され、Windows 上で Bash on Ubuntu が動かせるようになりました。Windows 上で仮想環境ではない生の Linux 環境と Bash シェルが使えるようになってとっても嬉しいですね。
この Bash on Ubuntu on Windows を利用する上での注意点として Linux サブシステム上のファイルを Windows 側のアプリからいじってはいけない というものがあります。Linux 側のファイルのメタデータ情報は NTFS の拡張属性に保存していますが、Windows 側のアプリケーションでこのファイルを操作すると拡張属性に入っている情報を消してしまう可能性があるためです。
ですが、先日実際にそれをやらかしてしまい、ファイルシステムがおかしくなってしまって焦りました。何とか復帰できたので備忘録的にまとめておきました。

まず Bash on Ubuntu 上でファイルを作ってみます。 f:id:aoe-tk:20170921235906p:plain

このファイルをうっかり Windows 側のツールで開いて修正、保存しちゃいます。 f:id:aoe-tk:20170921235924p:plain f:id:aoe-tk:20170921235943p:plain

これをもう一度 Bash 側で見てみます。 f:id:aoe-tk:20170922000003p:plain

あれ、タイムスタンプもファイルサイズも変わっていない (ファイルが消えてしまう場合もあります) 。 Ubuntu 上の Vim で開いてみると Windows 側で追加した文字列が反映されていません。 f:id:aoe-tk:20170922000123p:plain

しかも書き換えようとしても保存に失敗します! f:id:aoe-tk:20170922000142p:plain

じゃあファイルを消して作り直そうと思ったら、ファイルを消したはずなのに「入力/出力エラーです」と出て、消したはずのファイルを作ることもできません! f:id:aoe-tk:20170922000206p:plain

実際にこの現象が起きたときはかなり焦りました。以下に対処方を示していきますね。

Windows の Explorer でファイルの状態を確認します。ただし、Linux サブシステムのファイルは「保護されたオペレーティングシステムファイル」に当たるため、デフォルトでは Explorer から見えないようになっています。Explorer の設定を変更し、「保護されたオペレーティングシステムファイルを表示しない」のチェックを外します。 f:id:aoe-tk:20170922000234p:plain

Linux サブシステムのファイルシステム%LOCALAPPDATA%\lxss 以下にあります。 f:id:aoe-tk:20170922000249p:plain

Explorer で問題となったファイルのあるパスを確認すると、消したはずのファイルが残っていることが分かります。これを Explorer 上で削除します。 f:id:aoe-tk:20170922000304p:plain

これで再び Ubuntu 上でファイルを作成できるようになります。やれやれ。 f:id:aoe-tk:20170922000334p:plain

復旧したら Explorer の設定は元に戻しておきましょう。またうっかり触ってしまわないように。

とまあこんな感じで直すことができました。このように色々おかしなことになるので、Windows 側のアプリから Linux サブシステム上のファイルをいじってしまわないように注意しましょう (逆は問題ありません) 。もうちょっと何とかならないかなあ。

getterやsetter以外のメソッドをJavaBeansのアクセッサにする

ネット上で、Java への dis として「なんで Java のプロパティアクセッサはわざわざ get/set で始まる名前のメソッドにしないといけないんだ!」というのをよく見かけます。ですが実は JavaBeans の仕様としてはいわゆる getter/setter でないメソッドもプロパティのアクセッサにすることができます。余りにも JavaBeans のことがボロクソに言われるのでかっとなって書きましたw

まずは以下の Java クラスを見てください。

/**
 * getter/setterを使わないJavaBeansの例.
 */
public class Test {
    private String わーい;

    public String すごーい() {
        return わーい;
    }

    public void たーのしー(String myProperty) {
        this.わーい = myProperty;
    }

    @Override
    public String toString() {
        return "Test{" +
                "わーい='" + わーい + '\'' +
                '}';
    }
}

いわゆる getter/setter がありませんね。このクラスを次のように Introspector に掛けてプロパティを抽出し、実行してみます。

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;

/**
 * Beanの利用例.
 */
public class JavaBeansSampleApp {
    public static void main(String[] args) {
        try {
            Test test = new Test();
            BeanInfo testBeanInfo = Introspector.getBeanInfo(test.getClass());
            PropertyDescriptor[] propertyDescriptors = testBeanInfo.getPropertyDescriptors();
            for (PropertyDescriptor pd : propertyDescriptors) {
                if ("わーい".equals(pd.getName())) {
                    System.out.println("わーいプロパティのsetter: " + pd.getWriteMethod());
                    System.out.println("わーいプロパティのgetter: " + pd.getReadMethod());
                    pd.getWriteMethod().invoke(test, "なにこれー?");
                }
            }
            System.out.println(test);
        } catch (IntrospectionException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

結果は次のようになります。

わーいプロパティのsetter: public void aoetk.sample.Test.たーのしー(java.lang.String)
わーいプロパティのgetter: public java.lang.String aoetk.sample.Test.すごーい()
Test{わーい='なにこれー?'}

set で始まっていない たーのしー() メソッドが setter として認識され、get で始まっていない すごーい() メソッドが getter として認識されています。プロパティの writeMethod を実行するとちゃんと たーのしー() メソッドが実行されて値が設定されています。

からくりについて解説しましょう。次のように Bean に対応した BeanInfo 実装クラスを用意し、Bean と同じパッケージに置きます。以下の例では BeanInfo インターフェースのデフォルトメソッドを実装した SimpleBeanInfo クラスを継承し、必要なメソッドだけ実装しています。

/**
 * {@link Test}のJavaBeans情報.
 */
public class TestBeanInfo extends SimpleBeanInfo {
    @Override
    public PropertyDescriptor[] getPropertyDescriptors() {
        try {
            return new PropertyDescriptor[]{
                    new PropertyDescriptor("わーい", Test.class, "すごーい", "たーのしー")
            };
        } catch (IntrospectionException e) {
            return null;
        }
    }
}

ここでは getPropertyDescriptors() メソッドをオーバーライドし、 PropertyDescriptorインスタンスを返しています。もうお分かりですね。ここでこの JavaBean のプロパティの情報 (プロパティ名、アクセッサメソッド) を返しているわけです。

Introspector の API ドキュメント には次のような解説が記載されています。

Fooクラスについては、情報の問い合わせ時にnull以外の値を提供するFooBeanInfoクラスがあれば、明示的な情報を取得できます。まず、ターゲットのBeanクラスの完全指定されたパッケージ名に「BeanInfo」を付加して新規のクラス名とし、BeanInfoクラスを検索します。これに失敗した場合は、この完全指定されたパッケージ名の最後のクラス名にあたる部分を使って、BeanInfoパッケージ検索パスに指定されたパッケージごとに該当クラスを検索します。
(中略)
クラスの明示的なBeanInfoが見つからない場合は、低レベルのリフレクションを使ってクラスのメソッドを調べ、標準設計パターンを適用して、プロパティのアクセス用メソッド、イベント・ソース・メソッド、publicメソッドを識別します。
(以下略)

つまり、JavaBeans を作るとき、本来は対になる BeanInfo 実装クラスを用意し、Bean の情報を提供する必要があるのです。ですが、毎回それをやっては面倒なので、便宜的にあの命名規則が用意されているわけです。CoC の先駆けだったわけですね (ちなみに JavaBeans は 20 世紀に誕生した仕様です) 。

この BeanInfo ですが次のような情報を提供することができます。

  • プロパティの情報
  • イベントの情報
  • メソッドの情報
  • アイコン

アイコンとかイベントって何?と思いましたか。JavaBeans は本来可搬性のあるソフトウェアコンポーネント、特に GUI コンポーネントを開発するために作られた規格です。つまり、IDE の「ポトペタツール」に追加して使えるソフトウェア部品を作るためのものなのです。BeanInfo ではそのツールに対して提供する情報を定義します。

f:id:aoe-tk:20170426222842p:plain

こうして考えると、単なる DTO として JavaBeans を使うのはオーバースペックであることも分かりますね。なぜこんなにあちこちで JavaBeans が使われるようになったのかは歴史的な経緯が色々あるのですが、まあそれはまたの機会で。

なお、このエントリでやったいたずらは業務のコードではやらないでくださいね。いたずらに混乱を招くだけです。酒の席でのネタにとどめてくださいw

全体のコードは gist にアップしてあります。

https://gist.github.com/aoetk/e5a09f67f2ebbc206d0770b21116b69e

パーフェクトJava EEの感想

Java EE 7 に対応した解説本として出版されたパーフェクト Java EE を 著者のお一人である上妻 (id:n_agetsuma) さんから頂きました。ありがとうございます! 簡単ではありますが、本についての感想をまとめました。

gihyo.jp

実は本を頂いたのはもう半年前の夏のことだったのですが、色々と忙しい状況が続き、感想文の公開がこんなに遅くなってしまいました。ごめんなさい!

本の特色

この本の特色は、Java EE の Web Profile に内容を絞ったという思い切った点にあります。説明する対象を絞ることで、これまでの和書の Java EE 解説本よりも、各仕様の解説が深くなっており、多様な機能、意外と知られていない新機能などがしっかり網羅されています。ただ、そのために jBatch が対象から抜けちゃったのはちょっと残念だったかなと思っています。

また、著作メンバーが Oracle に近くない人で構成されているので、変にバイアスが掛かっていないというのも特色かなと思っています。いわゆる「よいしょ」的な内容が少なく、かなりぶっちゃけたことも書かれているので、実際に利用する人にとっても参考になるのではないでしょうか。

良かったと思った点

この本で特によかったと思ったのは次のような点です。

  • CDI についてガッツリ解説された初めての和書です。
    • Web 上の情報でもここまできっちり網羅された解説は (少なくとも日本語では) 見たことがありません。
    • この点だけでも買う価値があると言ってもいいと思っています。
  • 最新の JPA 仕様をきっちりカバーしています。
    • 新機能である Entity Graph もしっかり押さえられています。
    • JPA を利用するうえで必要とされる解説が一通り網羅されています。
  • Web Profile の範囲で EE6、7 で新たに追加された新機能をしっかりと押さえています。
    • 特に日本語で紹介される機会が少なかった非同期 ServletServlet のモジュール化、WebSocket、JPA Criteria API もきっちり解説されています。
    • 「こんなことできるかな?」を調べるためのリファレンスとして使えると思います。
  • JTA の解説がきちんとされているのも個人的にはポイント大でした。
  • そして、先行して EE8 の MVC も取り上げられている!と言いたかったところですが...嗚呼、あんなことになるなんて...。

イマイチだなと思った点

ちょっとここは良くなかったかなあと思った点も挙げておきます。

  • 読者の対象に初めて Java EE を使って Web アプリケーションを開発しようとしている人が入っていましたが、その割には開発環境構築の説明が不親切だと思いました。
    • やはり、IDE とセットでの環境構築の解説はすべきだと思います。
    • 記述内容も過去に古い J2EE での開発をしてきたことを前提としたところもありました。
  • 章立てが良くないと感じました。
    • いきなり CDI から入るのは違和感がありました。
    • 実際に作って試しながら進めてもらうには、やはりフロントから入っていくのが王道だと思います。
  • JSP は省略すべきではなかった!
    • 一応最後に付録で説明されていますが、ちょっとおざなりすぎかなと。その割には本文中で JSP を使っている箇所も多いですし。
    • ここは異論がある人も多いと思いますが、私は JSP はまだまだ重要だと思っています。JSP はまだまだ利用されている箇所が多く、情報を必要としている人は多くいるはずです。
    • JSP もバージョンアップごとに地味に機能追加がされています。そのような情報をカバーしていると喜ぶ人も多いんじゃないかなと思いました。

この本を読むべき人

以上、感じたことをつらつらと書きましたが、中々の良書だと思っています。特に次のような人にお勧めしたいですね。

  • これまで Java EE (J2EE) で開発してきたけど、最新の仕様にキャッチアップできていないなあと感じている人。
  • 現在 Java EE (J2EE) ベースのシステムを開発、保守しており、リファレンスを求めている人。
  • Spring Framework ベースのシステムを開発している人も読むべきです。Spring も何だかんだ言って EE を利用しており (特に ServletJPA) 、役に立つ記述は多いです。