20190506のJavaに関する記事は11件です。

PHPとJavaのコンストラクタを比べてみた

PHP と Java のコンストラクタを比較してみたいと思います。

要約

  • Java
    • 「暗黙的コンストラクタ呼び出し」が発生する場合がある
  • PHP
    • 「暗黙的コンストラクタ呼び出し」は発生しない
    • サブクラスでコンストラクタが 定義されている 場合
      • 「サブクラスのコンストラクタ」のみ が呼び出される
      • 「スーパークラスのコンストラクタ」を呼び出す場合は parent::__construct() を呼び出す
    • サブクラスでコンストラクタが 定義されていない 場合
      • 「スーパークラスのコンストラクタ」 が呼び出される

動作確認環境

  • Java
    • OpenJDK 11.0.1
  • PHP
    • 7.3.5
  • Ruby
    • 2.6.3p62

はじめに

Java 出身の人向けに、以下のように説明している記事を見かけます。

コンストラクタを定義するとき、Java ではクラス名と同名のメソッドとして定義する。
一方、PHP では __construct() で定義する。
以上。

class Sample
{
  public Sample() {
    System.out.println("Constructor of Sample Class");
  }
}
class Sample
{
  public function __construct() {
    echo "Constructor of Sample Class\n");
  }
}

でも、PHP と Java の違いって、本当にそれだけですかね?1

PHPに「暗黙的コンストラクタ呼び出し」はない

PHP と Java で違いが生じるのは 継承 を使ったときです。

Java

Java では、暗黙的に「スーパークラスのコンストラクタ」が呼び出されることがあります。2

public class SuperClass {
    public SuperClass() {
        System.out.println("Constructor of Super Class");
    }
}

public class SubClass extends SuperClass {
    public SubClass() {
        System.out.println("Constructor of Sub Class");
    }
}

public class Main {
    public static void main(String[] args) {
        new SubClass();
    }
}

上記コードを実行すると、以下のように表示されます。

Constructor of Super Class
Constructor of Sub Class

「サブクラスのコンストラクタ」には、「スーパークラスのコンストラクタ」を明示的に呼び出す処理( super() )は書かれていません。

しかし、このような場合、Java は「 引数を持たない スーパークラスのコンストラクタ」を 暗黙的に 呼び出すという 言語仕様 になっています。3 ここで 引数を持たない と書いてあるのが重要です。

コンストラクタ本体が明示的コンストラクタ呼出しで開始されず,宣言しているコンストラクタが根源的クラスであるクラスObjectの一部でなければ,コンストラクタ本体は,コンパイラにより暗黙に,上位クラスのコンストラクタ呼出し"super();"で開始すると仮定される。すなわち,その直接的上位クラスに存在する実引数を取らないコンストラクタ呼出しとする。

先程のコードの場合、「サブクラスのコンストラクタ」に「スーパークラスのコンストラクタ」を明示的に呼び出す処理が書かれていなかったため、暗黙的に引数を持たない スーパークラスのコンストラクタ」が呼び出されました。

コンパイルエラー

一方、「スーパークラスのコンストラクタ」に 「 引数を持つ コンストラクタ」のみが定義されている場合、「 引数を持たない コンストラクタ」を呼び出すことができないため、コンパイル時にエラーになります。

public class SuperClass {
    // 引数を持つコンストラクタのみが定義されている
    public SuperClass(String name) {
        System.out.println("Constructor of Super Class");
    }
}

public class SubClass extends SuperClass {
    public SubClass() {
        System.out.println("Constructor of Sub Class");
    }
}

public class Main {
    public static void main(String[] args) {
        new SubClass();
    }
}
Error:(4, 23) java: クラス com.example.SuperClassのコンストラクタ SuperClassは指定された型に適用できません。
  期待値: java.lang.String
  検出値: 引数がありません
  理由: 実引数リストと仮引数リストの長さが異なります

この場合は、「サブクラスのコンストラクタ」で、明示的に 「スーパークラスのコンストラクタ」を呼び出す必要があります。

public class SuperClass {
    // 引数を持つコンストラクタのみが定義されている
    public SuperClass(String name) {
        System.out.println("Constructor of Super Class");
    }
}

public class SubClass extends SuperClass {
    public SubClass() {
        // スーパークラスのコンストラクタを明示的に呼び出す
        super("hoge");
        System.out.println("Constructor of Sub Class");
    }
}

public class Main {
    public static void main(String[] args) {
        new SubClass();
    }
}

デフォルトコンストラクタ

また、サブクラスにコンストラクタが定義されていない場合も、暗黙的に「 引数を持たない スーパークラスのコンストラクタ」が呼び出されることがあります。

public class SuperClass {
    public SuperClass() {
        System.out.println("Constructor of Super Class");
    }
}

public class SubClass extends SuperClass {
    // サブクラスのコンストラクタが定義されていない
}

public class Main {
    public static void main(String[] args) {
        new SubClass();
    }
}

上記コードを実行すると、以下のように表示されます。

Constructor of Super Class

サブクラスにコンストラクタが定義されていない場合、Java は自動的に デフォルトコンストラクタ を作成します。
そして、デフォルトコンストラクタは「 引数を持たない スーパークラスのコンストラクタ」を呼び出す 言語仕様 になっています。

クラスがコンストラクタ宣言を含んでいなければ,実引数を取らないデフォルトコンストラクタ(default constructor)が自動的に提供される。

  • 宣言しているクラスが,ルートクラスであるクラスObjectならば,デフォルトコンストラクタは,空の本体をもつ。
  • そうでなければ,デフォルトコンストラクタは,実引数を取らず,実引数をもたない上位クラスのコンストラクタを単に呼び出す。

コンパイラがデフォルトコンストラクタを提供しているが,上位クラスが実引数をもたないコンストラクタをもっていなければ,コンパイル時エラーが発生する。

継承階層

なお、継承階層が深いコンストラクタから順番に呼び出されます。

public class AncestorClass {
    public AncestorClass() {
        System.out.println("Constructor of Ancestor Class");
    }
}

public class SuperClass extends AncestorClass {
    public SuperClass() {
        System.out.println("Constructor of Super Class");
    }
}

public class SubClass extends SuperClass {
    public SubClass() {
        System.out.println("Constructor of Sub Class");
    }
}

public class Main {
    public static void main(String[] args) {
        new SubClass();
    }
}

上記コードを実行すると、以下のように継承階層が深い順に実行されます。

  • AncestorClass のコンストラクタ
  • SuperClass のコンストラクタ
  • SubClass のコンストラクタ
Constructor of Ancestor Class
Constructor of Super Class
Constructor of Sub Class

PHP

一方、PHP では暗黙的に「スーパークラスのコンストラクタ」が呼び出されることはありません。
以下は、公式マニュアル からの引用です。

注意: 子クラスがコンストラクタを有している場合、親クラスのコンストラクタが 暗黙の内にコールされることはありません。 親クラスのコンストラクタを実行するには、子クラスのコンストラクタの 中で parent::__construct() をコールすることが 必要です。 子クラスでコンストラクタを定義していない場合は、親クラスのコンストラクタを継承します (ただし、private 宣言されている場合は除く)。 これは、通常のクラスメソッドと同様です。

PHP では、コンストラクタの継承は、通常のメソッドのオーバーライドと同様に考えることができます。
つまり、サブクラスでコンストラクタを定義した場合は、スーパークラスのコンストラクタをオーバーライドしたことになるのです。

したがって、PHP では、以下のように単純なルールにすることができます。

ルール

  • サブクラスでコンストラクタが 定義されている 場合
    • 「サブクラスのコンストラクタ」のみ が呼び出される
    • 「スーパークラスのコンストラクタ」を呼び出す場合は parent::__construct() を呼び出す
  • サブクラスでコンストラクタが 定義されていない 場合
    • 「スーパークラスのコンストラクタ」 が呼び出される

サブクラスでコンストラクタが定義されている場合

サブクラスでコンストラクタが 定義されている 場合は、「サブクラスのコンストラクタ」のみ が呼び出されます。
暗黙的に「スーパークラスのコンストラクタ」が呼び出されることはありません。

class SuperClass {
  public function __construct() {
    echo "Constructor of Super Class\n";
  }
}

class SubClass extends SuperClass {
  public function __construct() {
    echo "Constructor of Sub Class\n";
  }
}

new SubClass();
Constructor of Sub Class

また、通常のメソッドのオーバーライドと異なり、コンストラクタの場合はシグネチャが異なっていても例外的にオーバーライドと見なされます(公式マニュアル)。

メソッドをオーバーライドするときには、パラメータのシグネチャも同じでなければなりません。 もし違っていれば、PHP は E_STRICT レベルのエラーとなります。ただしコンストラクタは例外で、 異なるパラメータでオーバーライドすることができます。

要するに、 サブクラスでコンストラクタが定義されている場合は、常に「サブクラスのコンストラクタ」のみが呼び出される ということですね。

class SuperClass {
  public function __construct() {
    echo "Constructor of Super Class\n";
  }
}

class SubClass2 extends SuperClass {
  // スーパークラスのコンストラクタにはない引数がある
  public function __construct(String $name) {
    echo "Constructor of Sub Class\n";
  }
}

new SubClass2('hoge');
Constructor of Sub Class

サブクラスでコンストラクタが定義されていない場合

サブクラスでコンストラクタが 定義されていない 場合は、「スーパークラスのコンストラクタ」 が呼び出されます。

class SuperClass {
  public function __construct() {
    echo "Constructor of Super Class\n";
  }
}

class SubClass3 extends SuperClass {}

new SubClass3();
Constructor of Super Class

スーパークラスのコンストラクタを呼び出す

「サブクラスのコンストラクタ」から「スーパークラスのコンストラクタ」を呼び出す場合は、 parent::__construct() を呼び出します。

parent::__construct() は Java の super() に相当するものですが、いろいろと制約がある super() に比べると、はるかに制約のないものです。

class SuperClass {
  public function __construct() {
    echo "Constructor of Super Class\n";
  }
}

class SubClass4 extends SuperClass {
    public function __construct() {
    // 先頭行でなくてもいい
    echo "Constructor of Sub Class Called\n";
    // 何度呼び出してもいい
        parent::__construct();
        parent::__construct();
    }
}

new SubClass4();

parent::__construct() を 2 回呼び出したので、 "Constructor of Super Class" が 2 行表示されていることに注意してください。

Constructor of Sub Class Called
Constructor of Super Class
Constructor of Super Class

なお、PHP では 暗黙的に 「スーパークラスのコンストラクタ」を呼び出さないため、 自動的に 継承階層が深いコンストラクタから順番に呼び出すこともありません。
そうしたい場合は、それぞれのクラスで明示的に parent::construct() を呼び出す必要があります。

おまけ: Ruby

「そんな変な仕様になってるのは PHP のオブジェクト指向が貧弱だからだろ?」とか言われると心外なので、生まれながらのオブジェクト指向言語である Ruby でも試してみました。

詳しくは書きませんが、PHP と同様の動作をしていることが分かると思います。

# スーパークラス
class SuperClass
    def initialize
        puts 'initialize of Super Class'
    end
end

# サブクラス
# サブクラスのコンストラクタのみが呼び出される
class SubClass < SuperClass
    def initialize
        puts 'initialize of Sub Class'
    end
end

# サブクラス2
# サブクラスのコンストラクタのみが呼び出される
class SubClass2 < SuperClass
  # スーパークラスのコンストラクタにはない引数がある
    def initialize name
        puts 'initialize of Sub Class'
    end
end

# サブクラス3
# コンストラクタを定義していない場合
# スーパークラスのコンストラクタが呼び出される
class SubClass3 < SuperClass
end

# サブクラス4
# superメソッドを呼び出した場合
# スーパークラスの同名メソッド(initialize)が呼び出される
class SubClass4 < SuperClass
  def initialize
    super
  end
end

puts 'SubClass'
SubClass.new
puts 'SubClass2'
SubClass2.new('name')
puts 'SubClass3'
SubClass3.new
puts 'SubClass4'
SubClass4.new
SubClass
initialize of Sub Class
SubClass2
initialize of Sub Class
SubClass3
initialize of Super Class
SubClass4
initialize of Super Class

参考

Java

PHP

