- 投稿日:2019-05-06T22:30:10+09:00
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 ClassPHP
一方、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.newSubClass initialize of Sub Class SubClass2 initialize of Sub Class SubClass3 initialize of Super Class SubClass4 initialize of Super Class参考
Java
- Java言語規定 第2版
- The Java Language Specification Java SE 12 Edition
- 基礎から始めるJavaのコンストラクタ 構文から上手な使い方まで
- [Java]クラスの継承とコンストラクタ
PHP
- 公式マニュアル
Ruby
雑感
個人的好みだけで言うと、勝手に後ろで動かれるのは好きではないため、PHP や Ruby のほうが好みです。4
ただ、 継承 が is-a 関係 であるべきことを考えると、「暗黙的コンストラクタ呼び出し」を行わない PHP や Ruby では、サブクラスが全く関係ないスーパークラスを継承できてしまうようにも思います。
そういう観点で考えると、「暗黙的コンストラクタ呼び出し」はむしろそうなるのが自然で、そうなって問題が起きるようなプログラムはそもそもオブジェクト指向的でないということになるのかなとも思います。
PHP 4 ではじめてオブジェクト指向を取り入れたときは、PHP でも、コンストラクタはクラス名と同名のメソッドでした。その後、PHP 5 で
__construct()
と書くようになりましたが、PHP 4 形式で書いてもコンストラクタとして認識されました。しかし、PHP 7 では、PHP 4 形式で書いた場合に「E_DEPRECATED」エラーが出力されるようになりました。そして、今後リリースされる PHP 8 からは、コンストラクタとして認識しなくなる予定です。詳しくは RFC を参照のこと。 ↩Java だけでなく C++ でも同様だと思います。 ↩
最初に触れたオブジェクト指向言語(?)が PHP だったからというのもあるかもしれません。というか、それに尽きるか。 ↩
- 投稿日:2019-05-06T18:10:18+09:00
Spring Boot入門その1
この記事はこちらのブログをQiitaに移したものです。
まだSpring Bootを使うようになって1ヶ月ちょっとしか経っていなくて、説明が不適切な箇所があるかもしれませんが、その際は指摘していただければと思います。WEB+DB PRESS106号でSpring Bootの記事でタスク管理サービスとQiitaのクロール&配信サービスを作るというのがあったので、とりあえずタスク管理サービスをの方をやってみました。
今回WEB+DB PRESSの記事をまねて作ったソースコードはこちらにあります。今後色々と改良していく予定です。
ちなみに、完成品(?)はこんな感じです。
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を書くときはもう少し詳しく書ければと思います。
- 投稿日:2019-05-06T17:33:39+09:00
「GWの次の祝日はいつやろ」→「69日後」→「どんだけ〜」なWebアプリを作りました
わい「次の祝日はいつかなーっと」
— だいぱんまん?技術ブログ (@donchan922) 2019年5月6日
画面「7月15日(月)海の日まであと、69日10時間47分22秒」
わい「どんだけ〜」
そんなWebアプリ「次の祝日までどんだけ〜」を作りました☺️皆さんも一緒に次の祝日がくるのをカウントダウンして待ちわびてやりましょう!(絶望の69日後)https://t.co/sIvmI897Ee pic.twitter.com/lEWAKYH7Ca今年のGWは夢の10連休でしたが、それも今日で終わり。なんとなく次の祝日を調べてみたらなんと69日先。こうなりゃもう、次の祝日までカウントダウンして思う存分待ちわびてやろうと思い、勢いでWebアプリケーション「次の祝日までどんだけ〜」を作成しました
Webアプリケーション「次の祝日までどんだけ〜」
次の祝日までをカウントダウン形式で表示してくれるWebアプリです。文字だけだと味気ないので、祝日ごとに画像を表示しています。
画像はすべていらすとやのものを使わせていただきました。どの祝日の画像もすべて用意されていて驚きました。いらすとやさんすごい。。ただ、2020年から「体育の日」が「スポーツの日」に変更されるのですが、さずがにまだスポーツの日の画像は用意されていませんでした(用意してほしいですー
)。
技術の話
ここからは少し真面目に技術のことについて書いていきます。
使用技術
- フロントエンド
- 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をちょこちょこっと書いて行いました。ですので、スマホでも見ていただけると嬉しいです
ドメイン名が「next-holiday-dondake.herokuapp.com」とHerokuのデフォルトになっているのは見逃してください
気が向けば独自ドメインを取得しようと思います。
仕組み
- 祝日のリストを用意する
- 内閣府が提供している国民の祝日のCSVを利用
- 祝日ごとの画像を用意する
- いらすとやを利用
- 次の祝日の判定ロジックを組み込む
- 祝日のリストを読み込み、現在日時から直近の祝日を取得
- 画面を表示する
- Thymeleafを使って次の祝日情報(名前、日付、画像名)をViewに返す
- JavaScriptでカウントダウンを作る
こうやって仕組みを並べてみると、たくさんのサイトにお世話になっているなと感じます
オリジナルの仕組みとしては、「次の祝日の判定ロジックを組み込む」くらいですね。
リリース最優先で開発を進めたため、コードが汚かったり、テストコードを書いていなかったりします。ソースは公開していますので、プルリクお待ちしています
まとめ
連休明けは仕事や学校に行くのがおっくうになるかと思いますが、次の祝日は必ずや訪れます。皆さんも一緒に次の祝日がくるのをカウントダウンして待ちわびてやりましょう!(絶望の69日後)
参考リンク
- 投稿日:2019-05-06T17:12:43+09:00
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();よく使う、ソート後のデータを取得する方法をメモ。
- 投稿日:2019-05-06T17:06:56+09:00
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の値を求めるのに何段階か必要になります。
- 正規リクエスト文字列(canonicalRequest)の生成
- 署名文字列(StringToSign)の生成
- 署名キー(SigningKey)の生成
- 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サービスがあったら、コメントに書いていただけるとありがたいです。
お役に立ちましたら幸いです。
- 投稿日:2019-05-06T14:15:18+09:00
ABC - 027 - A&B&C
AtCoder ABC 027 A&B&C
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; } }
- 投稿日:2019-05-06T11:30:37+09:00
クロージャーの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(); 2innerが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.mjsimport * 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.jsclosure.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() 2Pythonもほぼ同じで分かりやすいが、一つ余分なものが入っている。その名は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.pya = 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 += 1closure.pyfrom 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()なぜこのようにしていいのか等含めて時間があったら別記事として書きたい。
まとめ
クロージャーって難しいなあ。
- 投稿日:2019-05-06T04:08:19+09:00
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(1.5.0)以降から使えるExecutorServiceやCallable
- Java8以降から使えるCompletableFutureを使って書く方法
対象環境
- 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.gradledependencies { 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.jsPromise.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.javaimport 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 foobarPromise以下の実行は非同期(別スレッド)で行われるので、この例では
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.javapublic 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つめの引数のみを指定している。処理フロー:
- Promise.resolveでステータスをfullfilledにしてthenにチェインする。
- fullfilledなのでthenでは第一引数に指定されたfunction1を実行する
- function1もaction.resolveによりステータスをfullfilledにする
- function1はaction.resolveにString型引数"Result-1"をセットする
- 次のthenもステータスがfullfilledなのでfunction2が実行される
- 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が実行される。処理フロー:
(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であろうと必ず実行される。
処理フロー:
(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)はfunction1~functionNの複数の処理を引数にとることができ、それらを並列実行する
並列実行が終わると、チェインされた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原則)
処理フロー:
(5)Promise.all:その2スレッドプールを自分で指定する
(4)で説明したとおり、Promise.allでFuncを並列動作をさせることができるが、事前に並列動作を行うときのスレッド生成ポリシーを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
- 投稿日:2019-05-06T04:08:19+09:00
【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(1.5.0)以降から使えるExecutorServiceやCallable
- Java8以降から使えるCompletableFutureを使って書く方法
対象環境
- 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.gradledependencies { 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.jsPromise.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.javaimport 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 foobarPromise以下の実行は非同期(別スレッド)で行われるので、この例では
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.javapublic 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つめの引数のみを指定している。処理フロー:
- Promise.resolveでステータスをfullfilledにしてthenにチェインする。
- fullfilledなのでthenでは第一引数に指定されたfunction1を実行する
- function1もaction.resolveによりステータスをfullfilledにする
- function1はaction.resolveにString型引数"Result-1"をセットする
- 次のthenもステータスがfullfilledなのでfunction2が実行される
- 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が実行される。処理フロー:
(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であろうと必ず実行される。
処理フロー:
(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)はfunction1~functionNの複数の処理を引数にとることができ、それらを並列実行する
並列実行が終わると、チェインされた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原則)
処理フロー:
(5)Promise.all:その2スレッドプールを自分で指定する
(4)で説明したとおり、Promise.allでFuncを並列動作をさせることができるが、事前に並列動作を行うときのスレッド生成ポリシーを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
- 投稿日:2019-05-06T03:15:28+09:00
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-TOKEN
Cookieが付与される
CSRFチェック対象外/orders POST 受注登録を行う
CSRFチェック対象環境
アプリケーション
- Spring Boot 2.1.4
ツール
- Apache JMeter 5.1.1
- 設定はGUI起動で実施します。
JMeterの設定
XSRF-TOKENの値を取得する
/login
のレスポンスにXSRF-TOKEN
のCookieが含まれることになるので、それを抽出することになります。
一応、DevToolsで/login
のレスポンスを見てみると、こんな感じです。設定
/login
の呼び出しはHTTP Request
サンプラーで定義します。(詳細は割愛)
このサンプラーの実行後に、正規表現抽出
を使用して、CSRFトークンを取得します。手順
/login
のHTTP Request
サンプラーを選択して右クリックADD
→Post Processors
→Regular Expression Extractor
を選択- 以下の通り設定する(NameとCommentsは適当です)
![]()
- Response Headersから抽出する
- ヘッダからの抽出になるので、先程DevToolsで確認した形式、要は「Set-Cookie:」を含んだ形式で正規表現を書く必要がある
というあたりがポイントでしょうか。
これ以降のシナリオでは、${xsrf_token}
を指定することで、抽出した値を変数から解決することができるようになります。X-XSRF-TOKENヘッダを設定する
/orders
はCSRFチェック対象となるため、コール時にX-XSRF-TOKEN
ヘッダを設定する必要があります。設定
/orders
の呼び出しはHTTP Request
サンプラーで定義します。(詳細は割愛)
このサンプラーの実行時に、HTTP Header Manager
を使用して、X-XSRF-TOKEN
ヘッダを設定します。手順
/orders
のHTTP Request
サンプラーを選択して右クリックADD
→Config Element
→HTTP Header Manager
を選択- 以下の通り設定する(NameとCommentsは適当、content-typeは今回のAPI固有の仕様で必要なため設定しています)
先程取得した
${xsrf_token}
を、X-XSRF-TOKEN
ヘッダとして設定しています。まとめ
以上の設定で、CSRF対策を通過させることができます。
JMeterに不慣れだと、こういうちょっとした応用ってすぐに思いつかなくて
意外と悩むかなと思った(不慣れな私の実体験です)ので書き留めておきました。参考ページ
以下を参考にさせていただきました。
ありがとうございました。https://www.blazemeter.com/blog/how-load-test-csrf-protected-web-sites
- 投稿日:2019-05-06T01:00:01+09:00
写真をリサイズして余白付き正方形にする 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.javapackage 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参考
画像リサイズ - Qiita の saka1029 さんのコメント