kagamihogeの日記

kagamihogeの日記です。

Spring RetryのREADME読んだ

https://github.com/spring-projects/spring-retry 初めて使ったんで記念にREADME読んだ。

spring-retry - README.md

本プロジェクトはSpringアプリケーションで宣言的なリトライ機能を提供します。Spring Batch, Spring Integration, Spring for Apache Hadoopや、それ以外でも、使用できます。

Quick Start

例:

@Configuration
@EnableRetry
public class Application {

    @Bean
    public Service service() {
        return new Service();
    }

}

@Service
class Service {
    @Retryable(RemoteAccessException.class)
    public void service() {
        // ... do something
    }
    @Recover
    public void recover(RemoteAccessException e) {
       // ... panic
    }
}

"service"メソッドを呼ぶ時にRemoteAccessExceptionで失敗する場合はリトライし(デフォルトは三回まで)、すべてのリトライが不成功に終わる場合は"recover"を呼びます。

@Retryableアノテーションの属性には様々なオプションがあり、リトライする例外のinclude, exclude指定、リトライ回数制限、バックオフポリシー、などがあります。

Building

Requires Java 1.7 and Maven 3.0.5 (or greater)

$ mvn install

Features and API

RetryTemplate

処理をロバストにして障害に繋がりにくくするには、もし試行回数を増やせば成功する可能性があるなら、失敗した操作を自動リトライすると良い場合があります。そういう類のエラーは本質的には一時的です。たとえば、webサービスRMIのリモート呼び出しの失敗は、ネットワーク障害やDBアップデートでのDeadLockLoserExceptionが原因になることがありますが、これらは短時間経過後に解消する場合があります。そうした操作のリトライを自動化するため、Spring RetryにはRetryOperationsがあります。RetryOperationsインタフェースは以下の通りです。

public interface RetryOperations {

    <T> T execute(RetryCallback<T> retryCallback) throws Exception;

    <T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback)
        throws Exception;

    <T> T execute(RetryCallback<T> retryCallback, RetryState retryState)
        throws Exception, ExhaustedRetryException;

    <T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback,
        RetryState retryState) throws Exception;

}

この基礎となるコールバックはシンプルなインターフェースで、ここにリトライするビジネスロジックを指定します。

public interface RetryCallback<T> {

    T doWithRetry(RetryContext context) throws Throwable;

}

コールバックを実行して失敗(Exceptionのスロー)すると、成功するか実装でアボートするまで、リトライします。

RetryOperationsインタフェースには各種のオーバーロードメソッドがあり、引数に、すべてのリトライが失敗する場合の様々なリカバリ、クライアントと実装で呼び出し中の情報を保存するリトライステータス(詳細後述)、を指定できます。

RetryOperationsの最もシンプルな汎用実装がRetryTemplateです。以下のように使います。

RetryTemplate template = new RetryTemplate();

TimeoutRetryPolicy policy = new TimeoutRetryPolicy();
policy.setTimeout(30000L);

template.setRetryPolicy(policy);

Foo result = template.execute(new RetryCallback<Foo>() {

    public Foo doWithRetry(RetryContext context) {
        // webサービスなど失敗する可能性のある何らかの処理
        return result;
    }

});

上の例ではwebサービスを実行してユーザに結果を返しています。もし呼び出しが失敗する場合はタイムアウトに達するまでリトライします。

RetryContext

RetryCallbackのメソッド引数はRetryContextです。基本的にはコールバックでこのコンテキストを使うことは少ないですが、場合によってはリトライの繰り返し中にデータを保存する領域として使用します。

RetryContextは、同一スレッド内で実行中のネストしたリトライがある場合、親コンテキストを持ちます。親コンテキストは呼び出し中に共有したいデータを格納するのに使用します。

RecoveryCallback

リトライがすべて失敗する場合にRetryOperationsは別のコールバックRecoveryCallbackに制御を渡せます。この機能を使うにはメソッドに一緒に2つのコールバックを渡します。