Ruby

雑感

個人的好みだけで言うと、勝手に後ろで動かれるのは好きではないため、PHP や Ruby のほうが好みです。4

ただ、 継承is-a 関係 であるべきことを考えると、「暗黙的コンストラクタ呼び出し」を行わない PHP や Ruby では、サブクラスが全く関係ないスーパークラスを継承できてしまうようにも思います。

そういう観点で考えると、「暗黙的コンストラクタ呼び出し」はむしろそうなるのが自然で、そうなって問題が起きるようなプログラムはそもそもオブジェクト指向的でないということになるのかなとも思います。


  1. PHP 4 ではじめてオブジェクト指向を取り入れたときは、PHP でも、コンストラクタはクラス名と同名のメソッドでした。その後、PHP 5 で __construct() と書くようになりましたが、PHP 4 形式で書いてもコンストラクタとして認識されました。しかし、PHP 7 では、PHP 4 形式で書いた場合に「E_DEPRECATED」エラーが出力されるようになりました。そして、今後リリースされる PHP 8 からは、コンストラクタとして認識しなくなる予定です。詳しくは RFC を参照のこと。 

  2. Java だけでなく C++ でも同様だと思います。 

  3. 引用した言語仕様は、やや古い第 2 版ですが、最新の 言語仕様 でもほぼ同じ記載になっています。 

  4. 最初に触れたオブジェクト指向言語(?)が PHP だったからというのもあるかもしれません。というか、それに尽きるか。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spring Boot入門その1

この記事はこちらのブログをQiitaに移したものです。
まだSpring Bootを使うようになって1ヶ月ちょっとしか経っていなくて、説明が不適切な箇所があるかもしれませんが、その際は指摘していただければと思います。

WEB+DB PRESS106号でSpring Bootの記事でタスク管理サービスとQiitaのクロール&配信サービスを作るというのがあったので、とりあえずタスク管理サービスをの方をやってみました。
今回WEB+DB PRESSの記事をまねて作ったソースコードはこちらにあります。今後色々と改良していく予定です。
ちなみに、完成品(?)はこんな感じです。
Spring_todo.png

1.Spring Bootとは

2003年にSpring FrameworkというJavaのWebフレームワークが作られて、それを使いやすくしたものとして2014年に作られたのがSpring Bootです。
DI(依存性の注入)の考え方を用いているのが特徴です。

2. Spring Bootで開発を行うために必要なもの

OpenJDK(フレームワーク以前にJavaで開発をやるんなら当然必要でしょう。バージョンは今時の機能を使うんなら最低でも8以上は必要かと)
Spring Tool Suite(STSと略す。EclipseベースのIDE。本だと3.9.5でやってたみたいだが、2019年4月時点だと4以降がデフォルトなので、自分は4.1.2でやった)
Lombok(ロンボックと読む。アノテーションをつけるだけでgetterとsetterを自動生成してくれる便利なライブラリです。ダウンロードしたlombok.jarをダブルクリックするだけでインストールされます。)
・データベース管理システム(Webシステムなんだから必要に決まってる。自分が普段使っているやつを使えばいいと思います。本ではH2 Databaseを使っていたが、自分は使い慣れているMySQLでやりました。)
Pleiades(プレアデスと読む。Eclipse日本語化プラグイン。英語が得意な人には必要ではないんだろうけど、自分は入れました。導入の仕方はこちらを参照(リンク先はWindowsでの説明だけど、Macでもfeaturesとpluginsフォルダにダウンロードしたものの中身を入れるという、同じやり方で大丈夫です)。)
Maven(メイヴンと読む。Javaのビルドツール。STSを公式ページからダウンロードしてインストールした場合は大丈夫(なはず)だけど、brewコマンドとかでインストールした場合はmvn -versionで入っているか確認して、なかったら公式ページから持ってくるかbrew install mavenとかでインストールしてください)

3. Spring Bootの構成の大雑把な説明

ここでは、Laravelとの比較っぽい感じでSpring Bootの構成をかいつまんで説明して行きます。なお、Spring BootではLaravelとかと違って、フォルダ構成は決まっていないので、自分で構成を考える必要があります。
Javaのソースはsrc/main/java以下に、Java以外(html, css, jsなど)はsrc/main/resources以下に置かれます。JUnitでテストを行うためのソースはsrc/test以下に置かれます。

src/main/java (Javaソースファイル)
・Application(メイン関数です。アノテーションに@SpringBootApplicationをつけます。プロジェクトを実行するとこのメイン関数が呼ばれます。Laravelのindex.phpに近いか。)
・Entity(データの定義です。@Entityアノテーションをつけます。lombok.Dataをインポートして@Dataをつければゲッターとセッターが自動的に生成されます。さらにJPA(Javaでデータベースを扱う標準技術)を使って@Table(name = “テーブル名”)をつければ、自動的にテーブルを作ってくれます。LaravelのModel(app直下のやつ)+マイグレーションといったところか。)
・Repository(Entityで定義したデータのCRUD(create, read, update, delete)の基本操作を行うためのもので、JpaRepositoryを継承したインタフェースとして定義されます。Laravelにはない機能ですね。)
・Controller(MVCのCを担うControllerで、Laravelのものと似ていますが、データに対する処理などは次に説明するServiceを呼び出す形で実装します。@Controllerアノテーションをクラスにつけます。また、Getメソッドならメソッドに@GetMapping(value=”(URL)”)を、Postメソッドならメソッドに@PostMapping(value=”(URL)”)をアノテーションにつけることでリクエストをマッピングすることができます。なので、LaravelだとController+routesに近いイメージかも)
・Service(データに対して行う処理を実装して、Controllerに渡すものです。これもLaravelにはないですが、あえて言えばModelとControllerの間をつなぐものというイメージかもしれないです。@Serviceアノテーションをつけます。実際の処理はRepositoryのCRUD操作を呼び出して実装します。)
・Form(htmlフォームに入力した値をコントローラに渡すためのクラス。バリデーションとかもここで行うことが多いようです。LaravelでいうRequest?)

src/main/resources(フロントエンドなど)
・Thymeleaf(タイムリーフと読む。植物のハーブから名付けられたようです。いわゆるテンプレートエンジン。LaravelだとBladeがデフォルトだけど、Spring Bootではこれ以外にもいくつかのテンプレートエンジンをサポートしているようです。)
・application.properties(アプリケーションの設定値を持たせるもので、Laravelだとconfig配下のファイルが近いと思います。使用するデータベースの設定なんかもここで行います。)

以上、かなり駆け足で書いたのでざっくりとした説明になってしまいましたが、その2を書くときはもう少し詳しく書ければと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「GWの次の祝日はいつやろ」→「69日後」→「どんだけ〜」なWebアプリを作りました

今年のGWは夢の10連休でしたが、それも今日で終わり。なんとなく次の祝日を調べてみたらなんと69日先。こうなりゃもう、次の祝日までカウントダウンして思う存分待ちわびてやろうと思い、勢いでWebアプリケーション「次の祝日までどんだけ〜」を作成しました:relaxed:

Webアプリケーション「次の祝日までどんだけ〜」

1.gif
次の祝日までどんだけ〜

次の祝日までをカウントダウン形式で表示してくれるWebアプリです。文字だけだと味気ないので、祝日ごとに画像を表示しています。

画像はすべていらすとやのものを使わせていただきました。どの祝日の画像もすべて用意されていて驚きました。いらすとやさんすごい。。ただ、2020年から「体育の日」が「スポーツの日」に変更されるのですが、さずがにまだスポーツの日の画像は用意されていませんでした(用意してほしいですー:pray:)。

技術の話

ここからは少し真面目に技術のことについて書いていきます。

使用技術

  • フロントエンド
    • Thymeleaf
    • JavaScript
    • CSS
  • バックエンド
    • Java
    • Spring Boot
    • Gradle
    • LomBok
  • デプロイ
    • Heroku

いつも個人開発はRuby on Railsで作るのですが、業務でJavaを使っていることもあり勉強がてら上記構成でWebアプリケーションを作成しました。Spring Boot、慣れればサクッとWebアプリが作れますね。

デプロイ先はおなじみHerokuです。なんせデプロイが簡単なのです。どれくらい簡単かというと、以下のようにたった3行のコマンドを実行するだけです。

# たった3行でHerokuにデプロイできました
$ heroku login
$ heroku create
$ git push heroku master

ちなみに、画面がシンプルなのでCSSフレームワーク(Bootstrapなど)は使っていません。レスポンシブ対応はCSSをちょこちょこっと書いて行いました。ですので、スマホでも見ていただけると嬉しいです:relaxed:

ドメイン名が「next-holiday-dondake.herokuapp.com」とHerokuのデフォルトになっているのは見逃してください:joy:気が向けば独自ドメインを取得しようと思います。

仕組み

  • 祝日のリストを用意する
    • 内閣府が提供している国民の祝日のCSVを利用
  • 祝日ごとの画像を用意する
  • 次の祝日の判定ロジックを組み込む
    • 祝日のリストを読み込み、現在日時から直近の祝日を取得
  • 画面を表示する

こうやって仕組みを並べてみると、たくさんのサイトにお世話になっているなと感じます:pray:オリジナルの仕組みとしては、「次の祝日の判定ロジックを組み込む」くらいですね。

リリース最優先で開発を進めたため、コードが汚かったり、テストコードを書いていなかったりします。ソースは公開していますので、プルリクお待ちしています:bow_tone1:

donchan922/next-holiday

まとめ

