自分のブログを振り返る

ブログを始めて1年半近くが経とうとしています。ブログを始めた当初は、3日坊主レベルであまり続かないんではないかと思っていましたが、我ながらそこそこいいペースで更新できているんではないかと思います。内容はたいしたことないけども…。
というわけで少し振り返ってみる。

きっかけ

仕事柄、普段から割と技術情報を中心にインプットすることはあるんですが、アウトプットはあまりできていませんでした。
何かしらの形でアウトプットする習慣を付けたいなと思ったのが、ブログを始めたきっかけです。あと文章を書く練習をするためでもありました。

気付いたこと

当初、書くネタが尽きて続かないんではないかと思っていましたが、意外とネタはいろんなところに転がっているものです。というか、仕事中にいろいろと技術ネタを収集することが多いので、備忘録として書き残しておきたいと思うケースがほとんどです。
あと、仕事中に調べたことや試行錯誤したことをあとからブログにまとめることによって、頭の中をきちんと整理できて理解を深めることができます。論理的に考えて情報を整理しないとなかなか言語化することはできないものです。

よかったこと

人間というのは過去にやったことがあってもしばらくすると忘れるものです。普段、備忘録としてブログを書くことが多いですが、あとになって自分のブログが役に立つことがよくあります。まさに備忘録。ありがとう、過去の自分。

まとめ

基本的に自分の備忘録ではあるけども、とりあえず今の更新ペースを維持しよう。あと、ブログ1本を書くスピードを上げたい。そのためには表現力とか語彙力を向上させる必要があるかもです。頑張ります。

HttpServletRequest#isRequestedSessionIdFromCookie は初回アクセスのときは false を返す

JSESSIONID とか Cookie とか URL リライティングに関連する備忘録。
Servlet API 3.x の話です。

レガシーな Web アプリのちょっとしたリプレース中なんですが、携帯サイトがなくなったり、サイト自体が SSL アクセスされるようになったりしたので、セッション維持に使ってた JSESSIONID の URL リライティング (JSESSIONID を URL に埋め込むやつ) をやめて、Cookie のセッションIDが使われるように設定を変更しました。

web.xml に以下の設定を追加。

<session-config>
  <tracking-mode>COOKIE</tracking-mode>
</session-config>

しかし、初回アクセスかつリダイレクトが絡むようなケースの場合、なぜかリダイレクト先の URL に jsessionid パラメータが付与される...。で、調べてみたところアプリの中にこんなコードがありました。(抜粋)

boolean useCookie = request.isRequestedSessionIdFromCookie();
if (!useCookie) {
    // jsessionid を URL にくっ付ける処理
    // もともとは携帯サイト向けの処理
}

意図的に URL に jsessionid を付与してました。ただ、今回は Cookie を使うように設定しているため、useCookie は true となって if 文のブロックは実行されないはず。

どうやら、isRequestedSessionIdFromCookie() は、web.xml ではなくリクエストに Cookie が含まれているかどうかで判定しているっぽい。なので、初回アクセスの場合には false を返すわけですね。(たぶん)

というわけで、このあたりの処理は削除して解決。

Bean Validation の initialize でアノテーションの属性値をインスタンス変数に保持する件

一応、調べてみます。ドキュメントはこちら。
http://docs.oracle.com/javaee/7/api/javax/validation/ConstraintValidator.html

This method can be accessed concurrently, thread-safety must be ensured by the implementation.

バリデータのインスタンスは同一のものが使い回されるようです。そのため、isValid はスレッドセーフに実装する必要があります。

で、アノテーションのパラメータを initialize の中でインスタンス変数に保持するコードは割と一般的ですが、アノテーションのパラメータに異なる値を渡して検証する場合、isValid の処理で不整合が発生しないんだろうか、ということを調べてみます。

こんなアノテーションを用意。param でパラメータを取れるようにしておきます。

gist.github.com

こんなバリデータを用意。initializeアノテーションのパラメータをインスタンス変数に保持して、isValid で利用することを想定します。バリデーションロジックは適当です。

gist.github.com

こんなテストコードを用意。Bean のフィールドに指定している @SampleValidation には同じパラメータを渡します。

gist.github.com

これを実行すると以下のように出力されます。同一のインスタンスが使われています。

@initialize : param=hoge, hashCode=1858609436
@isValid : param=hoge, hashCode=1858609436, value=bar
@isValid : param=hoge, hashCode=1858609436, value=foo