Foo foo = template.execute(new RetryCallback<Foo>() {
    public Foo doWithRetry(RetryContext context) {
        // ビジネスロジック
    },
  new RecoveryCallback<Foo>() {
    Foo recover(RetryContext context) throws Exception {
          // リカバリロジック
    }
});

テンプレートがアボートをする前にビジネスロジックが失敗する場合、リカバリコールバック経由で何らかの代替処理を行えます。

Stateless Retry

シンプルなケースではリトライは単にwhileループです。RetryTemplateは成功か失敗するまで試行し続けます。RetryContextはリトライかアボートするかを決定するための状態を持ちますが、この状態はスタック上にありグローバルに置く必要が無いので、これはステートレスリトライ(Stateless Retry)と呼びます。ステートレスとステートフルリトライの違いはRetryPolicyの実装にあります(RetryTemplateは両方とも出来ます)。ステートレスリトライでは、コールバックは失敗時のリトライと同一スレッドで実行します。

Stateful Retry

ある失敗がトランザクションリソースを無効にするケースの場合、何らかの考慮が必要です。これはシンプルなリモートコールでは無関係で、その理由は(基本的には)トランザクションリソースが無いためですが、Hibernateを使用しているなど、DB更新の場合には関係してきます。この場合には、失敗となった例外を即座に再スローすることだけが理にかなっており、これによって、トランザクションロールバックして新しいトランザクションを開始します。

こうした場合ではステートレスリトライでは不十分で、再スローとロールバックRetryOperations.execute()を終わる必要がありスタックにあったコンテキストを潜在的にロストするためです。このロストを回避するにはスタックではなくヒープ(少なくとも)を使うストレージ機能が必要です。Spring RetryにはRetryContextCacheがあり、これをRetryTemplateに追加します。RetryContextCacheのデフォルト実装はインメモリでMapを使います。メモリリーク回避のため、厳密に強制される最大容量は持ちますが、time to liveなど高度なキャッシュ機能はありません。必要に応じてそうした機能を持つMapの使用を考慮してください。また、クラスタでの複数プロセッサによる高度な環境においても何らかのクラスタキャッシュ機能を持つRetryContextCacheの実装を検討してください(とはいえ、クラスタ環境だとしても、これはやり過ぎかもしれませんが)。 RetryOperationsの責務の一つは、新規実行(基本的には新規トランザクションでラップ)に復帰する際、失敗した操作を認識することです。これを扱うには、Spring RetryではRetryStateを使います。これはRetryOperationsの特別なexecuteメソッドと組み合わせて動作します。

失敗した操作を認識する方法とは、複数のリトライに横断する状態を識別することです。状態の識別には、アイテムを識別するユニークキーを返す責務を持つRetryStateオブジェクトをユーザが返します。識別子はRetryContextCacheでキーとして使われます。

Warning: RetryStateが返すキーのObject.equals()Object.hashCode()は注意深く実装してください。アイテムの識別にはビジネスキーを使用するのが良いです。例としてJMSメッセージではメッセージIDを使用できます。

リトライが失敗する場合、(失敗する可能性が高い)RetryCallbackを呼ぶのではなく、失敗を処理する別の方法もあります。ステートレスの場合同様にRecoveryCallbackを使用する方法で、これはRetryOperationsのexecuteメソッドに渡します。

リトライするかどうかの決定は通常はRetryPolicyにデリゲートするので、リミットとタイムアウトに関する関心事はここに入れます(後述)。

Retry Policies

RetryTemplateの内部では、executeメソッドのリトライか失敗かの決定はRetryPolicyが判断し、また、RetryContextのファクトリでもあります。RetryTemplateの責務には、RetryContextを生成するポリシーを使うことと、毎回のリトライ時にRetryCallbackRetryContextを渡すこと、があります。コールバックが失敗すると、RetryTemplateRetryPolicyに(RetryContext内の)状態の更新を行うための呼び出しをする責務があり、それから、次回のリトライが実行可能かどうかをポリシーに問い合わせます。次回のリトライが実行不可能(例:リミットに達した、タイムアウトを超えた)な場合、ポリシーは失敗の状態を識別する責務はありますが、例外ハンドリングの背sキムはありません。RetryTemplateはオリジナルの例外をスローし、ステートフルの場合以外でリカバリが利用可能では無い場合はRetryExhaustedExceptionをスローします。また、コールバック(ユーザのコードのこと)から無条件にオリジナルの例外をスローするようなフラグをRetryTemplateに設定することも出来ます。

Tip: 失敗は本質的にはリトライ可能か不可能かのどちらかです。もしビジネスロジックがどんな場合でも同一の例外をスローする場合、リトライでその例外は使えません。かといってすべての例外型でリトライするのは止めて下さい。プログラマがリトライ可能と判断した例外でだけリトライしてください。ビジネスロジックを積極的にリトライするのは基本的には邪悪ではないですが、もし失敗が決定論的な場合、fatalが自明なリトライは時間を浪費するだけなので無駄です。

Spring Retryはシンプルで汎用のステートレスRetryPolicyの実装をいくつか提供しており、例えば上述の例で、SimpleRetryPolicyTimeoutRetryPolicyを使っています。

SimpleRetryPolicyは、指定の回数、例外型のリストに対してリトライします。

// 初回リトライを含む最大回数および
// すべての例外(デフォルト)に対するリトライを設定
SimpleRetryPolicy policy = new SimpleRetryPolicy(5, Collections.singletonMap(Exception.class, true));

// 上記のポリシーを適用
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(policy);
template.execute(new RetryCallback<Foo>() {
    public Foo doWithRetry(RetryContext context) {
        // ここにビジネスロジックを記述
    }
});

また、ExceptionClassifierRetryPolicyというより柔軟な実装があり、これはExceptionClassifierを使用して複数の例外型にそれぞれのリトライを設定できます。このポリシーはclassifierを呼び出して例外をRetryPolicyへのデリゲートに変換するので、たとえば、ある例外型を異なるポリシーにマッピングすることで、その例外型は他よりも多くリトライするようなことが可能です。

必要であれば自前のカスタムリトライポリシーを実装して下さい。たとえば、ソリューション固有で頻出なものがある場合、例外のclassificationをリトライ可能・不可能に割り当てます。

Backoff Policies

一時的な問題の発生後に再度リトライする場合、すこし待つのが良い場合が多く、基本的にはそうした問題の解決には待機だけが有効だからです。RetryCallbackが失敗するときに、RetryTemplateは設定されたBackoffPolicyに従って例外を待機させられます。

public interface BackoffPolicy {

    BackOffContext start(RetryContext context);

    void backOff(BackOffContext backOffContext)
        throws BackOffInterruptedException;

}

BackoffPolicyではbackOffを任意の方法で実装します。Spring Retryが提供するすべてのポリシーのデフォルト動作はObject.wait()を使います。backoffのよくある使い方は待機間隔を指数関数的に増加させるもので、これは二つのリトライがロックして両方とも失敗するのを避けるためです。これはイーサネットから得られた教訓です。Spring RetryはExponentialBackoffPolicyを用意しています。また、ランダム版のディレイポリシーもあり、これは複雑なシステムで関連性のある失敗が共鳴しあうのを避けたい場合に有用です。

Listeners

場合によっては、複数の異なるリトライに対する横断的関心事のためのコールバックを追加すると便利なケースがあります。Spring RetryにはRetryListenerがあります。RetryTemplateでRetryListenersを登録可能で、そのリトライ中で利用可能なRetryContextThrowableとコールバックがリスナに渡されます。

インタフェースは以下の通りです。

public interface RetryListener {

    void open(RetryContext context, RetryCallback<T> callback);

    void onError(RetryContext context, RetryCallback<T> callback, Throwable e);

    void close(RetryContext context, RetryCallback<T> callback, Throwable e);
}

最もシンプルなケースのリトライではopenとcloseコールバックはリトライ全体の前後に呼ばれてonErrorは毎回のRetryCallbackで呼ばれます。closeもThrowableを受け取ります。何らかのエラーがある場合はRetryCallbackが最後に投げたものになります。

注意点として、複数のリスナはリストに格納されるので順序を持ちます。この場合、openはその順序で呼ばれますが、onErrorとcloseは逆順で呼ばれます。

Declarative Retry

ある種のビジネスロジックが呼ばれるときはすべて毎回リトライさせたい場合があります。これの良くある例はリモートサービス呼び出しです。Spring RetryはAOPのインターセプタを提供しており、これはメソッド呼び出しをRetryOperationsでラップします。RetryOperationsInterceptorインターセプトされたメソッドを実行し、設定されているRepeatTemplateRetryPolicyに従って失敗時にリトライします。

Java Configuration for Retry Proxies

@Configuration@EnableRetryを追加して、リトライしたいメソッド(全メソッドに対しては型レベル)に@Retryableを付与します。また、複数のリトライリスナを指定できます。

@Configuration
@EnableRetry
public class Application {

    @Bean
    public Service service() {
        return new Service();
    }
    
    @Bean public RetryListener retryListener1() {
        return new RetryListener() {...}
    }
    
    @Bean public RetryListener retryListener2() {
        return new RetryListener() {...}
    }

}

@Service
class Service {
    @Retryable(RemoteAccessException.class)
    public service() {
        // ... do something
    }
}

@Retryableの属性でRetryPolicyBackoffPolicyが制御できます。

@Service
class Service {
    @Retryable(maxAttempts=12, backoff=@Backoff(delay=100, maxDelay=500))
    public service() {
        // ... do something
    }
}

100から500ミリ秒のランダムバックオフで12回まで試行します。また、stateful属性(デフォルトfalse)でリトライをステートフルにするかどうかを制御します。ステートフルにするにはインターセプトされるメソッドに引数を持たせる必要があり、それが状態のキャッシュキー構築に使われます。

また、@EnableRetryは、Sleeper型のbean・RetryTemplate内で使われるその他のストラテジーオブジェクト・実行時にリトライの振る舞いを制御するためのインターセプター、を参照します。

@EnableRetry@Retryable beansのプロキシを生成し、そのプロキシ(アプリケーションのbeanインスタンス)にはRetryableインタフェースを追加します。@EnableRetryは単にマーカーインタフェースですが、ある種のツールがリトライアドバイスを適用したい場合に役に立ちます(こうしたツールはbeanがRetryableを実装しているかどうかを基本的に考慮しない)。

リトライがすべて失敗する場合のエラー処理を設けたい場合はリカバリメソッドを作ります。@Retryableとそれに対する@Recoverのメソッドは同一クラスに宣言します。リカバリの戻り値型は@Retryableと一致させる必要があります。引数は任意でスローされる例外を含めることが可能で、また、同じく任意で大本のretryableのメソッドに渡される引数も含めることが可能です(引数の一部だけや引数無しも可能)。

@Service
class Service {
    @Retryable(RemoteAccessException.class)
    public void service(String str1, String str2) {
        // 何かしらの処理
    }
    @Recover
    public void recover(RemoteAccessException e, String str1, String str2) {
       // 必要に応じて大本の引数を使用してエラーハンドリング
    }
}

Version 1.2で特定プロパティの式を使用する機能を導入しました。

@Retryable(exceptionExpression="#{message.contains('this can be retried')}")
public void service1() {
  ...
}

@Retryable(exceptionExpression="#{message.contains('this can be retried')}")
public void service2() {
  ...
}

@Retryable(exceptionExpression="#{@exceptionChecker.shouldRetry(#root)}",
    maxAttemptsExpression = "#{@integerFiveBean}",
  backoff = @Backoff(delayExpression = "#{1}", maxDelayExpression = "#{5}", multiplierExpression = "#{1.1}"))
public void service3() {
  ...
}

いわゆるSpring SpEL式(#{...})を使用します。

式は#{${max.delay}}もしくは#{@exceptionChecker.${retry.method}(#root)}などのプロパティ・プレースホルダを持ちます。

  • exceptionExpressionはスローされる例外である#rootオブジェクトに対して評価が行われます。
  • maxAttemptsExpression@BackOffの式の属性は初期化中に一度だけ評価が行われます。こちらは評価用のルートオブジェクトはありませんが、コンテキスト内の他のbeanを参照できます。

XML Configuration

以下はSpring AOPを使用してremoteCallというサービスメソッドをリトライする設定の例です(AOPインターセプターの設定の詳細についてはSpring User Guideを参照してください)。

<aop:config>
    <aop:pointcut id="transactional"
        expression="execution(* com..*Service.remoteCall(..))" />
    <aop:advisor pointcut-ref="transactional"
        advice-ref="retryAdvice" order="-1"/>
</aop:config>

<bean id="retryAdvice"
    class="org.springframework.retry.interceptor.RetryOperationsInterceptor"/>

上記例はインターセプター付属のデフォルトのRetryTemplateを使用しています。ポリシーやリスナーを変更するには、インターセプターにRetryTemplateインスタンスをインジェクションする必要があります。

Contributing

Spring Retryはnon-restrictive Apache 2.0 licenseでリリースしており、一般的なGithub開発プロセスに従い、issueにはGithub trackerを使用してmasterにpull requestをマージしています。何か小さなことでもcontributeを望む場合遠慮は無用ですが、以下のガイドラインには従ってください。

non-trivialのパッチもしくはpull requestをアクセプトする前に、我々は https://cla.pivotal.io/*1へのサインを必要とします。contributor's agreementへのサインはメインリポジトリへのコミット権を意味しませんが、contributionsの受け入れ可能なことを意味し、無事通ればauthor creditを得られます。アクティブな開発者はコアチームへの参加を求められる場合があり、このときpull requestのマージ権が与えられます。

Code of Conduct

本プロジェクトにはContributor Covenantがあります。参加する場合、この規範を守ることを期待します。よからぬ行動に対しては spring-code-of-conduct@pivotal.io へ報告してください。

*1:リンクがあるが壊れている……

Spring BootでSpring Securityをredisでうごかす

Spring Securityのセッション保存先をメモリからredisに変更する。

やること

redisのインストール

他サイトを見て適当なマシンにredisをインストールしておく。

依存性の追加

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
    </parent>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <!-- spring-boot-starter-redisはdeprecated -->
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.session/spring-session-data-redis -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

上記pom.xmlのコメントにも書いたけどspring-boot-starter-redisはdeprecatedになっているので注意。

プロパティ設定

src/main/resources/application.yamlなどで設定変更する。設定内容はAppendix A. Common application propertiesを参照。必要に応じて、spring.security, spring.redis.*, spring.session.*あたりを修正する。

spring:
  security:
    user:
      password: hoge
  redis:
    host: 192.168.10.23

ここでは、spring-bootでspring-securityをデフォルトで動かすとuserというユーザが使えるのでそれのパスワードをテスト目的のために変更して、redisサーバのhostがデフォルトは127.0.0.1なのでこれを変更している。

エントリーポイント

起動用のmainをつくる。

package kagamihoge.springseurityredis;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

うごかす

spring-boot + spring-securityは特になんも設定しない状態だと http://localhost:8080 にアクセスするとデフォルトで以下のようなログイン画面になる。user/hogeでログインする。

f:id:kagamihoge:20180519162911j:plain

redis-cliで見るとなんかデータが入っている。

$ redis-cli 
127.0.0.1:6379> keys *
1) "spring:session:expirations:1526715420000"
2) "spring:session:sessions:f6fbfc66-2e0d-41ef-97c2-5ef66235eccf"
3) "spring:session:sessions:expires:f6fbfc66-2e0d-41ef-97c2-5ef66235eccf"
4) "spring:session:expirations:1526715480000"
5) "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user"
6) "spring:session:sessions:expires:657e59f9-caf9-4774-ae47-ff4c8eaa2752"
127.0.0.1:6379> 