連休明けは仕事や学校に行くのがおっくうになるかと思いますが、次の祝日は必ずや訪れます。皆さんも一緒に次の祝日がくるのをカウントダウンして待ちわびてやりましょう!(絶望の69日後

次の祝日までどんだけ〜

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SQLiteで昇順・降順にソートする方法

概要

データベースから値を取得するとき、昇順・降順にソートしてから取得ができる
※前提条件として、SQLiteOpenHelperを使用してデータベースに値が入っていること

使い方

number title
15 ABCD
67 EFGH
22 IJKL
12 MNOP

例えば上記のようにデータベース(DB)に値があったとする

        if(helper == null){
            helper = new TestOpenHelper(getActivity().getApplicationContext());
        }

        if(db == null){
            db = helper.getReadableDatabase();
        }
        Cursor cursor = db.query(
                "testdb",
                new String[] { "number","title" },
                null, //selection
                null, //selectionArgs
                null, //groupBy
                null, //having
                null, //orderBy
        );

登録順にDBを取得するだけならこの方法で良い。

order Byに指定する(ソート)

        if(helper == null){
            helper = new TestOpenHelper(getActivity().getApplicationContext());
        }

        if(db == null){
            db = helper.getReadableDatabase();
        }

        String order_by = "number ASC"; //ソートしたい値,昇順(ASC)か降順(DESC)か

        Cursor cursor = db.query(
                "testdb",
                new String[] { "number","title" },
                null, //selection
                null, //selectionArgs
                null, //groupBy
                null, //having
                order_by
        );

Cursorに値を代入する時にorder Byに指定する
→昇順はASC、降順はDESCを指定するだけ!

その後(おまけ)

        //データリストを回す
        cursor.moveToFirst();
        for (int i = 0; i < cursor.getCount(); i++) {
       //ここに任意の処理を書く
            cursor.moveToNext();
        }
        cursor.close();

よく使う、ソート後のデータを取得する方法をメモ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaでAWS Signature V4を生成してAPIをリクエストする

はじめに

AWS APIは、リクエストにAWS Signature V4という署名をつけることでIAM認証を利用できます。普通はAWS SDKを利用することでSignature V4の仕様をさほど意識する必要はないのですが、諸事情で、自前によるSignature V4実装をする機会がありましたので、メモを残します。

Java 11で実装しており、HTTPクライアントはApache HttpComponents Clientを使ってます。

コードは切り貼り、加工しているので、正しく動作するか若干怪しいです。

署名しない時のリクエスト

 わかりやすくするため、署名しないときのリクエストサンプルを示します。 Elasticsearch APIで検索リクエストするサンプルです。このサンプルではPOSTメソッドを利用しているためクエリパラメータはHTTP bodyに書いています。

このリクエストにAWS Signature V4署名を施し、認証されたユーザからのみAPIを受けるように改修していきます。

import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;

void reqFunc(){
    // リクエストの生成
    URI uri                 = new URI(AWS_ELASTICSEARCH_URL + "/index/hoge/_search?");
    HttpPost post           = new HttpPost(uri);
    post.setHeader("Content-Type", "application/json");
    post.setEntity(new StringEntity( "クエリー文字列(省略)", "UTF-8"));

    // HTTPクライアントでリクエストの実行
    HttpClient client       = HttpClientBuilder.create().build();
    CloseableHttpResponse res = (CloseableHttpResponse) client.execute(post);

    // ...レスポンスに対する処理...
}

署名に関する情報

AWS Signature V4はAmazonのWebサイトで情報提供がされています。下記のURLからたどって情報を得ました。
https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-4.html

また、認証に必要となるアクセスキーIDとシークレットアクセスキーの取得方法は、下記のURLからたどって情報を得ています。
https://docs.aws.amazon.com/ja_jp/general/latest/gr/aws-sec-cred-types.html

署名の実装

AWS Signature V4は、リクエスト情報そのものと、予め払い出されているアクセスキーID/シークレットアクセスキーを使いハッシュを生成し、リクエストヘッダに付与します。何度も繰り返しハッシュ化したり、ハッシュの対象対象がどこまでなのか分からず、試行錯誤しました。

HttpRequestInterceptorクラスの作成

最初に送信するリクエスト自身の情報を取得するため、HttpRequestInterceptorクラスと、それを挟み込んだHTTPクライアントを用意します。HTTPクライアントを生成する際にこのクラスを挟むことで、サーバへのリクエスト送信直前に処理を差し込むことができます。

  • HttpRequestInterceptor
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.protocol.HttpContext;

public class AmazonizeInterceptor implements HttpRequestInterceptor {

    @Override
    public void process(HttpRequest request, HttpContext context) throws Exception{
        AwsSigner4(request);  
    }

    private void AwsSigner4(HttpRequest request) throws Exception {
        /* この関数実装内でAWS Signature V4の実装をします。 */
    }
}
  • HTTPクライアント呼び出し部の修正
//...略...

void reqFunc(){
    //...略...

    // HTTPクライアントでリクエストの実行
    HttpClient client = HttpClientBuilder.create()
                        .addInterceptorLast(new AmazonizeInterceptor())  // ←これを追加する
                        .build();
    CloseableHttpResponse res = (CloseableHttpResponse) client.execute(post);

    //...レスポンスに対する処理...
}

これで、RequestInterceptorを挟み込んだHTTPクライアントでリクエストを行うと、サーバ送信前に先程定義したAwsSigner4関数が実行されるようになります。

AwsSigner4関数の実装

次に実際の署名処理を行うAwsSigner4関数の実装をしていきます。
処理内容は、リクエストヘッダーに「X-Amz-Date」と「Authorization」を追加するだけですが、Authorizationの値を求めるのに何段階か必要になります。

  1. 正規リクエスト文字列(canonicalRequest)の生成
  2. 署名文字列(StringToSign)の生成
  3. 署名キー(SigningKey)の生成
  4. Authorizationヘッダー文字列の生成

参考までに、canonicalRequestに含まれるヘッダー情報は、host,x-amz-dateが必須ですが、それ以外のヘッダー情報を追加しても良いみたいです。

import org.apache.http.util.EntityUtils;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.text.SimpleDateFormat;

private void AwsSigner4(HttpRequest request) throws Exception {

    /* X-Amz-Dateヘッダーの生成 */
    SimpleDateFormat xAmzDateFormatter  = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
    xAmzDateFormatter.setTimeZone(Calendar.getInstance(TimeZone.getTimeZone("UTC"))); // Signerの有効期限はUTCで判定される
    String xAmzDate         = xAmzDateFormatter.format(new Date()).trim();
    request.setHeader("X-Amz-Date", xAmzDate);

    /* Authorizationヘッダー文字列の生成 */
    /* 1. 正規リクエスト文字列(canonicalRequest)の生成 */
    String path             = getPath(request);
    String xAmzContentSha   = getBodyHash(request);
    String canonicalRequest = "POST\n"
                        + path + "\n"
                        + "\n"
                        + "host:" + request.getFirstHeader("Host").getValue() + "\n"
                        + "x-amz-date:" + xAmzDate + "\n"
                        + "\n"
                        + "host;x-amz-date\n"
                        + xAmzContentSha;

    /* 2. 署名文字列(StringToSign)の生成 */
    String awsRegion = "ap-northeast-1" ;  // AWS APIリクエスト先のリージョン情報
    String awsNameSpace = "es" ;  // リクエストするAWSサービスの名前空間
    String StringToSign = "AWS4-HMAC-SHA256\n"
                        + xAmzDate + "\n"
                        + xAmzDate.substring(0, 8) + "/" + awsRegion 
                        + "/" + awsNameSpace +"/aws4_request\n"
                        + DigestUtils.sha256Hex(canonicalRequest);

    /* 3. 署名キー(SigningKey)の生成 */
    String awsSecretAccessKey = "AWSシークレットアクセスキー" ;
    // X-Amz-Date → リージョン → AWSサービス名前空間 → 固定文字(aws4_request) の順でハッシュ化していく
    String hashStr    = getHmacSha256ByStrKey("AWS4" + awsSecretAccessKey, xAmzDate.substring(0, 8));
    hashStr           = getHmacSha256ByHexKey(hashStr, awsRegion);
    hashStr           = getHmacSha256ByHexKey(hashStr, awsNameSpace);
    String SigningKey = getHmacSha256ByHexKey(hashStr, "aws4_request");

    /* 4. Authorizationヘッダー文字列の生成 */
    String awsAccessKeyId = "AWSアクセスキーID" ;
    String sig          = getHmacSha256ByHexKey(SigningKey, StringToSign);
    String authorization = "AWS4-HMAC-SHA256 Credential="
                        + awsAccessKeyId
                        + "/" + xAmzDate.substring(0, 8)
                        + "/" + awsRegion
                        + "/" + awsNameSpace
                        + "/aws4_request,"
                        + "SignedHeaders=host;x-amz-date,"
                        + "Signature=" + sig;

    request.setHeader("Authorization", authorization);
}

/***  リクエストパスの取得 */
private String getPath(HttpRequest req) throws Exception {
    String uri              = req.getRequestLine().getUri();

    // URLのクエリ文字列とのセパレータである「?」はpathに含めない
    if (uri.endsWith("?"))  uri = uri.substring(0, uri.length()-1);

    // URLエンコードも色々種類があるらしく、Amazonが指定したロジックでエンコードする
    return awsUriEncode(uri,true);
}

/*** リクスとボディのハッシュ取得 */
private String getBodyHash(HttpRequest req) throws Exception{
    HttpEntityEnclosingRequest ereq = (HttpEntityEnclosingRequest) req;
    String body             = EntityUtils.toString(ereq.getEntity());
    return DigestUtils.sha256Hex(body);
}

/***
    * AWS指定スペックのURLエンコーダ
    * @param input
    * @param encodeSlash
    * @return
    * @throws UnsupportedEncodingException
    */
private String awsUriEncode(CharSequence input, boolean encodeSlash) throws UnsupportedEncodingException {
    StringBuilder result = new StringBuilder();
    boolean queryIn = false;
    for (int i = 0; i < input.length(); i++) {
        char ch = input.charAt(i);
        if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' || ch == '~' || ch == '.') {
            result.append(ch);
        } else if (ch == '/') {
            if (queryIn) result.append(encodeSlash ? "%2F" : ch);
            else result.append(ch);
        } else {
            if(!queryIn && ch=='?') {
                queryIn = true;
                result.append(ch);
            }else {
                byte[] bytes = new String(new char[] {ch}).getBytes("UTF-8");
                result.append("%" + Hex.encodeHexString(bytes,false));
            }
        }
    }
    return result.toString();
}

private String getHmacSha256ByStrKey(String strkey, String target) throws Exception {
    return getHmacSha256(strkey.getBytes(), target);
}

private String getHmacSha256ByHexKey(String hexkey, String target) throws Exception {
    return getHmacSha256(Hex.decodeHex(hexkey), target);
}

/***
    * target文字列をKeyを使いHMAC-SHA-256にハッシュ化する
    * @param target ハッシュ対象
    * @param key キー
    * @return Hex形式のハッシュ値
    */
private String getHmacSha256(byte[] key, String target) throws Exception {
    final Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(key, "HmacSHA256"));
    return String.valueOf(Hex.encodeHex(mac.doFinal(target.getBytes()), true));
}

まとめ

最終的にまとめると下記のようになりました。
リクエスト毎にフォーマッターを生成したり、定数が埋め込みになっていたりでアレですが、そこら辺はよしなに直してください。

import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.codec.binary.Hex;
import java.text.SimpleDateFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class AmazonizeInterceptor implements HttpRequestInterceptor {

    @Override
    public void process(HttpRequest request, HttpContext context) throws Exception{
        AwsSigner4(request);  
    }

    private void AwsSigner4(HttpRequest request) throws Exception {
        /* X-Amz-Dateヘッダーの生成 */
        SimpleDateFormat xAmzDateFormatter  = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
        xAmzDateFormatter.setTimeZone(Calendar.getInstance(TimeZone.getTimeZone("UTC"))); // Signerの有効期限はUTCで判定される
        String xAmzDate         = xAmzDateFormatter.format(new Date()).trim();
        request.setHeader("X-Amz-Date", xAmzDate);

        /* Authorizationヘッダー文字列の生成 */
        /* 1. 正規リクエスト文字列(canonicalRequest)の生成 */
        String path             = getPath(request);
        String xAmzContentSha   = getBodyHash(request);
        String canonicalRequest = "POST\n"
                            + path + "\n"
                            + "\n"
                            + "host:" + request.getFirstHeader("Host").getValue() + "\n"
                            + "x-amz-date:" + xAmzDate + "\n"
                            + "\n"
                            + "host;x-amz-date\n"
                            + xAmzContentSha;

        /* 2. 署名文字列(StringToSign)の生成 */
        String awsRegion = "ap-northeast-1" ;  // AWS APIリクエスト先のリージョン情報
        String awsNameSpace = "es" ;  // リクエストするAWSサービスの名前空間
        String StringToSign = "AWS4-HMAC-SHA256\n"
                            + xAmzDate + "\n"
                            + xAmzDate.substring(0, 8) + "/" + awsRegion 
                            + "/" + awsNameSpace +"/aws4_request\n"
                            + DigestUtils.sha256Hex(canonicalRequest);

        /* 3. 署名キー(SigningKey)の生成 */
        String awsSecretAccessKey = "AWSシークレットアクセスキー" ;
        // X-Amz-Date → リージョン → AWSサービス名前空間 → 固定文字(aws4_request) の順でハッシュ化していく
        String hashStr    = getHmacSha256ByStrKey("AWS4" + awsSecretAccessKey, xAmzDate.substring(0, 8));
        hashStr           = getHmacSha256ByHexKey(hashStr, awsRegion);
        hashStr           = getHmacSha256ByHexKey(hashStr, awsNameSpace);
        String SigningKey = getHmacSha256ByHexKey(hashStr, "aws4_request");

        /* 4. Authorizationヘッダー文字列の生成 */
        String awsAccessKeyId = "AWSアクセスキーID" ;
        String sig          = getHmacSha256ByHexKey(SigningKey, StringToSign);
        String authorization = "AWS4-HMAC-SHA256 Credential="
                            + awsAccessKeyId
                            + "/" + xAmzDate.substring(0, 8)
                            + "/" + awsRegion
                            + "/" + awsNameSpace
                            + "/aws4_request,"
                            + "SignedHeaders=host;x-amz-date,"
                            + "Signature=" + sig;

        request.setHeader("Authorization", authorization);
    }

    /***  リクエストパスの取得 */
    private String getPath(HttpRequest req) throws Exception {
        String uri              = req.getRequestLine().getUri();

        // URLのクエリ文字列とのセパレータである「?」はpathに含めない
        if (uri.endsWith("?"))  uri = uri.substring(0, uri.length()-1);

        // URLエンコードも色々種類があるらしく、Amazonが指定したロジックでエンコードする
        return awsUriEncode(uri,true);
    }

    /*** リクスとボディのハッシュ取得 */
    private String getBodyHash(HttpRequest req) throws Exception{
        HttpEntityEnclosingRequest ereq = (HttpEntityEnclosingRequest) req;
        String body             = EntityUtils.toString(ereq.getEntity());
        return DigestUtils.sha256Hex(body);
    }

    /***
        * AWS指定スペックのURLエンコーダ
        * @param input
        * @param encodeSlash
        * @return
        * @throws UnsupportedEncodingException
        */
    private String awsUriEncode(CharSequence input, boolean encodeSlash) throws UnsupportedEncodingException {
        StringBuilder result = new StringBuilder();
        boolean queryIn = false;
        for (int i = 0; i < input.length(); i++) {
            char ch = input.charAt(i);
            if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' || ch == '~' || ch == '.') {
                result.append(ch);
            } else if (ch == '/') {
                if (queryIn) result.append(encodeSlash ? "%2F" : ch);
                else result.append(ch);
            } else {
                if(!queryIn && ch=='?') {
                    queryIn = true;
                    result.append(ch);
                }else {
                    byte[] bytes = new String(new char[] {ch}).getBytes("UTF-8");
                    result.append("%" + Hex.encodeHexString(bytes,false));
                }
            }
        }
        return result.toString();
    }

    private String getHmacSha256ByStrKey(String strkey, String target) throws Exception {
        return getHmacSha256(strkey.getBytes(), target);
    }

    private String getHmacSha256ByHexKey(String hexkey, String target) throws Exception {
        return getHmacSha256(Hex.decodeHex(hexkey), target);
    }

    /***
        * target文字列をKeyを使いHMAC-SHA-256にハッシュ化する
        * @param target ハッシュ対象
        * @param key キー
        * @return Hex形式のハッシュ値
        */
    private String getHmacSha256(byte[] key, String target) throws Exception {
        final Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(key, "HmacSHA256"));
        return String.valueOf(Hex.encodeHex(mac.doFinal(target.getBytes()), true));
    }
}