次に、Bean のフィールドに指定しているアノテーションに別々のパラメータを渡します。

@SampleValidation(param = "hoge")
private String name;
@SampleValidation(param = "fuga")
private String value;

で、先ほどのテストコードを実行すると以下のように出力されます。アノテーションごとに別々のインスタンスが使われるようになります。

@initialize : param=fuga, hashCode=1858609436
@isValid : param=fuga, hashCode=1858609436, value=bar
@initialize : param=hoge, hashCode=786041152
@isValid : param=hoge, hashCode=786041152, value=foo
まとめ

アノテーションに別々のパラメータを渡すと、バリデータはアノテーションごとに別々のインスタンスが使われる。なるほど。

JMockit の部分モックを試す

JMockit の部分モックを試してみる。このあたりの API はバージョンによって変更や削除となることがあるため要注意。ちなみに今回試したバージョンは以下。

gist.github.com

メソッドをモック化する場合は、Expectations の中でモック化したいメソッドを登録する。登録していないメソッドはオリジナルのメソッドが呼び出される。
Expectations の引数にクラスを指定する場合、以降で生成されるそのクラスのインスタンスがモック化される。Expectations の引数にインスタンスを指定する場合、そのインスタンスのみモック化される。

昔は、@Mock("method()") とか書けたようだが、今は使えない模様。

参考

JMockit - Tutorial - Mocking

Atom から Visual Studio Code に乗り換える

普段、Markdown エディタとして使っている Atom がどうにも重たい。起動が重たいのはしょうがないにしても普通に文字を入力したいときに固まったりするし。
そこで、Atom よりは軽量と噂の Visual Studio Code に乗り換えようかと。標準で Emmet が使えるし。

code.visualstudio.com

現在のバージョンは v1.9.1 の模様。とりあえずインストール

これまで Atom を使っていたからなのか、UI が日本語だと見た目にもっさり感がある。

  1. Ctrl + Shift + P で Command Palette を開く
  2. Configure Language を実行する
  3. locale.json{ "locale":"en-US" } を設定する
  4. 再起動

その他の設定。

  1. File > Preferences > Settings で settings.json を開く
  2. 上書きする設定を右のペインに記載する
{
  "editor.fontSize": 13,
  "editor.fontFamily": "MeiryoKe_Gothic",
  "editor.tabSize": 2,
  "editor.renderWhitespace": "boundary"
}

等幅メイリオについてはこちら
"editor.renderWhitespace":"boundary" : 単語間の半角スペースを表示しない
※プロキシ環境の場合は "http.proxy" を設定


(2017/02/28 追記)
Atom では任意のフォルダを Project Folder として登録することができる。
で、普段利用するフォルダをいくつか登録しておいて、フォルダツリーから開きたいファイルを選択するような感じで使っていた。

Visual Studio Code にも EXPLORER でフォルダツリーを表示できるが、Atom のように任意のフォルダを複数登録することはできない模様。
というわけで、あるフォルダのファイルを手早く開きたいときは Ctrl + o でファイルを開くようにしている。

(2017/04/17 追記)
Visual Studio Code で JSON をフォーマットするショートカットは Alt+Shift+F だが、minify ができない。というわけで、以下の拡張機能をインストールする。

JSON Tools - Visual Studio Marketplace

  • pretty ⇒ Alt+M
  • minify ⇒ Shift+Alt+M

java.io.InputStream を複数のファイル読み込みで共有しない

java.util.zip.ZipOutputStream による ZIP アーカイブについて調べてたら、以下のようなコードを見かけました。(適当に抜粋してます)

gist.github.com

単純に、ディレクトリ配下のファイルを commons-io で取得して、ZIP にアーカイブするコードです。これ自体は正しく動作するし、目的通りに ZIP ファイルが生成されます。
しかし、呼び出し元が以下のような感じだったら。

archive(dirPath);
FileUtils.deleteDirectory(new File(dirPath));

要するに、ZIP ファイルを生成したあと、もとのディレクトリを削除するような場合。このとき、FileUtils#deleteDirectory でエラーが発生します。