JEP 326: Raw String Literalsをテキトーに訳した

http://openjdk.java.net/jeps/326

JEP 326: Raw String Literals

Owner    Jim Laskey
Created 2018/01/23 15:40
Updated 2018/04/03 18:29
Type    Feature
Status  Candidate
Component   specification / language
Scope   SE
Discussion  amber dash dev at openjdk dot java dot net
Effort  M
Duration    M
Priority    3
Reviewed by Alex Buckley
Endorsed by Brian Goetz
Release tbd_major
Issue   8196004

Summary

Java言語にraw string literalsを追加します。raw string literalは複数行のソースコードに展開可能で、\nや、\uXXXX形式のユニコードエスケープなど、エスケープシーケンスを解釈しません。

Goals

  • 以下により開発者の効率を高めます。
    • Javaのインジケータを使わない、読みやすい形式の文字列表現
    • supply strings targeted for grammars other than Java
    • 改行用の特別なインジケータを使わずに複数行のソースに展開できる文字列
  • raw string literalsは従来の文字列リテラルと同等な文字列を表現可能、ただしプラットフォーム固有の改行は除く。
  • エスケープの現行のjavac文字列リテラル処理および左マージンのトリムと同様なことをするライブラリ

Non-Goals

  • 新規のString演算子は導入しない。
  • Raw string literalsは文字列補間をすぐにはサポートしません。将来のJEPで導入する可能性はあります。
  • 従来の文字列リテラルには変更はありません。これには以下を含みます。
    • 開始・終了ダブルクオートの繰り返しによるデリミタのカスタマイズ
    • エスケープシーケンスの処理