あとがき

今回、私が実際に動作確認したのはElasticsearch APIのみですが、IAMの権限が正しく設定されていれば、他のAPIも同じ方法で利用できるかと思います。もし動作確認が取れたAWSサービスがあったら、コメントに書いていただけるとありがたいです。

お役に立ちましたら幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ABC - 027 - A&B&C

AtCoder ABC 027 A&B&C

AtCoder - 027

A問題

  • 3つの数字の内、2つは同じで1つ違う。違う1つを探す。
    • 2つを弾いて1つにしたい
    • XORしいていくと、打ち消せなかったものが残る
    private void solveA() {
        int wk = IntStream.range(0, 3).map(i -> nextInt()).reduce(0, (sum, i) -> sum ^ i);
        out.println(wk);
    }

A問題:無理やりstream

  • なんかびみょー
  • そもそも
    • Mapから条件に合致した値を取り出すのはcollectでいいのか?
    • このパターンだと条件に合致するのは1つのみというのが保証されているのでfindFirst()でもいいのか?

Stream難しい

    private void solveA2() {
        int[] wk = IntStream.range(0, 3).map(i -> nextInt()).toArray();
        Map<Integer, Long> resA = Arrays.stream(wk).mapToObj(i -> new Integer(i))
                .collect(Collectors.groupingBy(i -> i, Collectors.counting()));

        List<Entry<Integer, Long>> res = resA.entrySet().stream().filter(i -> i.getValue() != 2)
                .collect(Collectors.toList());
        //      Optional<Entry<Integer, Long>> res = resA.entrySet().stream().filter(i -> i.getValue() != 2).findFirst();
        out.println(res.get(0).getKey());
    }

B問題

  • 累積和と愚直実装を両方試した

    • 片方コメントアウトしてある
  • 人が流れるイメージ

i 1 2 3 4 5 平均人数
0 5 - 10 3
1 3 - 2 - 10 3
2 3 - 2 - - 10 3
3 3 - 2 - - - 10 3
4 3 - 3 - 3 - 3 - 3 3
i 1 2 3 4 5 平均人数
0 - 10 5 3
1 3 - 7 - 5 3
2 3 - 3 - 9 - 3
3 3 - 3 - 3 - 6 - 3
4 3 - 3 - 3 - 3 - 3 3
  • 橋が必要か否かは「片側にいる人数が、島×平均と同じか否か」のみで判定している

    • 多ければ、そこの島の人を減らすために橋が必要
    • 少なければ、そこの島に人を送るために橋が必要
  • 下図だと、1-3の間に橋を架けたら1-3は平均になった

i 1 2 3 4 5 平均人数
0 - 9 3 3 3
1 3 - 6 - 3 3 3
2 3 - 3 - 3 3 3 3
  • この例だと、4-5にも橋を架ける必要がある
    • 4-5の橋から左を見た時に、(3×4 - 9 != 0 )となっている
i 1 2 3 4 5 平均人数
0 - 9 6 3
1 3 - 6 - 6 3
2 3 - 3 - 3 - 6 3
3 3 - 3 - 3 3 - 3 3
    private void solveB() {
        int numN = nextInt();
        int[] wk = IntStream.range(0, numN).map(i -> nextInt()).toArray();
        int sum = Arrays.stream(wk).sum();
        if (sum == 0) {
            out.println(0);
            return;
        }
        if (sum % numN != 0) {
            out.println(-1);
            return;
        }

        int avg = sum / numN;
        int cnt = 0;

        /*  ここまではどっちの実装方法でも同じ */

        //------------------------------------------------------------------------
        /*
         * 累積和version
         */
        int[] forW = new int[numN];
        for (int i = 0; i < numN; i++) {
            if (i == 0) {
                forW[i] = wk[i];
            } else {
                forW[i] += forW[i - 1] + wk[i];
            }

        }
        /*
         * 最初は、0(i-1)から1(i)に橋が必要か否か?
         *  ->なので、スタートは1から。橋の最大値がN-1なので0(or N)のどちらかは対象外
         * 必要 -> 0(i-1)地点の人口が平均値と違う(多いか少ないかはどうでもよい)
         * 不要 -> 0(i-1)地点の人口が平均値と同じ
         *
         * i-1からiに橋が必要か否か
         * 必要 -> sum(i-1)と、(i-1)*平均値が違う(多いか少ないかはどうでもよい)
         * 不要 -> sum(i-1)と、(i-1)*平均値が同じ
         */
        for (int i = 1; i < numN; i++) {
            if (forW[i - 1] != i * avg) {
                cnt++;
            }
        }
        //------------------------------------------------------------------------
        /*
         * 愚直実装version
         */
        //      for (int i = 1; i < numN; i++) {
        //          int sumLeft = 0;
        //          for (int j = 0; j < i; j++) {
        //              sumLeft += wk[j];
        //          }
        //          int sumRight = 0;
        //          for (int j = i; j < numN; j++) {
        //              sumRight += wk[j];
        //          }
        //          if (sumLeft != i * avg || sumRight != (numN - i) * avg) {
        //              cnt++;
        //          }
        //      }
        //------------------------------------------------------------------------
        out.println(cnt);
    }

