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

文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

ようやくSaStrutsを卒業してSpring bootデビューしたので良かった点と悪かった点を書いてみます

springboot java プログラミング

長い道のりでした・・・
f:id:treeapps:20160113231034p:plain

今回の記事は長文になります。

ありがとうSeasar。そしてさようなら。

d.hatena.ne.jp

Seasarの生みの親であるひがやすお氏がSeasar終了宣言をしてから1年以上経ちました。

日本産フレームワークという事で、日本中でSeasarをみかけるようになり、中でもSaStrutsは沢山使われたと思います。というか、今現在でも私の会社では大量のSaStrutsを使ったサービスが保守稼働しております。勿論私も複数のSaStruts案件を今現在保守しています。

次のフレームワークは

久しぶりに新規案件の開発をする事になり、完全に終わりを迎えるSaStrutsを使うわけにもいかないので、いくつかのjavaフレームワークを選定する必要がありました。候補は以下の2つです。

  • Play framework
  • Spring boot

DropwizardやSpark等も興味深いFWですが、エンジニアが確保できそうもなかったり、継続してメンテナンスされるか怪しいFWを選ぶ事は難しいため、この2つになりました。今の会社では新規案件はPlayが多かったのですが、私はSpring bootを選択しました。理由は以下の通りです。

  • Lightbend(旧Typesafe)社がScala言語の開発元なので、今後java版をDeprecatedにしてscala版を推奨にする可能性があること(個人的な想像の話です)。
  • Playがバージョンアップ毎にそれなりに大きな変化があること(私はよく知りませんがそうみたいです)。
  • Springフレームワーク開発元であるPivotalの親がDell Technologiesであり、後ろ盾が強い(と思われる)こと。
  • 少し古い記事ですが、Spring bootが世界的に注目されていたこと。 Languages And Frameworks | Technology Radar | ThoughtWorks

要は、長期間バージョンアップしてくれそうで、革新的で、楽なものを選びたかったのです。社内でPlay採用事例が多いですが、ほとんどが「既存案件にPlayが多いから、いざという時に先駆者達に頼る事ができる(可能性がある)」という短絡的な理由で採用されている事が多いようでした。本来であれば、「このプロジェクトはこういう要件で、Playがプロジェクト要件にマッチするからPlayを選択した」となるのが理想ですが、現実はそうではありませんね。

私を含めた開発メンバー全員がPlayもSpring bootも使用経験が無いので「Playを選んでもSpring boot選んでもどちらにせよ解らない部分がだらけだし、Playを選択して困った時に正義のお助けマンが都合よく自分を助けてくれるという事を前提としたくはない。ならどちらを選択しても結局困る時は困るので、いっそ採用事例が極小なSpring boot使って自分が先駆者になるわ」としました。当ブログで既にいくつかSpring bootの記事を書いていますが、それらはいつか来る新FW選定のためでもあり、既に数ヶ月の検証(個人でですが)を経ての選択でした。

Spring bootを使ってみて

実はまだSpring bootを使ったWEBサイトはリリースできていないのですが、開発は終盤でほぼ落ち着いていおり、後はグローバルリリースを迎えるのみ、という状態です。

良かった点

Spring STSが便利

eclipseのpluginとしてSTSではなく、Spring STSの方です。

spring.io

これを使っておけばapplication.ymlのコード補完ができるようになり、Dashboardからpluginのインストールも簡単に行なえ、Boot Dashboardから目的のアプリの起動が自動登録され、すぐ起動できます。

とりあえずSTSを使っておけば、別途インストールするpluginをあまり必要ありません。

ちゃんとEclipse自体のバージョンアップに追従してくれているので、安心感があります。eclipseのpluginとしてのSTSより、Spring STSをダウンロードした方が恐らくほとんどの場合楽だと思われます。

application.ymlが便利

SaStruts時代はsystem.propertiesというファイルを用意して、それを設定ファイルとして使っていました。設定ファイルの値を環境毎に変える必要があったのですが、

system.properties
    ┗ system-xxx.properties