Motivation

エスケープシーケンスはJavaを含め多くのプログラミング言語で定義されており、直接書くのが難しい文字を表現するのに使います。たとえば、エスケープシーケンス\nはASCIIの改行を意味します。二行で"hello"と"world"を表示するには、文字列"hello\nworld\n"を使います。

System.out.print("hello\nworld\n");

以下のような表示になります。

hello
world

可読性に難点がある他、この例はUNIXベースのシステムをターゲットにしていますが、それ以外のOSでは\r\n(Windows)など異なる改行表現を使用する場合があります。Javaには、printlnなど高レベルのメソッドがあり、これはプラットフォームに適切な改行文字を使用します。

System.out.println("hello");
System.out.println("world");

GUIライブラリで"hello"と"world" を表示する場合、制御文字は意味が無い場合があります。

エスケープシーケンス、バックスラッシュ、はJavaの文字列リテラルでは\\です。二重のバックスラッシュはLeaning Toothpick Syndrome*1を生み出し、過剰なバックスラッシュが文字列の理解を難しくさせます。Javaデベロッパは以下のような例を良く使います。

Path path = Paths.get("C:\\Program Files\\foo");

ダブルクオート文字を使うための\"などのエスケープシーケンスも同様に非Javaプログラマが見る場合に理解を難しくさせます。例えば、ダブルクオートを含む文字列を検索するには以下のようになります。