C問題

  • 解法見ないと分からなかった。未だになんとなくしかわかっていない。
    • 図示できるのだけど、言葉で説明できない。。。
  • 解法:P.16~
    private void solveC() {
        long numN = nextLong();

        /*
         * 深さを調べる
         */
        int depthCnt = 0;
        for (long n = numN; n > 0; n >>= 1) {
            depthCnt++;
        }

        long cnt = 1;

        /*
         * 深さが偶数の場合は[ 2*n ] からスタート
         * 深さが奇数の場合は[ 2*n+1 ] からスタート
         */
        int adjust = depthCnt % 2;
        /*
         * 最初の数字をカウント出来なかったらAokiの勝利
         * これ以降は、
         * AokiがカウントできなかったらTakahashiの勝利
         * TakahashiがカウントできなかったらAokiの勝利
         * を交互に勝つまで繰り返す。
         * 多分、LongのMAXまでリピートしておけばいいんだろうけど、自信がないのでwhileで
         */
        boolean who = true;
        while (true) {
            cnt = 2 * cnt + adjust;
            if (cnt > numN) {
                out.println(who ? "Aoki" : "Takahashi");
                return;
            }
            who = !who;
            adjust = 1 - adjust;

        }

    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クロージャーの4言語比較(Python, JavaScript, Java, C++)

概要

クロージャーといえばJavaScriptという感じだけど、Pythonでクロージャーを使ってみる機会があったので、色々な言語で比較してみる。

間違えている部分があったらコメントにてお教えください!
もちろん間違いがないように努力します。

JavaScriptの場合

function outer(){
    let a = 0;
    let inner = () => {
      console.log(a++);
    };
    return inner;
}

let fn = outer();

>>> fn();
0
>>> fn();
1
>>> fn();
2

innerがaへの参照を保持しているので、変数aがGCに回収されないままである。
シンプルで分かりやすい。

ただ、以下のようにaがouterの引数として渡される場合でもaが保持されることに注意する。

function outer(a){
    let inner = () => {
      console.log(a++);
    };
    return inner;
}

let fn = outer(0);

>>> fn();
0
>>> fn();
1
>>> fn();
2

クロージャーを定義時と異なる場所で実行してみる

結論から言えば、もちろん定義時と異なる場所でも定義時の変数を参照出来る。

nodeでimport文を実行するために.mjsファイルにし、node --experimental-modulesコマンドを用いる。

module.mjs
// export let a = 1;
// aはエクスポートしない
let a = 1;

export function outer(){
  let b = 1000;
  let inner1 = ()=>{
    console.log(b++);
  }
  return inner1;
}

//関数内関数ではない
export function inner2(){
  console.log(a++)
}
closure.mjs
import * as m from "./module.mjs";

let fn = m.outer();

fn();
fn();
fn();
m.inner2();
m.inner2();
m.inner2();
console.log(a)

出力:

$ node --experimental-modules closure.mjs
(node:12980) ExperimentalWarning: The ESM module loader is experimental.
1000
1001
1002
1
2
3
file:///***********/closure.mjs:11
console.log(a)
            ^

ReferenceError: a is not defined

このようにaは定義されていないと出るのに、inner2で参照出来ている。

ということは、JavaScriptでは関数内関数でなくとも、関数はクロージャーになる。

firefox(66.0.3)で実際に実行してみる。

$ cp closure.mjs closure.js
$ cp module.mjs module.js

closure.jsのインポート文をimport * as m from "./module.js";と書き換えておく。

closure.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script type="module" src="closure.js"></script>
    <title>test</title>
  </head>
  <body>
  </body>
</html>

closure.htmlにfirefoxでアクセスし、ログを確認。

1000
1001
1002
1
2
3
ReferenceError: a is not defined[詳細]

全く同じ結果になった。
Pythonだとグローバル変数のスコープはそのファイル限りであるが、import/export機能を使い、その変数をexportしないのであればJavaScriptでも同じっぽい?(exportしないのだから当然か)

Pythonの場合

def outer():
    a = 0
    def inner():
        nonlocal a
        print(a)
        a += 1

    return inner

fn = outer()

>>> fn()
0
>>> fn()
1
>>> fn()
2

Pythonもほぼ同じで分かりやすいが、一つ余分なものが入っている。その名はnonlocal。
一つ外側のスコープの変数を変更しようとする時に必須となる。
ちなみにnonlocalではなくglobal aとすると今度はaはグローバル変数aを参照するようになる。

nonlocal 文は、列挙された識別子がグローバルを除く一つ外側のスコープで先に束縛された変数を参照するようにします。

global 文は、列挙した識別子をグローバル変数として解釈するよう指定することを意味します。

(引用元:Python 言語リファレンス)

a = 111
def outer():
    a = 0
    def inner1():
        # 参照するだけならnonlocal aとしなくてもOK
        print(a)
        # nonlocal aとしないとa+=1でエラーが出る
        # a += 1
    def inner2():
        #global aとするとa = 111の定義を参照する
        global a
        print(a)

    return (inner1, inner2)

inner1, inner2 = outer()

>>> inner1()
0
>>> inner2()
111

# 当たり前だがクロージャーから
# 参照しているouter関数内部のa=0は外部からアクセス出来ず、
# この場合global変数であるa=111がprintされる
>>> print(a) 
111

定義時と異なる場所で実行してみる

module.py
a = 1

def outer():
    b = 1000
    def inner1():
        nonlocal b
        print(b)
        b += 1
    return inner1

#inner2は関数内関数ではない
def inner2():
    #global aとしないとエラーが出る
    global a
    print(a)
    a += 1
closure.py
from module import *

inner1 = outer()

inner1()
inner1()
inner1()

inner2()
inner2()
inner2()

出力:

$ python closure.py
1000
1001
1002
1
2
3

JavaScriptと同様にできた!

(inner2でglobal aとしないままエラーを出して、エラーが出るからJavaScriptとは違うと書いていましたが、コメントでご指摘を頂き修正しました。)

Javaの場合

Java(7以前)にはクロージャーがないらしい。関数内で無名クラス(匿名クラス)を使うことによって似たようなことが出来る(ただし制限あり)。
Java8からはラムダ式が導入されたが、それもまたクロージャーではないらしい。
それらの経緯等含めて以下のリンクが詳細かつ非常に読みやすくおすすめ。
- Java 8:ラムダ式、パート1
- Java 8:ラムダ式、パート2

パート2はまだ読んでないが一応貼っておく。

以下ではざっくりした説明をしていく。
まずは無名クラスを使う例を試してみる。
Javaでは関数が第一級オブジェクトではないので、代わりに一つの関数のみをメンバーとして持つ無名クラスオブジェクトをouter関数からリターンしてみたらどうなるか、みたいなイメージでいいのだろうか。

interface Inner {
    public void print();
}

public class ClosureTest {

    public Inner outer() {
        //ここはfinal使わないとエラー
        final int a = 0;

        return new Inner() {
            public void print() {
                System.out.println(a);

                //finalなのでa++できない
            }
        }
    }

    public static void main(String[] args) {
        ClosureTest ct = new ClosureTest();

        Inner inner = ct.outer();

        inner.print(); 

    }

}

上の例の通り、finalをつけなければならないので、JavaScriptやPythonのようには出来ない。ただし、finalは参照に対してのfinalに過ぎないので、変数aを配列かArrayList等にして、要素の値を実行ごとに変えることは出来る。つまり同じことを実現すること自体は出来る。

次にラムダ式。
ラムダ式の場合は、スコープ外の変数を参照する場合、その変数はfinalにしなくてもいい。しかし、値を変更するとエラーが出る。

public class Closure {
    public static void main(String... args) {
            //無名クラスと違ってfinalでなくともいい!
            int a = 0;
            //しかしa++の部分でエラーが出る
            Runnable r = () -> System.out.println(a++);
        r.run();
      } 
}

エラー内容は以下の通り。

Exception in thread "main" java.lang.Error: Unresolved compilation problem:
Local variable a defined in an enclosing scope must be final or effectively final

ということは、変数aはfinalか、実質的にfinalでなくてはならないということ。実質的にfinalというのは、上の例のような変更を施さないこと。

つまり、スコープ外の変数を参照する場合の取扱いは、無名クラスとラムダ式で(ほぼ)同じ。

以前、無名クラスやら関数型インターフェースやらラムダ式の表記法やら学んだ当初はなんなんだこれと思っていたが、クロージャーの観点で見ると理解出来てきてくるような気がする。

C++の場合

C++11のラムダ式を使って簡単に書けるようだ。

まず、ラムダ式はこれ。

[](int a, int b) -> int { return a + b; }

以下のように使う。

auto fn = [](int a, int b) -> int { return a + b; }
int c = fn();

「-> int」の部分はこの関数の戻り値の型を示している。次のように省略してもいい。

[](int a, int b) { return a + b; }

[]は後述。

そして、

このラムダ式によって、その場に以下のような関数オブジェクトが定義される:

struct F {
 auto operator()(int a, int b) const -> decltype(a + b)
 {
    return a + b;
 }
};

(引用元:cpprefjp - C++日本語リファレンス)

()をオーバーロードして関数オブジェクトを実現しているのが面白い。
戻り値がautoとdecltype(a+b)の二つになっている理由はここ参照

[]はキャプチャのこと。

ラムダ式には、ラムダ式の外にある自動変数を、ラムダ式内で参照できるようにする「キャプチャ(capture)」という機能がある。キャプチャは、ラムダ導入子(lambda-introducer)と呼ばれる、ラムダ式の先頭にある[ ]ブロックのなかで指定する。

キャプチャには、コピーキャプチャと参照キャプチャがあり、デフォルトでどの方式でキャプチャし、個別の変数をどの方式でキャプチャするかを指定できる。

(引用元:cpprefjp - C++日本語リファレンス)

以下キャプチャの例

#include <iostream>

using namespace std;

int main(){
  int a = 1;
  int b = 2;

  //aをコピーキャプチャ
  auto fn1 = [a] () { cout << a << endl; };

  //aを参照キャプチャ
  auto fn2 = [&a] () { cout << a << endl; };

  //aとbをコピーキャプチャ
  auto fn3 = [=] () { cout << a + b << endl; };

  //aとbを参照キャプチャ
  auto fn4 = [&] () { cout << a + b << endl; };

  a = 1000;
  b = 2000;

  fn1();
  fn2();
  fn3();
  fn4();
}

出力:

1
1000
3
3000

コピーキャプチャの時は以下のようになるんだろうか。詳しい方いたらお教えください。

これは想像上のコードです
struct F {
  //変数名はaとbにはならなそう?
  int a = 1;
  int b = 2;
  auto operator()() const -> decltype(a + b)
  {
     cout << a + b << endl;
  }
};

そして、クロージャー(っぽいもの)はこのように書ける。

#include <iostream>
#include <functional>

std::function<int()> outer()
{
    int a = 0;
    //aをコピーキャプチャ
    auto inner = [a]() mutable -> int {
        return a++;
    };
    return inner;
}

int main()
{
    auto inner = outer()

    std::cout << inner() << std::endl;
    std::cout << inner() << std::endl;
    std::cout << inner() << std::endl;
    return 0;
}

mutableに関しては、

キャプチャした変数はクロージャオブジェクトのメンバ変数と見なされ、クロージャオブジェクトの関数呼び出し演算子は、デフォルトでconst修飾される。そのため、コピーキャプチャした変数をラムダ式のなかで書き換えることはできない。

コピーキャプチャした変数を書き換えたい場合は、ラムダ式のパラメータリストの後ろにmutableと記述する。

(引用元:cpprefjp - C++日本語リファレンス
とのこと。

この例では、外部スコープにあるaをコピーし、それをラムダ式(関数オブジェクト)の中でメンバー変数として保存する。
Javaのようにコピーされた変数aはconst(final)なわけだが、mutableという語句によって変更可能にしてしまう。

もちろん、上の例で

auto inner = [&a]() mutable -> int {}

のように参照キャプチャをするとouter()実行終了時に参照先が解放されてしまうのでコピーキャプチャでなければならない。

おまけ

Pythonで面白いクロージャーの使い方が出来る。
http.serverというライブラリがあり、簡易なwebサーバーをたてられる。
以下のようにして使うのだが、HTTPServer()の第二引数のhdはクラスオブジェクトでなければならない。しかし、hdがクロージャーでもうまくいく。

server = HTTPServer(('', int(port)), hd)
server.serve_forever()

hdがクロージャーの場合:

def handler_wrapper():
    counter = [0]
    def handler(*args):
        counter[0] += 1
        return HandleServer(counter, *args)

    return handler

hd = handler_wrapper()

なぜこのようにしていいのか等含めて時間があったら別記事として書きたい。

まとめ

クロージャーって難しいなあ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaでPromiseを使った非同期処理を記述したい ~ JavaScriptのPromiseライクな文法の試行~

概要

  • Java言語で「JavaScriptのPromiseライクな文法で、非同期処理・並行処理を記述してみたい」と思いやってみました。
  • ソースは以下リポジトリにあります
    https://github.com/riversun/java-promise

コード例

非同期処理1が終わったら、その結果を使った非同期処理2が実行される」処理はJavaではどのように書けば良いでしょうか?

  • 解1:Java1.4時代の解:Threadクラスの機能でがんばる
    • Threadを入れ子式に持つとか、joinで終了待ちとか。並列処理黎明期。
  • 解2:Java5(1.5)時代の解:Callable/Futureで幸せになれたのかな・・・
    • Future/Callableで結果返せてうれしい、加えセマフォやラッチなど小道具そろったがそれなりに頑張る必要あり。
  • 解3:Java8時代の解:CompletableFutureで幸せになる、はず。

    • ついに待望?!のFuture/Promiseパターン的な仕組みが標準で登場!
  • 本稿では上の3つの解とは別の切り口で、以下のコードのようにJavaScriptのPromiseライクに書いてみました。

JavaでPromiseのサンプル
Promise.resolve()
 .then((action, data) -> {
   //非同期処理1
   new Thread(() -> {System.out.println("Process1");action.resolve("Result-1");}).start();
 })
 .then((action, data) -> {
   //非同期処理2
   new Thread(() -> {System.out.println("Process2");action.resolve("Result-2");}).start();
 })
 .start();

本稿でやりたいこと

  • やりたいことは以下のような処理を「JavaScriptのPromiseライクに記述すること」となります
    • 複数あるAPIを非同期に呼んで、結果を受け取ったら次のAPIを呼ぶ、という一連の処理
    • 複数の処理を同時に(並列に)に動かし、それがすべて完了したら次の処理に移るような処理

本稿で対象としないこと

  • (アカデミックな)Future/Promiseパターンの具現化
  • Java標準のコンカレント処理の使い方

対象環境

  • Java5以降
    • ライブラリはJava1.6ベースのAndroidでも動作します
    • Java8のコンカレント系APIはつかっていません

使い方(依存関係)

ライブラリjava-promiseとしてMavenレポジトリにありますので、以下を追加すればすぐに使えます。

Maven

POM.xmlのdependency
<dependency>
    <groupId>org.riversun</groupId>
    <artifactId>java-promise</artifactId>
    <version>1.1.0</version>
</dependency>

Gradle

build.gradle
dependencies {
    compile 'org.riversun:java-promise:1.1.0'
}
build.gradle(Android)
dependencies {
    implementation 'org.riversun:java-promise:1.1.0'
}

本編

JavaScriptで書くPromiseと、本稿で紹介するJavaで書く方法との比較

まず、比較のためにJavaScriptでPromiseを書いてみる

以下のコードは'foo'という文字列に非同期に実行された処理結果('bar')を連結するだけのJavaScriptのサンプルコードとなる。MDNでPromiseのサンプルとして公開されているものから抜粋した。

Example.js
Promise.resolve('foo')
    .then(function (data) {
        return new Promise(function (resolve, reject) {
            setTimeout(function () {
                const newData = data + 'bar';
                resolve(newData);
            }, 1);
        });
    })
    .then(function (data) {
        return new Promise(function (resolve, reject) {
            console.log(data);
            resolve();
        });
    });
console.log("Promise in JavaScript");

実行結果は以下のとおり

Promise in JavaScript
foobar

次にJava8でjava-promiseを使って書く

Example.java
import org.riversun.promise.Promise;

public class Example {

    public static void main(String[] args) {

        Promise.resolve("foo")
                .then(new Promise((action, data) -> {
                    new Thread(() -> {
                        String newData = data + "bar";
                        action.resolve(newData);//#resolveで次の処理に移行
                    }).start();//別スレッドで実行
                }))
                .then(new Promise((action, data) -> {
                    System.out.println(data);
                    action.resolve();
                }))
                .start();//処理開始のトリガー
        System.out.println("Promise in Java");
    }
}

実行結果は以下のとおり

Promise in Java
foobar

Promise以下の実行は非同期(別スレッド)で行われるので、この例ではSystem.out.println("Promise in Java");が実行されているのがわかる。

処理の都合最後に.start()を呼び出してPromiseチェインのトリガーをしている以外はJavaScriptのPromiseライクな文法に近づけてみた。

記法

ラムダ式を使わないで書く(Java7以前)

ラムダ式を使わなければ以下のようになる

ラムダ式を使わないで書いた場合
Promise.resolve("foo")
        .then(new Promise(new Func() {
            @Override
            public void run(Action action, Object data) throws Exception {
                new Thread(() -> {
                    String newData = data + "bar";
                    action.resolve(newData);
                }).start();
            }
        }))
        .then(new Promise(new Func() {
            @Override
            public void run(Action action, Object data) throws Exception {
                new Thread(() -> {
                    System.out.println(data);
                    action.resolve();
                }).start();
            }
        }))
        .start();

(action,data)->{}となっていた部分の正体はJavaScriptでいうところの function を表すインタフェースとなる。

Func.java
public interface Func {
    public void run(Action action, Object data) throws Exception;
}

さらにシンプルに書く

Promise.then(new Promise())ではなくPromise.then(new Func())でもOK。new Funcはラムダ式におきかえるとPromise.then((action,data)->{})となり、さらにシンプルになる。

then(Func)をつかって書く
Promise.resolve("foo")
   .then((action, data) -> {
       new Thread(() -> {
           String newData = data + "bar";
           action.resolve(newData);
       }).start();
   })
   .then((action, data) -> {
       System.out.println(data);
       action.resolve();
   })
   .start();

Promiseをつかった並行実行の各種パターン紹介

(1) Promise.then:非同期処理を順番通り実行する

コード:

public class Example20 {

    public static void main(String[] args) {

        // 処理1(別スレッド実行)
        Func function1 = (action, data) -> {
            new Thread(() -> {
                System.out.println("Process-1");
                Promise.sleep(1000);// Thread.sleepと同じ
                action.resolve("Result-1");// ステータスを"fulfilled"にして、次の処理に結果("Result-1")を伝える
            }).start();// 別スレッドでの非同期処理開始
        };

        // 処理2
        Func function2 = (action, data) -> {
            System.out.println("Process-2 result=" + data);
            action.resolve();
        };

        Promise.resolve()// 処理を開始
                .then(function1)// 処理1実行
                .then(function2)// 処理2実行
                .start();// 開始

        System.out.println("Hello,Promise");
    }

実行結果:

Hello,Promise
Process-1
Process-2 result=Result-1

説明:
thenの文法は Promise.then(onFulfilled[, onRejected]); つまり引数を2つまでとることができる
最初の引数onFulfilledは前の実行がfulfilled(≒成功)ステータスで終了した場合に実行される。
2つめの引数onRejectedはオプションだが、こちらは前の実行がrejected(≒失敗)ステータスで終了した場合に実行される。このサンプルは1つめの引数のみを指定している。

処理フロー:

image.png

  1. Promise.resolveでステータスをfullfilledにしてthenにチェインする。
  2. fullfilledなのでthenでは第一引数に指定されたfunction1を実行する
  3. function1action.resolveによりステータスをfullfilledにする
  4. function1action.resolveにString型引数"Result-1"をセットする
  5. 次のthenもステータスがfullfilledなのでfunction2が実行される
  6. function2実行時の引数dataにはfunction1の結果"Result-1"が格納されている

(2) action.resolve,action.reject:実行結果によって処理を分岐する

コード:

public class Example21 {

    public static void main(String[] args) {

        Func function1 = (action, data) -> {
            System.out.println("Process-1");
            action.reject();// ステータスを "rejected" にセットして実行完了
        };

        Func function2_1 = (action, data) -> {
            System.out.println("Resolved Process-2");
            action.resolve();
        };

        Func function2_2 = (action, data) -> {
            System.out.println("Rejected Process-2");
            action.resolve();
        };

        Promise.resolve()
                .then(function1)
                .then(
                        function2_1, // ステータスが fulfilled のときに実行される
                        function2_2 // ステータスが rejected のときに実行される
                )
                .start();

        System.out.println("Hello,Promise");

    }
}

実行結果:

Hello,Promise
Process-1
Rejected Process-2

説明:

function1

action.reject();

で完了しているので、ステータスがrejectedとなる。
次のthen

 .then(
         function2_1, // ステータスが fulfilled のときに実行される
         function2_2 // ステータスが rejected のときに実行される
 )

としている。
前述のとおり、thenの文法は Promise.then(onFulfilled[, onRejected]);であるので、
function1の完了ステータスがrejectedであるため、ここではthenの2つめの引数であるfunction2_2が実行される。

処理フロー:

image.png

(3)Promise.always: resolve、rejectどちらの処理結果も受け取る

コード:

public class Example30 {

    public static void main(String[] args) {
        Func function1 = (action, data) -> {
            action.reject("I send REJECT");
        };
        Func function2 = (action, data) -> {
            System.out.println("Received:" + data);
            action.resolve();
        };
        Promise.resolve()
                .then(function1)
                .always(function2)// ステータスが"fulfilled"でも"rejected"でも実行される
                .start();
    }
}

実行結果:

Received:I send REJECT

説明:

.always(function2)

のようにalways((action,data)->{})は、その前の処理が resolvedによるステータスfulfilledであろうと、rejectedによるステータスrejectedであろうと必ず実行される。

処理フロー:

image.png

(4)Promise.all複数の並列な非同期処理の完了待ちをして次に進む

コード:

public class Example40 {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        //非同期処理1
        Func function1 = (action, data) -> {
            new Thread(() -> {
                Promise.sleep(1000); System.out.println("func1 running");action.resolve("func1-result");
            }).start();
        };
        //非同期処理2        
        Func function2 = (action, data) -> {
            new Thread(() -> {
            Promise.sleep(500);System.out.println("func2 running"); action.resolve("func2-result");
            }).start();
        };
        //非同期処理3
        Func function3 = (action, data) -> {
            new Thread(() -> {
            Promise.sleep(100);System.out.println("func3 running");action.resolve("func3-result");
            }).start();
        };
        //最後に結果を受け取る処理
        Func function4 = (action, data) -> {
            System.out.println("結果を受け取りました");
            List<Object> resultList = (List<Object>) data;
            for (int i = 0; i < resultList.size(); i++) {
                Object result = resultList.get(i);
                System.out.println("非同期処理" + (i + 1) + "の結果は " + result);
            }
            action.resolve();
        };

        Promise.all(function1, function2, function3)
                .always(function4)
                .start();
    }
}

実行結果:

func3 running
func2 running
func1 running
非同期処理の結果を受け取りました
非同期処理1の結果は func1-result
非同期処理2の結果は func2-result
非同期処理3の結果は func3-result

説明:

  • Promise.all(function1,function2,・・・・functionN)function1functionNの複数の処理を引数にとることができ、それらを並列実行する

  • 並列実行が終わると、チェインされたthen(ここではalways)に処理が移行する。

  • 上の例では function1,function2,function3が並列に実行されるが、function1~function3すべてがfulfilledで完了した場合は、各function1~function3の結果がListに格納されthenに渡る。その際、格納順序は、引数に指定された function1,function2,function3の順番となる。(この仕様もJavaScriptのPromiseと同一)

  • function1~function3のうち、どれか1つでも失敗≒rejectになった場合、いちばん最初にrejectになったfunctionの結果(reject reason)が次のthenに渡る。(fail-fast原則)

処理フロー:

image.png

(5)Promise.all:その2スレッドプールを自分で指定する

(4)で説明したとおり、Promise.allFuncを並列動作をさせることができるが、事前に並列動作を行うときのスレッド生成ポリシーをExecutorをつかって定義可能。また、既に別の用途で使うために用意したスレッドプールをPromise.allに転用しても良い。

コード例:

public class Example41 {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {

        final ExecutorService myExecutor = Executors.newFixedThreadPool(2);

        // 非同期処理1
        Func function1 = (action, data) -> {
            System.out.println("No.1 " + Thread.currentThread());
            new Thread(() -> {
                Promise.sleep(1000);System.out.println("func1 running");action.resolve("func1-result");
            }).start();
        };

        // 非同期処理2
        Func function2 = (action, data) -> {
            System.out.println("No.2 " + Thread.currentThread());
            new Thread(() -> {
                Promise.sleep(500);System.out.println("func2 running");action.resolve("func2-result");
            }).start();
        };

        // 非同期処理3
        Func function3 = (action, data) -> {
            System.out.println("No.3 " + Thread.currentThread());
            new Thread(() -> {
                Promise.sleep(100);System.out.println("func3 running");action.resolve("func3-result");
            }).start();
        };

        // 最後に結果を受け取る処理
        Func function4 = (action, data) -> {
            System.out.println("No.4 final " + Thread.currentThread());
            System.out.println("結果を受け取りました");
            List<Object> resultList = (List<Object>) data;
            for (int i = 0; i < resultList.size(); i++) {
                Object result = resultList.get(i);
                System.out.println("非同期処理" + (i + 1) + "の結果は " + result);
            }
            myExecutor.shutdown();
            action.resolve();
        };

        Promise.all(myExecutor, function1, function2, function3)
                .always(function4)
                .start();
    }
}

実行結果:

No.1 Thread[pool-1-thread-2,5,main]
No.2 Thread[pool-1-thread-2,5,main]
No.3 Thread[pool-1-thread-2,5,main]
func3 running
func2 running
func1 running
No.4 final Thread[pool-1-thread-1,5,main]
結果を受け取りました
非同期処理1の結果は func1-result
非同期処理2の結果は func2-result
非同期処理3の結果は func3-result

結果から、Funcは同じスレッドプールから取り出されたスレッドで実行されていることがわかる。
(Funcの中であえてさらに非同期処理(new Thread)しているので、その非同期処理は指定したスレッドプールの外側になる)

説明:

  • Promise.allの実行に使うExecutorを定義する。以下はプールサイズが2のスレッドプール。
final ExecutorService myExecutor = Executors.newFixedThreadPool(2);
  • Promise.all(executor,func1,func2,func3,・・・・funcN)のようにしてExecutorを指定できる
 Promise.all(myExecutor, function1, function2, function3)
         .always(function4)
         .start();
  • 独自にExecutorを指定した場合は、忘れずにshutdownする
Func function4 = (action, data) -> {
   //中略
    myExecutor.shutdown();
    action.resolve();
};

スレッド生成ポリシー:

  • スレッドプールのサイズは2以上を指定する必要がある。(つまり、singleThreadExecutorは利用不可。)
  • java-promiseでは、Promise.allを行う場合、非同期実行のため1スレッドを使う。
  • さらに、Promise.allで並列実行をおこなうため、並列実行用に最低1スレッドが必要。(1スレッドだと並列とはいわないが)
  • この2つを合計すると2スレッド以上必要になる。

まとめ

  • JavaでPromiseを「JavaScriptライクに記述」する方法を試行しました
    • Java8ラムダ式をうまくとりいれるとJavaScriptの記法に近いカタチでPromiseを実行できました
    • 簡潔な記法で気の利いた処理ができるという点はJavaScript(ES)他スクリプト系言語の進化に学びたいとおもいます
      (非同期実行もJavaScriptではasync/awaitまで進化しました)
  • JavaでPromise(java-promise)のライブラリ側ソースコードは以下にあります
    https://github.com/riversun/java-promise

    • git clone https://github.com/riversun/java-promise.git して
    • mvn test すると単体テスト動確ができます
  • また、本稿内に掲載したサンプルコードは以下にあります
    https://github.com/riversun/java-promise-examples/tree/master-ja

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】JavaでPromiseを使った非同期処理を記述したい ~ JavaScriptのPromiseライクな文法の試行~

概要

  • Java言語で「JavaScriptのPromiseライクな文法で、非同期処理・並行処理を記述してみたい」と思いやってみました。
  • ソースは以下リポジトリにあります
    https://github.com/riversun/java-promise

コード例

非同期処理1が終わったら、その結果を使った非同期処理2が実行される」処理はJavaではどのように書けば良いでしょうか?

  • 解1:Java1.4時代の解:Threadクラスの機能でがんばる
    • Threadを入れ子式に持つとか、joinで終了待ちとか。並列処理黎明期。
  • 解2:Java5(1.5)時代の解:Callable/Futureで幸せになれたのかな・・・
    • Future/Callableで結果返せてうれしい、加えセマフォやラッチなど小道具そろったがそれなりに頑張る必要あり。
  • 解3:Java8時代の解:CompletableFutureで幸せになる、はず。

    • ついに待望?!のFuture/Promiseパターン的な仕組みが標準で登場!
  • 本稿では上の3つの解とは別の切り口で、以下のコードのようにJavaScriptのPromiseライクに書いてみました。

JavaでPromiseのサンプル
Promise.resolve()
 .then((action, data) -> {
   //非同期処理1
   new Thread(() -> {System.out.println("Process1");action.resolve("Result-1");}).start();
 })
 .then((action, data) -> {
   //非同期処理2
   new Thread(() -> {System.out.println("Process2");action.resolve("Result-2");}).start();
 })
 .start();

本稿でやりたいこと

  • やりたいことは以下のような処理を「JavaScriptのPromiseライクに記述すること」となります
    • 複数あるAPIを非同期に呼んで、結果を受け取ったら次のAPIを呼ぶ、という一連の処理
    • 複数の処理を同時に(並列に)に動かし、それがすべて完了したら次の処理に移るような処理

本稿で対象としないこと

  • (アカデミックな)Future/Promiseパターンの具現化
  • Java標準のコンカレント処理の使い方

対象環境

  • Java5以降
    • ライブラリはJava1.6ベースのAndroidでも動作します
    • Java8のコンカレント系APIはつかっていません

使い方(依存関係)

ライブラリjava-promiseとしてMavenレポジトリにありますので、以下を追加すればすぐに使えます。

Maven

POM.xmlのdependency
<dependency>
    <groupId>org.riversun</groupId>
    <artifactId>java-promise</artifactId>
    <version>1.1.0</version>
</dependency>

Gradle

build.gradle
dependencies {
    compile 'org.riversun:java-promise:1.1.0'
}
build.gradle(Android)
dependencies {
    implementation 'org.riversun:java-promise:1.1.0'
}

本編

JavaScriptで書くPromiseと、本稿で紹介するJavaで書く方法との比較

まず、比較のためにJavaScriptでPromiseを書いてみる

以下のコードは'foo'という文字列に非同期に実行された処理結果('bar')を連結するだけのJavaScriptのサンプルコードとなる。MDNでPromiseのサンプルとして公開されているものから抜粋した。

Example.js
Promise.resolve('foo')
    .then(function (data) {
        return new Promise(function (resolve, reject) {
            setTimeout(function () {
                const newData = data + 'bar';
                resolve(newData);
            }, 1);
        });
    })
    .then(function (data) {
        return new Promise(function (resolve, reject) {
            console.log(data);
            resolve();
        });
    });
console.log("Promise in JavaScript");

実行結果は以下のとおり

Promise in JavaScript
foobar

次にJava8でjava-promiseを使って書く

Example.java
import org.riversun.promise.Promise;

public class Example {

    public static void main(String[] args) {

        Promise.resolve("foo")
                .then(new Promise((action, data) -> {
                    new Thread(() -> {
                        String newData = data + "bar";
                        action.resolve(newData);//#resolveで次の処理に移行
                    }).start();//別スレッドで実行
                }))
                .then(new Promise((action, data) -> {
                    System.out.println(data);
                    action.resolve();
                }))
                .start();//処理開始のトリガー
        System.out.println("Promise in Java");
    }
}

実行結果は以下のとおり

Promise in Java
foobar

Promise以下の実行は非同期(別スレッド)で行われるので、この例ではSystem.out.println("Promise in Java");が実行されているのがわかる。

処理の都合最後に.start()を呼び出してPromiseチェインのトリガーをしている以外はJavaScriptのPromiseライクな文法に近づけてみた。

記法

ラムダ式を使わないで書く(Java7以前)

ラムダ式を使わなければ以下のようになる

ラムダ式を使わないで書いた場合
Promise.resolve("foo")
        .then(new Promise(new Func() {
            @Override
            public void run(Action action, Object data) throws Exception {
                new Thread(() -> {
                    String newData = data + "bar";
                    action.resolve(newData);
                }).start();
            }
        }))
        .then(new Promise(new Func() {
            @Override
            public void run(Action action, Object data) throws Exception {
                new Thread(() -> {
                    System.out.println(data);
                    action.resolve();
                }).start();
            }
        }))
        .start();

(action,data)->{}となっていた部分の正体はJavaScriptでいうところの function を表すインタフェースとなる。

Func.java
public interface Func {
    public void run(Action action, Object data) throws Exception;
}

さらにシンプルに書く

Promise.then(new Promise())ではなくPromise.then(new Func())でもOK。new Funcはラムダ式におきかえるとPromise.then((action,data)->{})となり、さらにシンプルになる。

then(Func)をつかって書く
Promise.resolve("foo")
   .then((action, data) -> {
       new Thread(() -> {
           String newData = data + "bar";
           action.resolve(newData);
       }).start();
   })
   .then((action, data) -> {
       System.out.println(data);
       action.resolve();
   })
   .start();

Promiseをつかった並行実行の各種パターン紹介

(1) Promise.then:非同期処理を順番通り実行する

コード:

public class Example20 {

    public static void main(String[] args) {

        // 処理1(別スレッド実行)
        Func function1 = (action, data) -> {
            new Thread(() -> {
                System.out.println("Process-1");
                Promise.sleep(1000);// Thread.sleepと同じ
                action.resolve("Result-1");// ステータスを"fulfilled"にして、次の処理に結果("Result-1")を伝える
            }).start();// 別スレッドでの非同期処理開始
        };

        // 処理2
        Func function2 = (action, data) -> {
            System.out.println("Process-2 result=" + data);
            action.resolve();
        };

        Promise.resolve()// 処理を開始
                .then(function1)// 処理1実行
                .then(function2)// 処理2実行
                .start();// 開始

        System.out.println("Hello,Promise");
    }

実行結果:

Hello,Promise
Process-1
Process-2 result=Result-1

説明:
thenの文法は Promise.then(onFulfilled[, onRejected]); つまり引数を2つまでとることができる
最初の引数onFulfilledは前の実行がfulfilled(≒成功)ステータスで終了した場合に実行される。
2つめの引数onRejectedはオプションだが、こちらは前の実行がrejected(≒失敗)ステータスで終了した場合に実行される。このサンプルは1つめの引数のみを指定している。

処理フロー:

image.png

  1. Promise.resolveでステータスをfullfilledにしてthenにチェインする。
  2. fullfilledなのでthenでは第一引数に指定されたfunction1を実行する
  3. function1action.resolveによりステータスをfullfilledにする
  4. function1action.resolveにString型引数"Result-1"をセットする
  5. 次のthenもステータスがfullfilledなのでfunction2が実行される
  6. function2実行時の引数dataにはfunction1の結果"Result-1"が格納されている

(2) action.resolve,action.reject:実行結果によって処理を分岐する

コード:

public class Example21 {

    public static void main(String[] args) {

        Func function1 = (action, data) -> {
            System.out.println("Process-1");
            action.reject();// ステータスを "rejected" にセットして実行完了
        };

        Func function2_1 = (action, data) -> {
            System.out.println("Resolved Process-2");
            action.resolve();
        };

        Func function2_2 = (action, data) -> {
            System.out.println("Rejected Process-2");
            action.resolve();
        };

        Promise.resolve()
                .then(function1)
                .then(
                        function2_1, // ステータスが fulfilled のときに実行される
                        function2_2 // ステータスが rejected のときに実行される
                )
                .start();

        System.out.println("Hello,Promise");

    }
}

実行結果:

Hello,Promise
Process-1
Rejected Process-2

説明:

function1

action.reject();

で完了しているので、ステータスがrejectedとなる。
次のthen

 .then(
         function2_1, // ステータスが fulfilled のときに実行される
         function2_2 // ステータスが rejected のときに実行される
 )

としている。
前述のとおり、thenの文法は Promise.then(onFulfilled[, onRejected]);であるので、
function1の完了ステータスがrejectedであるため、ここではthenの2つめの引数であるfunction2_2が実行される。

処理フロー:

image.png

(3)Promise.always: resolve、rejectどちらの処理結果も受け取る

コード:

public class Example30 {

    public static void main(String[] args) {
        Func function1 = (action, data) -> {
            action.reject("I send REJECT");
        };
        Func function2 = (action, data) -> {
            System.out.println("Received:" + data);
            action.resolve();
        };
        Promise.resolve()
                .then(function1)
                .always(function2)// ステータスが"fulfilled"でも"rejected"でも実行される
                .start();
    }
}

実行結果:

Received:I send REJECT

説明:

.always(function2)

のようにalways((action,data)->{})は、その前の処理が resolvedによるステータスfulfilledであろうと、rejectedによるステータスrejectedであろうと必ず実行される。

処理フロー:

image.png

(4)Promise.all複数の並列な非同期処理の完了待ちをして次に進む

コード:

public class Example40 {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        //非同期処理1
        Func function1 = (action, data) -> {
            new Thread(() -> {
                Promise.sleep(1000); System.out.println("func1 running");action.resolve("func1-result");
            }).start();
        };
        //非同期処理2        
        Func function2 = (action, data) -> {
            new Thread(() -> {
            Promise.sleep(500);System.out.println("func2 running"); action.resolve("func2-result");
            }).start();
        };
        //非同期処理3
        Func function3 = (action, data) -> {
            new Thread(() -> {
            Promise.sleep(100);System.out.println("func3 running");action.resolve("func3-result");
            }).start();
        };
        //最後に結果を受け取る処理
        Func function4 = (action, data) -> {
            System.out.println("結果を受け取りました");
            List<Object> resultList = (List<Object>) data;
            for (int i = 0; i < resultList.size(); i++) {
                Object result = resultList.get(i);
                System.out.println("非同期処理" + (i + 1) + "の結果は " + result);
            }
            action.resolve();
        };

        Promise.all(function1, function2, function3)
                .always(function4)
                .start();
    }
}

実行結果:

func3 running
func2 running
func1 running
非同期処理の結果を受け取りました
非同期処理1の結果は func1-result
非同期処理2の結果は func2-result
非同期処理3の結果は func3-result

説明:

  • Promise.all(function1,function2,・・・・functionN)function1functionNの複数の処理を引数にとることができ、それらを並列実行する

  • 並列実行が終わると、チェインされたthen(ここではalways)に処理が移行する。

  • 上の例では function1,function2,function3が並列に実行されるが、function1~function3すべてがfulfilledで完了した場合は、各function1~function3の結果がListに格納されthenに渡る。その際、格納順序は、引数に指定された function1,function2,function3の順番となる。(この仕様もJavaScriptのPromiseと同一)

  • function1~function3のうち、どれか1つでも失敗≒rejectになった場合、いちばん最初にrejectになったfunctionの結果(reject reason)が次のthenに渡る。(fail-fast原則)

処理フロー:

image.png

(5)Promise.all:その2スレッドプールを自分で指定する

(4)で説明したとおり、Promise.allFuncを並列動作をさせることができるが、事前に並列動作を行うときのスレッド生成ポリシーをExecutorをつかって定義可能。また、既に別の用途で使うために用意したスレッドプールをPromise.allに転用しても良い。

コード例:

public class Example41 {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {

        final ExecutorService myExecutor = Executors.newFixedThreadPool(2);

        // 非同期処理1
        Func function1 = (action, data) -> {
            System.out.println("No.1 " + Thread.currentThread());
            new Thread(() -> {
                Promise.sleep(1000);System.out.println("func1 running");action.resolve("func1-result");
            }).start();
        };

        // 非同期処理2
        Func function2 = (action, data) -> {
            System.out.println("No.2 " + Thread.currentThread());
            new Thread(() -> {
                Promise.sleep(500);System.out.println("func2 running");action.resolve("func2-result");
            }).start();
        };

        // 非同期処理3
        Func function3 = (action, data) -> {
            System.out.println("No.3 " + Thread.currentThread());
            new Thread(() -> {
                Promise.sleep(100);System.out.println("func3 running");action.resolve("func3-result");
            }).start();
        };

        // 最後に結果を受け取る処理
        Func function4 = (action, data) -> {
            System.out.println("No.4 final " + Thread.currentThread());
            System.out.println("結果を受け取りました");
            List<Object> resultList = (List<Object>) data;
            for (int i = 0; i < resultList.size(); i++) {
                Object result = resultList.get(i);
                System.out.println("非同期処理" + (i + 1) + "の結果は " + result);
            }
            myExecutor.shutdown();
            action.resolve();
        };

        Promise.all(myExecutor, function1, function2, function3)
                .always(function4)
                .start();
    }
}

実行結果:

No.1 Thread[pool-1-thread-2,5,main]
No.2 Thread[pool-1-thread-2,5,main]
No.3 Thread[pool-1-thread-2,5,main]
func3 running
func2 running
func1 running
No.4 final Thread[pool-1-thread-1,5,main]
結果を受け取りました
非同期処理1の結果は func1-result
非同期処理2の結果は func2-result
非同期処理3の結果は func3-result

結果から、Funcは同じスレッドプールから取り出されたスレッドで実行されていることがわかる。
(Funcの中であえてさらに非同期処理(new Thread)しているので、その非同期処理は指定したスレッドプールの外側になる)

説明:

  • Promise.allの実行に使うExecutorを定義する。以下はプールサイズが2のスレッドプール。
final ExecutorService myExecutor = Executors.newFixedThreadPool(2);
  • Promise.all(executor,func1,func2,func3,・・・・funcN)のようにしてExecutorを指定できる
 Promise.all(myExecutor, function1, function2, function3)
         .always(function4)
         .start();
  • 独自にExecutorを指定した場合は、忘れずにshutdownする
Func function4 = (action, data) -> {
   //中略
    myExecutor.shutdown();
    action.resolve();
};

スレッド生成ポリシー:

  • スレッドプールのサイズは2以上を指定する必要がある。(つまり、singleThreadExecutorは利用不可。)
  • java-promiseでは、Promise.allを行う場合、非同期実行のため1スレッドを使う。
  • さらに、Promise.allで並列実行をおこなうため、並列実行用に最低1スレッドが必要。(1スレッドだと並列とはいわないが)
  • この2つを合計すると2スレッド以上必要になる。

まとめ

  • JavaでPromiseを「JavaScriptライクに記述」する方法を試行しました
    • Java8ラムダ式をうまくとりいれるとJavaScriptの記法に近いカタチでPromiseを実行できました
    • 簡潔な記法で気の利いた処理ができるという点はJavaScript(ES)他スクリプト系言語の進化に学びたいとおもいます
      (非同期実行もJavaScriptではasync/awaitまで進化しました)
  • JavaでPromise(java-promise)のライブラリ側ソースコードは以下にあります
    https://github.com/riversun/java-promise

    • git clone https://github.com/riversun/java-promise.git して
    • mvn test すると単体テスト動確ができます
  • また、本稿内に掲載したサンプルコードは以下にあります
    https://github.com/riversun/java-promise-examples/tree/master-ja

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JMeterでSpring SecurityのCSRF対策を使用したAPIを呼び出す際の設定

やりたいこと

長いタイトルの通りです。
Vue.jsのアプリケーションから呼び出すAPIをSpring Bootで作ってまして、
そのAPIの負荷試験を行うことになったと。
JMeterを使用してシナリオを作成するのですが、Spring SecurityのCookieCsrfTokenRepositoryを使用したCSRF対策を使っているので

  • CSRFトークンとしてXSRF-TOKEN Cookieの値を取得して
  • POSTを使用したAPIをコールする際のX-XSRF-TOKENヘッダとして設定する

ということをシナリオ中で行う必要が生じたわけです。

試験対象APIのざっくり仕様

イメージだけ、こんな感じです。

url HTTPメソッド 概要
/login POST ID/Passwordを送信してログインする
ログイン成功時にXSRF-TOKENCookieが付与される
CSRFチェック対象外
/orders POST 受注登録を行う
CSRFチェック対象

環境

アプリケーション

  • Spring Boot 2.1.4

ツール

  • Apache JMeter 5.1.1
    • 設定はGUI起動で実施します。

JMeterの設定

XSRF-TOKENの値を取得する

/loginのレスポンスにXSRF-TOKENのCookieが含まれることになるので、それを抽出することになります。
一応、DevToolsで/loginのレスポンスを見てみると、こんな感じです。

XSRF-TOKEN.jpg

設定

/loginの呼び出しはHTTP Requestサンプラーで定義します。(詳細は割愛)
このサンプラーの実行後に、正規表現抽出を使用して、CSRFトークンを取得します。

手順
  1. /loginHTTP Requestサンプラーを選択して右クリック
  2. ADDPost ProcessorsRegular Expression Extractor を選択
  3. 以下の通り設定する(NameとCommentsは適当です) csrfトークン取得.jpg
  • Response Headersから抽出する
  • ヘッダからの抽出になるので、先程DevToolsで確認した形式、要は「Set-Cookie:」を含んだ形式で正規表現を書く必要がある

というあたりがポイントでしょうか。
これ以降のシナリオでは、${xsrf_token}を指定することで、抽出した値を変数から解決することができるようになります。

X-XSRF-TOKENヘッダを設定する

/ordersはCSRFチェック対象となるため、コール時にX-XSRF-TOKENヘッダを設定する必要があります。

設定

/ordersの呼び出しはHTTP Requestサンプラーで定義します。(詳細は割愛)
このサンプラーの実行時に、HTTP Header Managerを使用して、X-XSRF-TOKENヘッダを設定します。

手順
  1. /ordersHTTP Requestサンプラーを選択して右クリック
  2. ADDConfig ElementHTTP Header Manager を選択
  3. 以下の通り設定する(NameとCommentsは適当、content-typeは今回のAPI固有の仕様で必要なため設定しています)

X-XSRF-TOKEN付与.jpg

先程取得した${xsrf_token}を、X-XSRF-TOKENヘッダとして設定しています。

まとめ

以上の設定で、CSRF対策を通過させることができます。
JMeterに不慣れだと、こういうちょっとした応用ってすぐに思いつかなくて
意外と悩むかなと思った(不慣れな私の実体験です)ので書き留めておきました。

参考ページ

以下を参考にさせていただきました。
ありがとうございました。

https://www.blazemeter.com/blog/how-load-test-csrf-protected-web-sites

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

写真をリサイズして余白付き正方形にする Java プログラム

はじめに

やりたかったこと

  • Instagram に写真を投稿するとき、余白付きの正方形にしたい
  • 大きい画像はサーバーサイドで再圧縮されるのでリサイズしたい
  • PC で編集してから投稿したかったのでスマホアプリはめんどい
    • 既存のデスクトップアプリにありそうだけど勉強も兼ねて作ってみた

環境

  • Java SE 8u201
  • JavaFX 8
  • macOS Mojave 10.14.3

コード

何ができるか

  • 画像を 1080 * 1350 以内にアスペクト比維持でリサイズ
  • 1080 * 1080 正方形に収めて余白をつける(白 or 黒)(オプション)
    • 余白不要ならば f
    • 白余白なら w、黒余白なら b
  • コマンドラインでオプションと元画像ディレクトリ与えたら PNG で出力する
    • フォルダならその中の画像ファイル全てを対象に
    • 出力は resized_ を前につけて元画像と同じディレクトリに

全文

image-resizer.java
package main;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.AreaAveragingScaleFilter;
import java.awt.image.BufferedImage;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageFilter;
import java.awt.image.ImageProducer;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.imageio.ImageIO;


import javafx.application.Application;
import javafx.stage.Stage;

public class Main extends Application {

    public static boolean makeSquare = true;;

    public static ArrayList<File> dirFiles = new ArrayList<File>();
    public static BufferedImage inputImage;
    public static double maxWidth = 1080;
    public static double maxHeight = 1350;

    public static int outputWidth;
    public static int outputHeight;

    public static int[] startCoodinate = {0, 0};
    public static Color background;

    public static String outputPreffix = "resized_";
    public static File output;
    public static String extension = "png";
    public static List<String> readble = Arrays.asList("jpeg", "jpg", "gif", "png", "bmp");


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

    @Override
    public void start(Stage primaryStage) throws Exception {
        Parameters params = getParameters();
        List<String> args = params.getRaw();

        switch(args.get(0)) {
        case "f":
            makeSquare = false;
            break;
        case "w":
            background = Color.white;
            break;
        case "b":
            background = Color.black;
            break;
        default:
            System.out.println("Input 'f', 'w' or 'b' as 1st argument.");
            System.exit(0);
        }

        if (args.size() > 1) {
            for (int i = 1; i < args.size(); i++) {
                System.out.println("Processing " + args.get(i) + ".");
                if (checkDirectory(args.get(i))) {
                    for (File f: dirFiles) {
                        System.out.println("Processing " + f.getName() + ".");
                        initialize(f);
                        scaleImage(inputImage, output, outputWidth, outputHeight);
                    }
                    dirFiles.clear();
                } else {
                    try {
                        initialize(new File(args.get(i)));
                        scaleImage(inputImage, output, outputWidth, outputHeight);
                    } catch (Exception e){
                        System.out.println(new File(args.get(i)).getName() + " could not be loaded.");
                    }
                }
            }
            System.exit(0);
        } else {
            System.out.println("Input file or directory name as 2nd and later arguments.");
            System.exit(0);
        }
    }

    public static void initialize(File input) throws Exception {
        inputImage = ImageIO.read(input);
        calculateSize(inputImage);

        String fileNameWithoutExtension = input.getName().substring(0, input.getName().lastIndexOf(".") + 1);
        output = new File(input.getParent(), outputPreffix + fileNameWithoutExtension + extension);
    }

    public static void calculateSize(BufferedImage org) {
        if (makeSquare) {
            maxHeight = maxWidth;
        }
        double scale = Math.min(maxWidth / org.getWidth(), maxHeight / org.getHeight());
        outputWidth = (int)(org.getWidth() * scale);
        outputHeight = (int)(org.getHeight() * scale);
        if (makeSquare) {
            startCoodinate[0] = (int)Math.max(0, (maxWidth - outputWidth) / 2);
            startCoodinate[1] = (int)Math.max(0, (maxHeight - outputHeight) / 2);
        }
    }

    public static boolean checkDirectory(String dir) {
        File inputDir = new File(dir);
        if(inputDir.isDirectory()) {
            for (File f: inputDir.listFiles()) {
                if (readble.contains(f.getName().substring(f.getName().lastIndexOf(".") + 1).toLowerCase())) {
                    dirFiles.add(f);
                } else {
                    System.out.println(f.getName() + " was skipped. Only JPG, GIF, PNG, BMP are supported.");
                }
            }
        }
        return inputDir.isDirectory();
    }

    public static void scaleImage(BufferedImage org, File out, int width, int height){
        try {
            ImageFilter filter = new AreaAveragingScaleFilter(width, height);
            ImageProducer p = new FilteredImageSource(org.getSource(), filter);
            Image dstImage = Toolkit.getDefaultToolkit().createImage(p);

            if (makeSquare) {
                outputWidth = (int)maxWidth;
                outputHeight = (int)maxHeight;
            }
            BufferedImage dst = new BufferedImage((int)outputWidth , (int)outputHeight, BufferedImage.TYPE_INT_ARGB);
            Graphics2D g = dst.createGraphics();
            g.setColor(background);
            g.fillRect(0, 0, dst.getWidth() - 1, dst.getHeight() - 1);
            g.drawImage(dstImage, Math.max(0, startCoodinate[0]), Math.max(0, startCoodinate[1]), null);
            g.dispose();
            ImageIO.write(dst, extension, out);
            System.out.println(out.getName() + " was successed.");
        } catch (IOException e) {
            System.out.println(out.getName() + " could not be written.");
        }
    }

}

使いやすくするために

  • Windows マシンでは以下のバッチファイルを使うとラク
  • もっと言うと、cmd /c "C:\*\image-resizer.bat" をタスクバーに入れるとラク
image-resizer.bat
@echo off
setlocal
set /p dir="対象のファイルまたはフォルダ(複数指定可): "
set /p makeSqare="余白を追加して正方形にしますか?(y/n): "
if %makeSqare%==y (
    set /p firstAug="余白の色は白または黒です(w/b): "
)
if %makeSqare%==n (
    set firstAug=f
)
echo 処理を開始します
java -jar image-resizer.jar %firstAug% %dir%
echo リサイズされた PNG が元画像と同じフォルダに保存されました
pause

参考

画像リサイズ - Qiitasaka1029 さんのコメント

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む