java.io.IOException: Unable to delete file: ...
        at org.apache.commons.io.FileUtils.forceDelete(FileUtils.java:2192)
        at org.apache.commons.io.FileUtils.cleanDirectory(FileUtils.java:1585)
        at org.apache.commons.io.FileUtils.deleteDirectory(FileUtils.java:1467)
        at org.apache.commons.io.FileUtils.forceDelete(FileUtils.java:2183)
        at org.apache.commons.io.FileUtils.cleanDirectory(FileUtils.java:1585)
        at org.apache.commons.io.FileUtils.deleteDirectory(FileUtils.java:1467)
        at org.apache.commons.io.FileUtils.forceDelete(FileUtils.java:2183)
        at org.apache.commons.io.FileUtils.cleanDirectory(FileUtils.java:1585)
        at org.apache.commons.io.FileUtils.deleteDirectory(FileUtils.java:1467)
        at org.apache.commons.io.FileUtils.forceDelete(FileUtils.java:2183)
        at org.apache.commons.io.FileUtils.cleanDirectory(FileUtils.java:1585)
        at org.apache.commons.io.FileUtils.deleteDirectory(FileUtils.java:1467)
        ...

冒頭のコードだと、writeZipEntry メソッドに File オブジェクトの配列を渡して、同じ InputStream の参照でそれぞれのファイルを読み込んでいます。最後に finally で InputStream を close していますが、最後に処理したファイルのストリームしか close されていないのではないかと推測しています。そのため、後続のディレクトリの削除処理でエラーが発生しているのではないか、というわけです。(自信はない…)

冒頭のコードを以下のように変更すると、先のエラーは発生しなくなります。
要するに、処理するファイルごとに writeZipEntry メソッドを呼び出して、毎回 InputStream を close するようにします。

gist.github.com

java.io.InputStream は複数のファイル読み込みで共有せず、ファイル読み込みごとに close すること。

余談

ちなみに冒頭のコードですが、writeZipEntry メソッドの最後で System#gc を呼び出すとエラーが発生しなくなります。うーむ…。

Spring Boot + Spring Batch + Java 6 でバッチを実装した話

今回、Spring Boot + Spring Batch + Java 6 でバッチを実装する機会があったので、もろもろを備忘録としてまとめる。

開発環境

Spring Tool Suite v3.8.3 を使用。Spring Starter Project からプロジェクトを作成する。

  • Java Version 1.6
  • Spring Boot 1.4.3
  • I/O > Batch

Dependencies で Batch をチェックしてプロジェクトを作成すると、spring-boot-starter-batch の依存が追加される。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-batch</artifactId>
</dependency>

プロジェクトを作成して、Run Spring Boot App すると以下のエラーが出る。

... nested exception is java.lang.UnsupportedClassVersionError: org/apache/tomcat/jdbc/pool/DataSource : Unsupported major.minor version 51.0

Tomcat JDBCJava 6 に対応していない模様。今回、Tomcat やデータベースを使う予定がなかったので、思い切って Tomcat JDBC を依存から除外する。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-batch</artifactId>
  <exclusions>
    <exclusion>
      <groupId>org.apache.tomcat</groupId>
      <artifactId>tomcat-jdbc</artifactId>
    </exclusion>
  </exclusions>
</dependency>

とりあえず、Java 6 で Spring Boot + Spring Batch が動作することを確認。

Tasklet

今回は割とシンプルなアプリケーションだったので、Tasklet のみで構成。main メソッドからの流れは基本的に本家のクイックスタートの通り。> Spring Batch

プロパティファイル

今回、src/main/resources 下の application.properties の他に、エンドユーザが設定するプロパティファイルとして application-env.properties を用意しました。(アプリ内部で利用するプロパティとエンドユーザが設定するプロパティをファイルとして分割しようと思って)

application-env.properties は config ディレクトリに配置して、jar を実行するときに -Dspring.config.location でプロパティファイルのパスを指定すれば読み込める模様。尚、Linux 環境では --spring.config.location なので注意。これで30分くらいハマった…。

java -jar batch.jar -Dspring.config.location=config/application-env.properties

-Dspring.config.location で外部のプロパティファイルを指定したとき、クラスパス上の application.properties は読み込まれるのか不安でしたが、普通に読み込めました。このあたりは別エントリにまとめました。

Spring Boot で複数の @ConfigurationProperties のプロパティを読み込む - kntmr-blog