Pattern pattern = Pattern.compile("\\\"");

実際のところ、エスケープシーケンスは例外的事項でありJavaの日常的な開発に出てくるものではありません。制御文字を使う機会は少なく、エスケープの存在は可読性とメンテナンス性に悪影響を及ぼします。Once we come to this realization, the notion of a non-interpreted string literal becomes a well reasoned result.

現実のJavaコードには、他のプログラム(SQL, JSON, XML, 正規表現など)のコードを埋め込むことがあり、これらは、ユニコードエスケープ・バックスラッシュ・改行を除くと、リテラル文字列そのままでキャプチャされる仕組みを必要とします。

本JEPの提案は、raw string literal、という新しい種類のリテラルを提案します。Javaエスケープと行端仕様とは別途で、多くの状況下で既存の文字列リテラルよりも可読性とメンテナンス性に優れる文字列を提供します。

File Paths Example

Traditional String Literals

Runtime.getRuntime().exec("\"C:\\Program Files\\foo\" bar");

Raw String Literals

Runtime.getRuntime().exec(`"C:\Program Files\foo" bar`);

Multi-line Example

Traditional String Literals

String html = "<html>\n" +
              "    <body>\n" +
              "          <p>Hello World.</p>\n" +
              "    </body>\n" +
              "</html>\n";

