読者です 読者をやめる 読者になる 読者になる

mike-neckのブログ

JavaかJavaFXかJavaEE(なんかJava8が多め)

Java9(ea163)でHttpClientを使ってみたノート

ディレクトリ構成やmodule-info.javaで少々ハマったので(10分)、ノート。


Java SE9から公式のやっと面倒くさくないHttp Clientが出るので試してみた。

試してみたのは jdk.incubator.httpclient モジュールの jdk.incubator.http.HttpClient とその関連クラス。

1. ディレクトリー構成

Java9からはモジュールを定義しておかないと、使いたいクラスがうまく利用できない。したがって、まずモジュール名を決定した後にモジュール名と同じ名前のディレクトリーを作り、その下に src ディレクトリーを作る。

まずモジュール名は com.example とした。

ディレクトリーを次のように作る。

root/
└── com.example
    └── src

2. モジュールの定義

src ディレクトリーの下に module-info.java を作る。ここには、公開するパッケージの定義と利用するモジュールを記入する。詳しい話は IT Proの連載記事 などを参考にした。なお、今回作るプログラムを配置するパッケージを com.example.ex とした。また、利用するモジュールは上述のとおり jdk.incubator.httpclient である。

module com.example {
  exports com.example.ex;
  requires jdk.incubator.httpclient;
}

現在のディレクトリー構成

root/
└── no.lib
    └── src
        └── module-info.java

3. プログラムを書く

適当に書く。

package com.example.ex;

import jdk.incubator.http.HttpClient;
import jdk.incubator.http.HttpRequest;
import jdk.incubator.http.HttpResponse;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class HttpClientTest {

    public static void main(String[] args) {
        final ExecutorService executor = Executors.newFixedThreadPool(1);
        final HttpClient client = HttpClient
                .newBuilder()
                .version(HttpClient.Version.HTTP_1_1)
                .executor(executor)
                .followRedirects(HttpClient.Redirect.SAME_PROTOCOL)
                .build();
        final HttpRequest request = HttpRequest.newBuilder(URI.create("https://www.google.com/teapot"))
                .GET()
                .build();
        client.sendAsync(request,
                HttpResponse.BodyHandler.asString(StandardCharsets.UTF_8))
                .thenApply(HttpResponse::body)
                .thenApply(s -> String.format("[%s] - %s", Thread.currentThread().getName(), s))
                .thenAccept(System.out::println)
                .join();
        executor.shutdown();
    }
}

実行結果