等と2段構成にし、system-xxx.propertiesはsystem.propertiesの設定値を上書きする仕組みで、system-xxx.propertiesはビルドする時に環境毎にファイルで上書きする、という事を頑張ってantで書いていました。この仕組で特定環境の場合だけメールの宛先を変える、といった事をしていました。

しかしSpring bootではもうそれは最初から考慮されていて、spring.profiles.activeの値(下記のdevelopment,staging,productionの部分)によって、以下が自動的に読み込まれます。ビルドして環境毎にファイルを上書き、といった事はもう必要ありません。

application.yml
    ┣ application-development.yml
    ┣ application-staging.yml
    ┗ application-production_.yml

挙動としては、application.ymlが読み込まれ、spring.profiles.activeで指定したymlが上書きで読み込まれる、という形になります。application-xxx.ymlは追加で上書き読み込みとなるため、上書きしたい設定値のみを記述する事になります。

今まで私が手動でやっていた事が、全自動でやってくれるので、非常に楽でした。一切開発することなく、今まで手動でやってくれた事が自動化されました。

更に、このapplication.ymlはコード補完が効く(しかも曖昧な中間一致検索!)ので、指定できる項目にどんなものがあるのか、等が簡単に解ります。

セッションレプリケーションが楽

例えばjdbcでレプリケーションしたい場合は、以下のように設定するだけで、起動時にDBにレプリケーション用テーブルが無ければ自動生成し、テーブルが有れば再利用します。MongoDBやRedisの設定も用意されています。

spring:
  session:
    store-type: jdbc

この設定で以下のテーブルが自動生成されます。