Raw String Literals

String html = `<html>
                   <body>
                       <p>Hello World.</p>
                   </body>
               </html>
              `;

Regular Expression Example

Traditional String Literals

System.out.println("this".matches("\\w\\w\\w\\w"));

Raw String Literals

System.out.println("this".matches(`\w\w\w\w`));

Output:

true

Polyglot Example

Traditional String Literals

String script = "function hello() {\n" +
                "   print(\'\"Hello World\"\');\n" +
                "}\n" +
                "\n" +
                "hello();\n";
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval(script);

Raw String Literals

String script = `function hello() {
                    print('"Hello World"');
                 }
hello();
            `

ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval(script);

Output:

"Hello World"

Database Example

Traditional String Literals

String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" +
               "WHERE `CITY` = ‘INDIANAPOLIS'\n" +
               "ORDER BY `EMP_ID`, `LAST_NAME`;\n";

Raw String Literals

String query = ``
                 SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
                 WHERE `CITY` = ‘INDIANAPOLIS'
                 ORDER BY `EMP_ID`, `LAST_NAME`;
               ``;

Description

raw string literalはリテラルの新しい形式になります。

Literal:
  IntegerLiteral
  FloatingPointLiteral
  BooleanLiteral
  CharacterLiteral
  StringLiteral
  RawStringLiteral
  NullLiteral

RawStringLiteral:
  RawStringDelimiter RawInputCharacter {RawInputCharacter} RawStringDelimiter

RawStringDelimiter:
    ` {`}

raw string literalは1文字以上をバッククオート` (\u0060) (backquote, accent grave)のシーケンスで囲みます。raw string literalは1つ以上のバッククオートで開始し、同数のバッククオートで終了します。異なる個数のバッククオートは文字列の一部として扱います。

raw string literal内へのバッククオートの埋め込みは開始終了のバッククオートの数を増減させることで対応可能です。

raw string literalの文字は、CRとCRLFを除き、解釈を行いません。CR(\u000D)とCRLF(\u000D\u000A)は常にLF(\u000A)に変換されます。この変換はプラットフォーム間の振る舞いの驚きを最小にするためです。

バッククオートの開始があるが対応する終了が無い場合コンパイル時エラーになります。

Java言語仕様では従来の文字列リテラルには二種類のエスケープ、ユニコードエスケープとエスケープシーケンス、を規定しています。Raw string literalsはエスケープを解釈しません。つまり、エスケープ文字列はそのままになります。

\uxxxx形式のユニコードエスケープは、字句解析による解釈前に文字列入力の一部として処理されます。raw string literalの要求をサポートするために、字句解析がバッククオートの開始を見つけたらユニコードエスケープ処理を無効化し、終了したら再度有効化します。一貫性を保つため、ユニコードエスケープ\u0060は、バッククオートの代替としては使いません。

以下はraw string literalsの例です。

`"`                // a string containing " alone
``can`t``          // a string containing 'c', 'a', 'n', '`' and 't'
`This is a string` // a string containing 16 characters
`\n`               // a string containing '\' and 'n'
`\u2022`           // a string containing '\', 'u', '2', '0', '2' and '2'
`This is a
two-line string`   // a single string constant