プロパティは @ConfigurationProperties を付けた Bean 経由で読み込む。@ConfigurationProperties は、カンマ区切りのプロパティを自動的に List<T> に格納してくれたり、Bean Validation と組み合わせてプロパティをチェックできるので便利。
以下の依存を追加。(@NotBlank とかを使おうとすると、STS が追加してくれる)

<dependency>
  <groupId>javax.validation</groupId>
  <artifactId>validation-api</artifactId>
  <version>1.1.0.Final</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

@NotNulljavax.validation.constraints パッケージで提供されているアノテーション@NotBlank, @NotEmptyHibernate Validator が提供するアノテーション。それぞれのおおまかな違いは以下を参考にさせていただきました。

@NotNull/@NotEmpty/@NotBlankの違い - 見習いプログラミング日記

Hibernate Validator のバリデーションメッセージは、デフォルトでは「may not be empty」のような英語メッセージとなる。今回、バリデーションメッセージを日本語化するために ValidationMessages.properties を追加したが、ログに出力されるメッセージが文字化ける…。アノテーションの message 属性に直接日本語を書くと問題ないんだけど。
で、ソースコードに直接メッセージを書きたくなかったのと、「may not be empty」でもまぁ問題ないかなということで、バリデーションメッセージの日本語化は諦めることに。

Logback

ロギングは Logback を使う。Logbackを使う場合は src/main/resources 配下に logback-spring.xml を配置する。今回は、コンソールとファイルに同じ内容を出力して、ファイルサイズでローテートするようにしている。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE logback>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml" />
    <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
    <!--<include resource="org/springframework/boot/logging/logback/file-appender.xml" />-->
    
    <property name="LOG_FILE" value="../logs/batch.log" />
    
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_FILE}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <fileNamePattern>${LOG_FILE}.%i</fileNamePattern>
            <minIndex>1</minIndex>
            <maxIndex>5</maxIndex>
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <maxFileSize>100KB</maxFileSize>
        </triggeringPolicy>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>
</configuration>

メッセージプロパティ

メッセージプロパティを使う場合は、src/main/resources の application.properties に以下の設定を追加する。

spring.messages.basename=messages
spring.messages.cache-seconds=-1
spring.messages.encoding=UTF-8

ソースコードからは MessageSource クラス経由で messages.properties のメッセージを取得する。

// MessageSource クラスを DI
@Autowired
private MessageSource messageSource;

// メッセージを取得
messageSource.getMessage(code, args, Locale.JAPAN);

Apache Commons IO

今回、あるディレクトリ配下のファイルを読み込んでごにょごにょする、みたいな要件がありました。Java 7 以降であれば、Files クラスとか Paths クラスとか何かと便利な API が使えていいんですが、今回は如何せん Java 6 …。というわけで、Apache Commons IO 2.3 を使う。

<dependency>
  <groupId>commons-io</groupId>
  <artifactId>commons-io</artifactId>
  <version>2.3</version>
</dependency>

あと、やっぱり try-with-resources が使えないのが地味にツライ。

パッケージング

パッケージングは mvn package するだけ。

ステータスコード

今回、Spring Batch で作成した jar をシェルスクリプトからキックして、RETVAL=$?ステータスコードを取得する想定でした。

当初、StepContribution#setExitStatus メソッドに ExitStatus.COMPLETED or ExitStatus.FAILED を渡して正常終了か異常終了を判別してみたものの、異常終了の場合でもステータスコードが 0 になる…。

// 正常終了
contribution.setExitStatus(ExitStatus.COMPLETED);
// 異常終了
contribution.setExitStatus(ExitStatus.FAILED);

というわけで、ChunkContext クラスから StepExecution を辿って、StepExecution#setStatus メソッドに BatchStatus.COMPLETED or BatchStatus.FAILED を渡すことで正常終了か異常終了を判別することができた。
ちなみに BatchStatus.FAILED を渡すと、ステータスコードに 5 が返る。

// 正常終了
chunkContext.getStepContext().getStepExecution().setStatus(BatchStatus.COMPLETED);
// 異常終了
chunkContext.getStepContext().getStepExecution().setStatus(BatchStatus.FAILED);

ちなみに本家のクイックスタートにも記載されているが、ステータスコードを返すときは main メソッドで以下のようにして System#exit を呼び出す。

public static void main(String[] args) {
    System.exit(SpringApplication.exit(SpringApplication.run(FooBatchApplication.class, args)));
}