mysql> show create table spring_session \G
*************************** 1. row ***************************
Create Table: CREATE TABLE `spring_session` (
  `SESSION_ID` char(36) NOT NULL,
  `CREATION_TIME` bigint(20) NOT NULL,
  `LAST_ACCESS_TIME` bigint(20) NOT NULL,
  `MAX_INACTIVE_INTERVAL` int(11) NOT NULL,
  `PRINCIPAL_NAME` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`SESSION_ID`),
  KEY `SPRING_SESSION_IX1` (`LAST_ACCESS_TIME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

mysql> show create table spring_session_attributes \G
*************************** 1. row ***************************
Create Table: CREATE TABLE `spring_session_attributes` (
  `SESSION_ID` char(36) NOT NULL,
  `ATTRIBUTE_NAME` varchar(200) NOT NULL,
  `ATTRIBUTE_BYTES` blob,
  PRIMARY KEY (`SESSION_ID`,`ATTRIBUTE_NAME`),
  KEY `SPRING_SESSION_ATTRIBUTES_IX1` (`SESSION_ID`),
  CONSTRAINT `SPRING_SESSION_ATTRIBUTES_FK` FOREIGN KEY (`SESSION_ID`) REFERENCES `spring_session` (`SESSION_ID`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

今までtomcatのxmlでせっせと登録していた作業はもう必要ありません。

ロガー周りが楽

現在のSpring boot(v1.4.1)ではlogbackが標準になっており、logback.xmlではなく、logback-spring.xmlというSpring拡張ロガー設定ファイルを使うとロガーを便利に扱う事ができます。

ローカルであれば、最初から用意されている以下をincludeするだけで、コンソールにカラフルなログを表示する事ができます。ERRORの時は赤いフォント、WARNの時は黄色いフォントで表示されるので、視覚的に異常に気づく事ができます。

<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />


他にも、logback-spring.xmlには強力な機能が2点あります。1点目は「logback-spring.xmlからapplicaiton.ymlの値が参照できる」点です。

例えば

<springProperty name="springProfilesActive" scope="context" source="spring.profiles.active" defaultValue="unknown profile"/>

と定義すると、例えばSMTPAppendarのメール件名に、以下のように環境名を設定する事ができます。

<subject>${springProfilesActive}</subject>

今までこれをantのreplacetoken機能でファイルの変数値をリテラルに置換するような事をしている場合がありましたが、それももう不要です。application.ymlからどんな値も参照できます。


そして2点目は「if文が使えること」です。

よくある使い方としては、ローカル環境はログをファイル出力せず、コンソール出力のみとする。development,staging,production環境の場合はコンソール出力せず、ファイル出力+ERRORレベルをメール送信する、という環境毎の設定をします。今までは環境毎にlogback.xmlを用意し、ビルド時に環境に応じたファイルで上書きしていましたが、それももう不要です。以下のように書くと、application.ymlのspring.profiles.activeの設定値に応じて適用するappenderを分ける事ができます。

<springProfile name="local">
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
</springProfile>
<springProfile name="!local">
    <root level="INFO">
        <appender-ref ref="ROLLING-FILE" />
        <appender-ref ref="SMTP" />
    </root>
</springProfile>
Controllerが使いやすい

とにかく楽だと思いました。@GetMapping と書けばGetリクエストを受け付けられるし、@PostMapping と書けばPostになります。SaStrutsでこれはできませんでした。

MediaTypeをjsonにして自作クラスを返り値に設定するだけでjsonが返すこともできます。レスポンスのHTTPステータスコードも @ResponseStatus(HttpStatus.NOT_FOUND) 等と書くだけで指定できます。

他にも、メソッドの引数にHttpServletRequest reqを追加すると空気を読んでメソッドインジェクションしてくれたりします。「これ引数に入れたらインジェクションされるかな?」と思って試すと大抵インジェクションされてびっくりします。

@GetMapping(value = "/")
String top(HttpServletRequest req) {
    return "index";
}
actuatorのhealthcheckが便利

Spring内の様々な情報を参照できるactuatorですが、healthCheckは非常に便利です。

例えばMySQLとsolr(spring-boot-starter-data-solr使用)の依存を使用している場合、何もしなくても以下のチェックが行われます。ただactuatorのhealthCheckのURLにアクセスするだけです。

{
    status: "DOWN",
    diskSpace: {
        status: "UP",
        total: 1114478608384,
        free: 951398924288,
        threshold: 10485760
    },
    solr: {
        status: "DOWN",
        error: "org.apache.solr.client.solrj.SolrServerException: Server refused connection at: http://127.0.0.1:8983/solr"
    },
    db: {
        status: "UP",
        database: "MySQL",
        hello: 1
    }
}

ここに追加したいヘルスチェック項目があれば、HealthIndicatorをimplementsして@Componentを付けたクラスを用意すれば、↑に追加してくれます。

ここに表示されているdiskSpaceやdbのチェックのON・OFFもapplication.ymlで行う事ができます。さらに、特定の環境の場合は上記のように詳細なデータを表示し、production環境の場合はstatusしか表示しない(勿論内側ではsolrもdbもチェックさせる事が可能)、というsensitive設定もあります。AWSのELB・ALBや外部の監視ベンダにhealthCheckを公開する場合に使ったりします。

gzip圧縮転送が楽

apacheであれば、mod_deflateを使って静的コンテンツをgzip圧縮転送したりしますね。Spring bootにはgzip圧縮転送機能が内蔵されており、以下のように設定するだけで、指定したmime-typeの場合のみgzip圧縮してくれます。

server:
  compression:
    enabled: true
    mime-types:
      - text/javascript
      - text/css
      - application/javascript
      - application/css
Fully ececutable jarが楽

gradleの場合は、以下の設定をwebアプリケーションプロジェクトに追加、「gradle clean :プロジェクト名:bootRepackage」といった感じでビルドするとFully executable jarになります。

bootRepackage {
    executable = true
}

今までのexecutable jarは、「java -jar hoge.jar」と起動していましたが、fully executable jarの場合は違います。linuxのserviceとしてexecutable jarが実行可能になります。

具体的には、executable = trueにしてビルドすると、jarファイルの先頭に以下のようなbashスクリプトが追加されます。

#!/bin/bash
#
#    .   ____          _            __ _ _
#   /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
#  ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
#   \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
#    '  |____| .__|_| |_|_| |_\__, | / / / /
#   =========|_|==============|___/=/_/_/_/
#   :: Spring Boot Startup Script ::
#

### BEGIN INIT INFO
# Provides:          spring-boot-application
# Required-Start:    $remote_fs $syslog $network
# Required-Stop:     $remote_fs $syslog $network
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Spring Boot Application
# Description:       Spring Boot Application
# chkconfig:         2345 99 01
### END INIT INFO

[[ -n "$DEBUG" ]] && set -x

# Initialize variables that cannot be provided by a .conf file
WORKING_DIR="$(pwd)"
# shellcheck disable=SC2153
[[ -n "$JARFILE" ]] && jarfile="$JARFILE"
..... snip ....

これはlinuxの起動スクリプト(/etc/init.d/xxxx)なので、当然「chkconfig add xxx」してOS起動時に自動起動する事もできるし、「/etc/init.d/xxx start」や「service xxx start」でexecutable jarを操作する事ができるようになります。このfully executable jarを使う事で、jarのパスを全く気にすることなく起動する事ができるようになります。(実際は/etc/init.d配下にjarのシンボリックリンクを貼る事になります)

swaggerとの連携が楽

springfoxの依存を追加して@EnableSwagger2したクラスを用意すると、Spring bootとswaggerが連動します。

例えば以下のようなRestControllerのクラスを用意します。

@RestController
@RequestMapping(value = "/api")
@Api(tags = { "タグを記述" })
public class TestApiController {

	@ApiOperation(value = "ハローAPI", notes = "テストAPIです。")
	@GetMapping(value = "/hello", produces = { MediaType.JSON })
	public Hello hello(@RequestParam(value = "callback", required = false) String callback,
			@RequestParam(value = "name", required = false) String name) {
		String content = Optional.ofNullable(name).filter(v -> v != null && !v.trim().isEmpty())
				.map(v -> String.join(v, "Hello ", " !!!")).orElse("");
		return new Hello(System.currentTimeMillis(), content);
	}

	static class Hello {
		private final long id;
		private final String content;
		public Hello(long id, String content) { this.id = id; this.content = content; }
		public long getId() { return id; }
		public String getContent() { return content; }
	}
}

http://localhost:8080/swagger-ui.html にアクセスすると、以下のようなswaggerのAPI定義書ができます。

f:id:treeapps:20161018012842p:plain

Response Headersを見ると、ちゃっかりcontent-encodingがgzipになっています。(前述のgzip設定がswaggerにも反映されている模様)

設定ファイルと定数クラスの紐付けが楽

昔はsystem.propertiesに設定を書き、javaからPropertiesクラスを経由するような仕組みを自分で構築していました。

Spring bootではapplication.ymlにアプリケーション固有の設定値を書き、それを読み込むクラスを作るだけです。

例えばapplication.ymlに以下の設定を追加し、

applicationConfig:
  serviceName: オレオレサービス

以下のjavaクラスを作ると、↑の設定値を参照する事ができます。properties.load(inputStream); のようなコードは不要です。

@Component
@ConfigurationProperties(prefix = "applicationConfig")
@Getter
@Setter
public class ApplicationConfig {
    private String serviceName;
}

※ @Getter @Setterはlombokのアノテーションです。getterとsetterを自動生成しています。

参照する時は、以下のようにDIして使います。

@Autowired
private ApplicationConfig applicationConfig;
System.out.println(applicationConfig.getServiceName());

この例では単独のStringですが、以下のようにすると、Listにする事もできます。

applicationConfig:
  params:
    - aaa
    - bbb
    - ccc
@Component
@ConfigurationProperties(prefix = "applicationConfig")
@Getter
@Setter
public class ApplicationConfig {
    private List<String> params;
}
静的リソースのバージョニングが便利

application.ymlに spring.resources.chain.strategy.fixed.version という設定を追加すると、例えばversionに「12345」という値を設定し、spring.resources.chain.strategy.fixed.pathに適用させたいパスを複数書いておくと、thymeleafのth:xxxで指定した場合に以下のようにバージョンを差し込んでくれます。

<script type="application/javascript" src="/assets/12345/js/vendor.js"></script>

application.ymlには環境変数の埋め込みができるので、例えば以下のように指定しておいて、production環境で環境変数RESOURCE_VERSIONにgitのリビジョンやタイムスタンプ値を設定すると、ビルドした時だけ変わる静的リソースパスを生成してくれます。

version: ${RESOURCE_VERSION:12345}

よく以下のようにタイムスタンプを付ける例を見かけますが、ソースにいちいちそれを付けるのも鬱陶しいし、キャッシュが強力なブラウザはQueryStringを無視する事もあるので、このバージョニング機能はを使っておくと非常に楽に対応する事ができます。

<script type="application/javascript" src="/assets/js/vendor.js?timestamp=yyyyMMddHHmmss"></script>

画像・js・cssをキャッシュさせたい、でも通常のやり方ではデプロイ後にユーザにキャッシュされたコンテンツを見せてしまう、そんな時にこのバージョニング機能を使うといいかと思います。ローカル環境はキャッシュ全無効、develop〜production環境はキャッシュ全開設定にしておき、ビルドした時だけバージョン部分が書き換わる、という事が可能になります。

このバージョニング機能ですが、内部的には「/assets/12345/js/vendor.js -> /assets/js/vendor.js」というリライト処理がされており、どちらのURLでもアクセス可能になっています。

悪かった点

続いて悪い点です。良い点ばかりではないのです。

application.ymlで警告が出てしまうプロパティが有る

例えば「management」ですが、プロパティとしては存在しているのですが、application.ymlに記述してみると、黄色いマーカーで「Unknown property」と言われます。(勿論正常に動きます)

f:id:treeapps:20161018020701p:plain

どうやらapplication.propertiesとapplication.ymlで内部の実装が異なるようで、application.yml側が対応できておらず警告が出てしまう、という事があるようです。

Spring Batchが難しい

私だけかもしれませんが、Spring Batchはとても難しいと感じました。

ジョブ・ステップという段階を踏み、その中でread, process, write, という処理を行う(シンプルなtasklet機能もある)のがSpring Batchなのですが、バッチクラスの登録周りが非常に難しいと感じました。

当初は以下のような構造を予定していたのですが、恥ずかしながら設定の仕方が解りませんでした。

HogeBatch
    ┗ ジョブ1
    ┣ ステップ1
    ┗ ステップ2
FugaBatch
    ┗ ジョブ2
    ┣ ステップ1
    ┗ ステップ2

色々試してみても「複数のジョブは登録できない」「ジョブ2が動いてくれない」などの現象が起きたりし、リファレンスとチュートリアルを見ても最後までよく解りませんでした。

あまりにも思い通りに動いてくれなかったので、以下のようにコマンドライン引数で指定したバッチ名を実行する、という安直な方法を取りました。

@SpringBootApplication
@Slf4j
public class App {
 
    @Autowired
    private Environment env;
 
    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(App.class, args)) {
            App app = ctx.getBean(App.class);
            int code = app.run(args, ctx);
            System.exit(code);
        } catch (Throwable t) {
            System.exit(1);
        }
    }

    private int run(String[] args, final ConfigurableApplicationContext ctx) {
        String batchNameValue = env.getProperty("HogeBatch");
        Batch batch = Batch.of(batchNameValue);
        if (batch == null)
            throw new IllegalArgumentException();
        switch (batch) {
        case HogeBatch:
            return ctx.getBean(HogeBatch.class).execute(args);
        default:
            throw new IllegalArgumentException("Unknown batch");
        }
    }
}

しかしこれではローカル環境の場合、いちいち環境変数に実行したいバッチ名を指定するという手間があるため、以下のようにローカルで対象バッチを実行するクラスを用意しました。

public class LocalHogeBatch {

	public static void main(String[] args) {
		System.setProperty("batch.name", "HogeBatch");
		App.main(args);
	}
}

これで擬似的に環境変数「-Dbatch.name=HogeBatch」と指定したことにし、エンドポイントであるApp.javaを呼ぶ事で、ローカル環境専用のバッチクラスを作りました。これを作っておけばローカルのバッチの呼び出しも楽になります。production環境等の場合は、executable jar経由で実行するので「java -jar -Dbatch.name=HogeBatch xxx.jar」といった呼び出し方になります。

実は上記のようなオレオレバッチ形式ではなく純粋なSpring Batch(@EnableBatchProcessingを使ったやりかた)で動いたのですが、保守フェーズに入った時にとてもこれを維持していける気がしないと感じたので、諦めてオレオレバッチ形式にしました。

例外ハンドリング周りに注意

単にドキュメントを読み切れていなかったのですが、最初例外エラーのハンドリング方法が解りませんでした。

最終的に以下である事が解りました。SaStrutsの時と比較するとイメージが掴めると思うので比較してみます。

SaStrutsの場合 Spring bootの場合
通常のエラー struts-config.xmlにExceptionHandlerを定義 HandlerExceptionResolverを継承したクラスを定義
それ以外で起きたエラー web.xmlのerror-page要素を定義 EmbeddedServletContainerCustomizerを継承したクラスを定義

例えばHandlerExceptionResolverを継承すると、以下の情報が取得できるので、エラー時に相当色々な事ができます。

ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);

一方error-page要素の代わりとなるEmbeddedServletContainerCustomizerでは、以下の引数しかありません。

void customize(ConfigurableEmbeddedServletContainer container);

従って、error-page要素と同様、シンプルな設定しかできませんが、ここからコントローラが呼び出されるので、コントローラ側のメソッドにHttpServletRequestをメソッドインジェクションさせれば、requestから様々な情報を取得する事ができます。

@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
    container.addErrorPages(
        new ErrorPage(HttpStatus.NOT_FOUND, "/error/404"), // ← コントローラーを呼ぶ
        new ErrorPage(HttpStatus.UNAUTHORIZED, "/error/401")
    );
}

HandlerExceptionResolverはハンドリングできる範囲は限られており、Controller内で起きた例外や、html(Thymeleafのタグ)で起きた例外等しか捉える事ができないため、取りこぼしが発生します。その取りこぼしをEmbeddedServletContainerCustomizerでカバーする事になります。

EmbeddedServletContainerCustomizerは、例えば以下のようにbasic認証を有効にして、basic認証を求められた時にパスワードを間違えたりキャンセルした場合は、EmbeddedServletContainerCustomizerを設定しないとjspの生のエラー画面が表示されてしまいますが、設定しておくと、任意のコントローラーを呼び出し、任意の画面に遷移させる事ができます。

security:
  basic:
    enabled: true
  user:
    name: admin
    password: admin

以下のようなStackTraceが表示される場合は、大抵EmbeddedServletContainerCustomizerの定義忘れです。

2016-10-18 02:52:43.898 ERROR 7951 --- [nio-8080-exec-6] .C.[.[.[.[dispatcherServletRegistration] : Servlet.service() for servlet dispatcherServletRegistration threw exception

org.thymeleaf.exceptions.TemplateInputException: Error resolving template "error", template might not exist or might not be accessible by any of the configured Template Resolvers
	at org.thymeleaf.TemplateRepository.getTemplate(TemplateRepository.java:246)
	at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1104)
	at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1060)
.... snip ....
2016-10-18 02:52:43.898 ERROR 7951 --- [nio-8080-exec-6] o.a.c.c.C.[Tomcat].[localhost]           : Exception Processing ErrorPage[errorCode=0, location=/error]

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateInputException: Error resolving template "error", template might not exist or might not be accessible by any of the configured Template Resolvers
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982)
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
.... snip ....
Caused by: org.thymeleaf.exceptions.TemplateInputException: Error resolving template "error", template might not exist or might not be accessible by any of the configured Template Resolvers
	at org.thymeleaf.TemplateRepository.getTemplate(TemplateRepository.java:246)
	at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1104)
ログ設定まわりの混乱

application.ymlでアプリケーションが出力するログのファイル名やファイルパスやディレクトリを設定する事が可能ですが、これが嵌まりました。

ドキュメントをよく読めって話ですが、デフォルトの挙動に癖があるので注意です。

まず、logging.fileとlogging.pathは同時に指定する事ができません。同時に指定した場合は、logging.fileのみが適用される挙動になります。(ドキュメントに書いてある)

logging.pathですが、ファイルパスの事かな?と想像しますが、違います。実はディレクトリパスを表しています。「パス」というのは「/var/log/xxx.log」のようなファイルパスを想像してしまいますが、この場合は「/var/log」といったディレクトリのパスのことをlogging.pathと定義しています。非常に紛らわしいので注意です。

一方logging.fileですが、ファイル名の事かな?と想像しますが、これも違います。実はファイルパス、例えば「/var/log/hoge.log」のことを指しています。これはちょっと変数名から内容が想像できませんね。

まとめると、以下になります。

設定項目 内容 サンプル値
logging.path ディレクトリパス /var/log
logging.file ファイルパス /var/log/hoge.log

これだったら、「logging.dir.path」「loggign.file.path」とでも命名していれば迷わずに済みそうなのですけどね・・

domaとの連携に一手間必要


一見するとdoma-spring-boot-starterの依存追加だけで連携できそうですが、これでは足りません。

Domaの返却するDataSourceはそのままだとSpringの管理外になるため、実行時例外発生時にロールバックされなくなるとのこと。TransactionAwareDataSourceProxyで包むように修正。

http://qiita.com/nyasba/items/1e22c2401f3849f9071d

との事なので、以下のように一手間必要になります。

@Configuration
@EnableTransactionManagement
public class DomaConfig implements Config {
 
    private DataSource dataSource;
    private Dialect dialect;
    private SqlFileRepository sqlFileRepository;
 
    @Autowired
    public void setDataSource(DataSource dataSource) {
        this.dataSource = new TransactionAwareDataSourceProxy(dataSource);
    }
 
    @Autowired
    public void setDialect() { this.dialect = new MysqlDialect(); }
 
    @Autowired
    public void setSqlFileRepository(@Value("${spring.profiles.active}") String springProfilesActive) {
        if ("local".equals(springProfilesActive)) {
            this.sqlFileRepository = new NoCacheSqlFileRepository();
        } else {
            this.sqlFileRepository = new GreedyCacheSqlFileRepository();
        }
    }
 
    @Override
    public DataSource getDataSource() { return this.dataSource; }
    @Override
    public Dialect getDialect() { return this.dialect; }
    @Override
    public SqlFileRepository getSqlFileRepository() { return this.sqlFileRepository; }
}

↑に色々書きましたが、これは古い情報で、今はTransactionAwareDataSourceProxyを手動でラップする必要はありません。


とのことです!
具体的にはTransactionAwareDatasourceProxyは以下で設定されていると思われます。

public DomaConfigBuilder dataSource(DataSource dataSource) {
this.dataSource = new TransactionAwareDataSourceProxy(dataSource);
return this;
}

https://github.com/domaframework/doma-spring-boot/blob/master/doma-spring-boot-autoconfigure/src/main/java/org/seasar/doma/boot/autoconfigure/DomaConfigBuilder.java

更に、現在は以下のようにapplication.ymlで全て設定可能なので、DomaConfigクラスを作る必要はありません。
f:id:treeapps:20161019230953p:plain

mime-typeにjsonpが無い

これは凄く困ったのですが、Controllerに指定するMediaTypeクラスにはjsonp(application/javascript)の定義がありません。というか、デフォルトではjsonpレスポンスが返せない作りになっているようです。これは流石に困るので、MediaTypeを以下のように拡張する事で、jsonpのmime-typeに対応しました。

import java.nio.charset.Charset;
import org.springframework.http.MediaType;

public class CustomMediaType extends MediaType {
    private static final long serialVersionUID = 1L;
    public final static String JSON = "application/json;charset=UTF-8";
    public final static String JSONP = "application/javascript";
    static {
        APPLICATION_JSON = valueOf(JSON);
        APPLICATION_JSONP = valueOf(JSONP);
    }
    public final static MediaType APPLICATION_JSON;
    public final static MediaType APPLICATION_JSONP;
    public CustomMediaType(MediaType other, Charset charset) { super(other, charset); }
}

Controllerで以下のように使います。

@GetMapping(value = "/hello", produces = { CustomMediaType.JSON, CustomMediaType.JSONP })
public JsonModel hello() {
    .... snip ....

確かPlayもjsonpレスポンスを標準で返す事ができなかったと思いますが、APIサーバを作ろうとするとjsonpのレスポンスを返す場合が多く必須要件になりやすいので、標準機能として組み込んであると嬉しいですね。

総評

長々と書きましたが、Spring bootは実際に業務で実戦投入してみて、非常に素晴らしいフレームワークだと感じました。

あと、SpringプロダクトではなくSeasarプロダクトである、Domaを今回ORMとして使いましたが、


Domaは(個人的に)最強のORM


だと感じました。

S2JDBCも素晴らしいのですが、例えばMySQLでload dataを外部SQLで実行する事ができなかったのですが、domaでは実行する事ができます。これができるということは、CSVをload dataするような処理をjavaでトランザクションを効かせて記述する事ができる、という事に繋がります。これは非常にありがたい事です。今までload dataしたいがためにそこだけbashで書いていたのですが、それも不要になりました。

Entity等を自動生成するdoma-genもあるし、機能も豊富だし、(基本的に)外部SQLファイルを書かなくてはいけないので、慎重にSQLを書く傾向が強まります。(何故か流れるようなインターフェースだと適当なSQLを書くのに、外部SQLとなると慎重になる人が多い)

アプリケーションログに以下のように実行したファイルパスと、プレースホルダが展開された状態のSQLログが出力される点も見逃せません。

2016-10-18 20:13:09.637  INFO 8148 --- [http-nio-8080-exec-7] o.s.doma.jdbc.UtilLoggingJdbcLogger      : [DOMA2076] SQLログ : SQLファイル=[META-INF/hoge/dao/HogeDao/selectHoge.sql],
select * from hoge where id = '123'


 
Spring bootは単体でできる事が非常に多く、ポータビリティが非常に高いと感じています。業務では一応nginxでリバースプロキシしていますが、webサーバとしての機能をあまり使っておらず、かなりの部分をSpring bootだけで処理しています。極端な言い方をすると、jarだけで動的・静的な処理のほとんどを賄えるので、リバースプロキシやSSI等が不要ならwebサーバいらないかも?と考えてしまう程です。(実際は残念ながらそんな事は無いですが・・・)

静的ファイルのパフォーマンスについても、最近のtomcatはnginxやapacheに引けを取らない程の処理能力を誇っているので、速度的な問題は起きにくいと思われます。

ビルド面に関しても本当に楽で、今まで環境毎に設定ファイルを上書きしていた処理も不要だし、環境毎のlogback.xmlの上書き処理も不要だし、「gradle clean :batch: bootRepackage」とパッケージング処理くらいしかしなくなりました。(あくまで私の場合のお話です)

Springは次期メジャーバージョンでProject Reactorというリアクティブ全開なフレームワークに変貌するようで、今後も目が離せません。一方Play frameworkも次期メジャーバージョンアップでakka-httpベースになるなど、大変身するようです。 Play Framework Roadmap

両者とも激的に変わるかもしれないので、しっかりついていけるよう勉強しないといけませんね!

Spring bootやるならこの本は絶対読むべき!

以下の本は絶対約に立つので、読んでおいた方がいいと思います。他にもSpring or Spring boot本はいくつかありますが、個人的に本当に知っておいた方がいいと思った事が一番書いてあったのが以下でした。ページ数は724ページというボリューム満点で一気に知識を増やせます!!(ただし問題のSpring batchの項はありません。。)

Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発

Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発

なお、Spring batchに関しては以下の情報を頂きましたので、読んでみようと思います。Springを使う方は是非読んでおくといいのではないかと思います!