classファイルでは、ある文字列定数がraw string literalか従来の文字列リテラル由来かどうかは記録しません。

従来の文字列リテラル同様、raw string literalは常にjava.lang.String型です。raw string literals由来の文字列も、従来の文字列リテラル由来の文字列と同様の扱いです。

Escapes

エスケープシーケンスを解釈しない複数行文字列を開発者が望む可能性は高いです。この要求に応えるため、Stringクラスにエスケープシーケンスを実行時に解釈するインスタンスメソッドを追加します。

public String unescape()

このメソッドは、JLSが定義しているエスケープシーケンス(3.3 Unicode Escapes, 3.10.6. Escape Sequences for Character and String Literals)と同一スペルの\を頭につけた文字を、そのエスケープシーケンスが表現する文字に変換します。

例(b0からb3はtrueになる)

boolean b0 = `\n`.equals("\\n");
boolean b1 = `\n`.unescape().equals("\n");
boolean b2 = `\n`.length == 2;
boolean b3 = `\n`.unescape().length == 1;

エスケープ変換を細かく制御できるメソッドを提供します。

また、エスケープ反転ツール用のメソッドもあります。以下のメソッドもStringに追加します。

public String escape()

このメソッドは、' 'より小さいすべての文字をUnicodeかcharacter escape sequencesに変換し、'~'より上の文字はUnicode escape sequencesに変換、", ', \エスケープシーケンスに変換します。

例(b0からb3はtrueになる)

boolean b0 = "\n".escape().equals(`\n`);
boolean b1 = ``.escape().equals(`\u2022`);
boolean b2 = "•".escape().equals(`\u2022`);
boolean b3 = !"•".escape().equals("\u2022");

Source Encoding

ソースファイルに非ASCII文字がある場合、javacコマンド(javac -encoding)で正しいエンコーディングを使う必要があります。もしくは、raw stringに適切なUnicodeエスケープをし、Unicodeエスケープを適切な非ASCIIに変換するライブラリを使用します。

Margin Management

複数行文字列の課題の一つに、左マージン(いわゆるヒアドキュメント)か、周囲のコードのインデントで文字列をフォーマットするか、があります。理想的には、文字列は周囲のコードとブレンドさせたいです。よって問題は、余分な左スペースの扱い方をどうするか、になります。

柔軟に対処出来るようにするため、raw string literalsのマージンはスキャンされます。余分な左スペースをトリムするメソッドをStringに追加する予定です。

Alternatives

Choice of Delimiters

従来の文字列リテラルとraw string literalは両方ともデリミタで文字シーケンスを囲みます。従来の文字列リテラルは開始・終了のデリミタにダブルクオート文字を使います。この対称性によりリテラルの読み込みとパースは簡単です。raw string literalも対称性のあるデリミタを用いますが、別の異なるデリミタを使う必要があり、これは文字シーケンス内にエスケープを付与しないダブルクオートを使う可能性があるためです。raw string literalのデリミタの選択には以下の考慮事項があります。

  • デリミタは短い文字列・マージン管理・一般的な可読性で効果的であるように大仰なものにならないこと。
  • 開始デリミタはその後にraw string literalのボディが続くことを明確に示すこと。
  • 終了デリミタは文字列にあまり出てこないものなこと。終了デリミタが文字列内に出現する場合、終了デリミタの埋め込みルールは明確かつシンプルなこと。埋め込みはエスケープ無しの実現が必須です。

デリミタには今のところ3つのLatin1文字、single-quote, double-quote, and backtick、としています。それ以外は明快さに影響を及ぼし、従来の文字列リテラルとの間に一貫性を欠くと考えています。

従来の文字列リテラルとraw string literalとに違いを出す必要があります。ダブルクオート以外の文字かカスタムフレーズである種の複合デリミタを作ることにより、raw string literalsでダブルクオートを使うことは出来ます。たとえば$"xyz"$abcd"xyz"abcdなどです。これら複合デリミタは基本的な要件は満たしますが、明快さに欠け、終了デリミタの埋め込みはシンプルではありません。Also, there is a temptation in the custom phrases case to assign semantic meaning to the phrase, heralding another industry similar to Java annotations.