WARNING: Using incubator modules: jdk.incubator.httpclient
[pool-1-thread-1] - <!doctype html><html class="google" lang="en"> <script>(function(H){H.className=H.className.replace(/\bgoogle\b/,'google-js')})(document.documentElement)</script> <meta charset="utf-8"> <meta content="initial-scale=1, minimum-scale=1, width=device-width" name="viewport"> <title>Error 418 (I&#8217;m a teapot)!?</title> <link href="//www.gstatic.com/teapot/teapot.min.css" rel="stylesheet"> <a href="//www.google.com/"><span aria-label="Google" id="logo"></span></a> <p><b>418.</b> <ins>I&#8217;m a teapot.</ins></p> <p>The requested entity body is short and stout. <ins>Tip me over and pour me out.</ins></p> <div id="teaset"><div id="teabot"></div><div id="teacup"></div></div> <script src="//www.gstatic.com/teapot/teapot.min.js"></script> </html>

Process finished with exit code 0

現在のディレクトリー構成

└── com.example
    └── src
        ├── com
        │   └── example
        │       └── ex
        │            └── HttpClientTest.java
        └── module-info.java

たまにモジュール内のパッケージの export 先が決まっていることがあったりするので、気をつけたい。

Spring BootでThymeleaf3を使うときのbuild.gradle

ググってもMavenの方法しか出てこないのでメモ。

start.spring.io で作ったbuild.gradleに次を追加するだけ。

ext['thymeleaf.version'] = '3.0.0.RELEASE'
ext['thymeleaf-layout-dialect.version'] = '2.0.0'

元ネタはこちらのステファン・ニコルさんのコメント

github.com

JSUG勉強会 2017 - 3 〜ドメイン駆動設計 powered by Spring のメモ

JSUGの勉強会に参加したので、そのメモ。

jsug.doorkeeper.jp

資料は公開されるだろうから、ここでしか聞けないことを中心にメモ(ツイート)したつもりだが、資料に盛り込まれている可能性は十分にある。なお、資料は以下の通り。

www.slideshare.net


個人的な感想

  • ドメイン駆動のセッションや話は「ドメイン駆動はいいぞ」という感じで抽象的な話になりがちなことが多いのだが、今回はフレームワークが限定されたこともあってどのようなオブジェクトをどのように作っていくという具体的な話が聞けて満足だった

  • ビジネスルールとドメインロジック
    • ビジネスルール(契約・法令・明文化されていない商習慣)などがソフトウェア開発の分析対象(もちろん、ビジネスルールがゲームのルールだったりサービスのルールだったりもする)
    • その分析結果をプログラミング言語で表現したものがドメインロジック

  • ソフトウェア開発のプロセス
    • いわゆるウォーターフォールのプロセス(情報収集・分析・設計・実装・テスト・リリース・運用)に加えてモデリングをおこなう
    • モデリングすると無理・無駄・ムラが減る
    • しかしモデリングするから仕様を決めなくてよいというわけではない
    • これらのプロセスをすべて毎日やる
  • Spring Frameworkドメイン駆動
  • いわゆるウォーターフォール(個人の所感を含む)

    • 一般的な文脈で言うSIerウォーターフォールは「伝言ゲーム」
    • 「伝言ゲーム」 = 顧客へのヒアリングする人と設計する人と開発する人が違うような開発
      • まあ、規模が大きいからと言って数百人とか用意しているようなプロジェクトだと、数百人で顧客のところへ押しかけても顧客が迷惑するだけですけどね…
    • もちろんフレームワークを何を選定するかはプロジェクトの自由なので、Spring Bootで「伝言ゲーム」開発も可能
  • ソフトウェアの構成

トランザクションスクリプト = ドメインモデルを構築しない手続きだけを書いたソフトウェア

トランザクションスクリプト型のSpringアプリケーションの構成

f:id:mike_neck:20170329221918p:plain

ドメイン駆動設計によるSpringアプリケーションの構成

f:id:mike_neck:20170329222009p:plain

ドメイン駆動でアプリケーションを作ると、各層とは別にドメインモデルの層を用意して、ここをインクリメンタルに成長させていく

  • ドメインモデル
    • ビジネスロジックの要素は次のものしかない
      • 数値
      • 日付
      • 文字列
    • オブジェクトの種類は次の3パターン
      • 値オブジェクト(数値/日付/文字列をラップしただけのオブジェクト、いわゆるValue Object)
      • 区分オブジェクト(振る舞いをもった enum)
      • コレクションオブジェクト(ListSet をラップしたオブジェクト(ListSet には直接触れさせないで、必要な操作だけを提供する))
  • モジュールの単位
  • ドメインオブジェクトとその他の層
  • ソースコードの文書化

    • メソッド名、クラス名だけでなく、メソッドの返す型、引数の型にはJava APIが出てこないようにして、ソースコードが自然とドメインを語るようにする
  • 具体的なドメインモデルによるSpringアプリケーションの構成

    • HTML ⇔ Controller ⇔ ドメインオブジェクト(プレゼンテーション層)
      • コントローラーはドメインオブジェクトを引数に取るようにする
      • 表示処理(〜の場合には表示する/しない)の演算をプレゼンテーション層には書かない(すべてドメインモデルにておこなう)
      • ドメインオブジェクトにはgetter/setterを設けず、 DirectFieldAccess を有効化してフィールドアクセスする
    • サービス層(アプリケーション層)
      • サービス層(アプリケーション層)には @Validated を引数のドメインオブジェクトにつけておいて、契約による設計を実現させる
    • データソース層
      • データベース層とドメインオブジェクトは別物ゆえ、マッピングを作っている(MyBatis使用)
      • ドメインオブジェクトはロジックの置き場なので、テーブルの論理を持ち込まない
      • データベース層も更新が楽になる、かつデータベースの論理を徹底できるように、きっちり正規化する

後半のトークセッション

ドメインモデルの用語はどこから取り入れているのかという質問に対して

同じく名前についてわたびきさんのエピソード

ロジックの順序について

既存コードが有る状態でのドメイン駆動の取り込み方 : 少しずつ入れていく

Javaでの和暦の扱いについて

Date and Time APIでの和暦の扱いをちゃんと調べていなかったので、調べた。

なお、以下で用いるコードにおいては次のフォーマッタおよび、タイムゾーンを使った。

final ZoneId tokyo = ZoneId.of("Asia/Tokyo");

final DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendText(ChronoField.ERA)
        .appendText(ChronoField.YEAR_OF_ERA)
        .appendLiteral("年")
        .appendText(ChronoField.MONTH_OF_YEAR)
        .appendText(ChronoField.DAY_OF_MONTH)
        .appendLiteral("日")
        .toFormatter(Locale.JAPAN);

final DateTimeFormatter iso = DateTimeFormatter.ISO_DATE;

1.和暦を扱う場合は以下のクラスを使う。

  • java.time.chrono.JapaneseDate
  • java.time.chrono.JapaneseEra

2.日付インスタンスの作り方

  • 日付を指定する場合は JapaneseDate#of(JapaneseEra, int, int, int) あるいは JapaneseDate#of(int, int, int) を使う。
  • 現在時刻が欲しい場合は JapaneseDate#now または JapaneseDate#now(ZoneId) または JapaneseDate#now(Clock) を使う。
  • 他の日付型から変換したい場合は JapaneseDate#from(TemporalAccessor) を使う。

というわけで、試してみましょう。まず、東京の今の日付が欲しい場合は次のようになります。

final JapaneseDate now = JapaneseDate.now(tokyo);
System.out.println(now.format(formatter));

結果

平成29年3月23日

昭和最後の日付近辺

final JapaneseDate showa = JapaneseDate.of(1989, 1, 7);
System.out.println(showa.format(formatter));
final JapaneseDate heisei = JapaneseDate.of(1989, 1, 8);
System.out.println(heisei.format(formatter));

結果

昭和64年1月7日
平成1年1月8日

明治は6年以前というのがないそうです

try {
  final JapaneseDate meiji = JapaneseDate.of(1872, 12, 31);
  System.out.println(meiji.format(formatter));
} catch (Exception e) {
  System.out.println(e.getClass().getCanonicalName());
  System.out.println(e.getMessage());
  System.out.println("1872/12/31");
}

結果

java.time.DateTimeException
JapaneseDate before Meiji 6 is not supported
1872/12/31

というわけで、明治6年以前は出力されないので、 厳密 にJIS X 0301に対応している模様です。


同様に和暦を変換していきます。

final JapaneseDate h = JapaneseDate.of(平成, 29, 1, 1);
System.out.println(h.format(iso));
final JapaneseDate m = JapaneseDate.of(明治, 6, 1, 1);
System.out.println(m.format(iso));
try {
  JapaneseDate.of(明治, 5, 12, 31);
} catch (Exception e) {
  System.out.println(e.getClass().getCanonicalName());
  System.out.println(e.getMessage());
  System.out.println("明治5年12月31日");
}

結果

2017-01-01
1873-01-01
java.time.DateTimeException
JapaneseDate before Meiji 6 is not supported
明治5年12月31日

こちらも難しいことは特に必要なさそうです。


最後に別の型からの変換

final LocalDate ldm = LocalDate.of(1873, 1, 1);
System.out.println(JapaneseDate.from(ldm).format(formatter));
try {
  final LocalDate ldk = LocalDate.of(1872, 12, 31);
  System.out.println(JapaneseDate.from(ldk).format(formatter));
} catch (Exception e) {
  System.out.println(e.getClass().getCanonicalName());
  System.out.println(e.getMessage());
  System.out.println("明治5年12月31日");
}

結果

明治6年1月1日
java.time.DateTimeException
JapaneseDate before Meiji 6 is not supported
明治5年12月31日

最後に古の java.util.Date からの変換ですが、かなり面倒です。もう java.util.Date 使うなと言いたくなるくらいです。

final Date date = new Date();
final Instant instant = date.toInstant();
final ZonedDateTime zonedDateTime = instant.atZone(tokyo);
final JapaneseDate japaneseDate = JapaneseDate.from(zonedDateTime);
System.out.println(japaneseDate.format(formatter));

結果

平成29年3月23日

API的には JapaneseDate.from(instant) とやりたくなりますが、ゾーン情報を持たない Instant は変換できません(。逆もまた然り)。まあ、日付の計算も面倒ですし、 java.util.Date 使うのやめましょう。


java.util.Date 使うのやめましょう」と書くと、「これだからJava8の使えるサーバーの人間は…」と言われるので、紹介しておくと、これと同じAPIAndroidでも使えます。threetenbpというプロジェクトがあって、そちらがターゲットバージョン1.6で同じAPI(パッケージは異なる)のものを提供しています。したがってJava8が使えない環境の方はそちらを使うとよいです。

github.com

また、ThreeTenABP(Three Ten Android Back Port)というプロジェクトもあるようですが、ソースを見たところクラスの数が異様に少ないので同じAPIが使えるかどうかわかりません。どうやらメモリおよび速さの関係からバイナリファイルの中にゾーンの情報をすべて入れているようなのですが、JapaneseEraとかどうやってるんだろうという印象を持ったりします。

github.com

(追記:2017/03/24 1:03)教えてもらいました。ThreeTenABPはThreeTenに依存しているので同じAPIを使えるようです。


おわり