連続クォート、"""xyz"""、について。これについては曖昧さを回避するのが面倒です。たとえば"" + x + ""は、従来文字列リテラル+変数+従来文字列リテラルの結合としても、" + x + "という7文字の raw string literalとしてでもパースが可能です。

バッククオートの利点は別の目的で使われない点です。連続クオートと空文字で発生する曖昧さを回避できます。Java言語仕様の用語における新しいデリミタになります。バッククオートは、シンプルな埋め込みルールを含め、デリミタの要求をすべて満たします。

デリミタの選択における別の考慮事項は将来の技術発展の可能性です。rawおよび従来の文字列リテラルの両方でシンプルなデリミタを使用するようなことが、将来の技術では出来るかもしれません。

このJEPではバッククオートを提案します。言語の現在のクオートとは異なりますが、同様の目的を果たします。

Multi-line Traditional String Literals

このオプションはraw string literalとは別モノですが、raw string literalsに加えて従来の文字列リテラルに複数行機能を持たせることも合理的かもしれません。この機能を有効化すると従来の文字列リテラルの複数行をエラーにするツールとテストに影響が出る可能性があります。

Other Languages

Javaは、raw stringsを言語レベルでサポートしない、数少ない現代的なプログラミング言語グループの1つに取り残されています。

以下の言語、C, C++, C#, Dart, Go, Groovy, Haskell, Java, JavaScript, Kotlin, Perl, PHP, Python, R, Ruby, Scala, Swift、はraw string literalsをサポートしており、デリミタとrawおよび複数行文字列の使用方法について調査しました。Unixツールのbash, grep, sedの文字列表現も調査しました。

複数行リテラル問題の解消には、従来の文字列リテラルのダブルクオートのボディでCRとLFを使えるようにJava仕様を変更する、という手法もあります。ただし、そういうダブルクオートの使い方ではエスケープの解釈が必要です。

異なる解釈の振る舞いを明示するには、異なるデリミタが必要です。Java以外の言語では様々なデリミタを採用しています。

Delimiters Language/Tool
"""...""" Groovy, Kotlin, Python, Scala, Swift
`...` Go, JavaScript
@"..." C#
R"..." Groovy (old style)
R"xxx(...)xxx" C/C++
%(...) Ruby
qq{...} Perl

Python, Kotlin, Groovy and Swiftはraw stringsにはダブルクオート3つを使います。これは既存の文字列リテラルとの連続性を反映したものです。

Go and JavaScriptはバッククオートです。文字列であまり使われない文字を選択しています。Markdownとの相性は良くないですが、大半の場合では問題ありません。

異色どころでは、C#@"..."などのメタタグは本JEPで提案するバッククオートと機能的に似ています。しかし、@Javaではアノテーションを示唆します。そうしたメタタグの使用法を持ち込むことは、将来的にそのメタタグの使用を制限します。

Heredoc

raw stringsのクオートの別案にヒアドキュメント*2があります。ヒアドキュメントはUnixシェルで最初に使われ始め、Perlなどに導入されました。ヒアドキュメントにはプレースホルダとエンドマーカがあります。プレールホルダは文字列をコード内に挿入する場所とエンドマーカを指定します。エンドマーカ―は文字列の最後につけます。

System.out.println(<<HTML);
<html>
    <body>
        <p>Hello World.</p>
    </body>
</html>
HTML

ヒアドキュメントはraw stringsの一つの案ではありますが、アナクロに思われます。さらにマージン管理の問題がやはり発生して複雑化します。

Testing

従来の文字列リテラルをraw stringリテラルで置き換えたテストのコピーを作成し、Stringのテストスイートを拡張します。

行端およびコンパイル単位の終了時におけるコーナーケースのテストをネガティブテストに追加します。

エスケープとマージン管理のメソッドのテストを追加します。

Risks and Assumptions

本JEPの仮定として、Markdown, Go, JavaScriptを含むraw string literalsにはバッククオートがあまり出現せず、そのため、バッククオートを繰り返すデリミタなら他よりも煩わしくない、としています。

*1:Leaning Toothpick、つまり斜めに傾けたつまようじが大量に並んでる様を揶揄する言葉

*2:"here" documents or heredocsが原文なんだけど「ヒアドキュメント」一つにまとまてしまった