54
64

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Spring 4.3 Web関連の主な変更点

Last updated at Posted at 2016-05-29

今回は、Spring Framework 4.3の変更点紹介シリーズの第5回で、Web関連の変更点を紹介します。
(リリース前の投稿を更新しました!! 差分は、「★6/11追加」でマークしてあります)

シリーズ

動作検証環境

  • Spring Framework 4.3.0.RELEASE
  • Spring Boot 1.4.0.BUILD-SNAPSHOT (2016/6/11時点)

Web Improvements

今回は、Web関連の主な変更点をみていきます。

No Web関連の主な変更点
1 HEADとOPTIONSに対するリクエストハンドラーをSpring MVCが暗黙的に用意してくれるため、これらのHTTPメソッドをハンドリングするための実装が不要になります。
2 リクエストマッピング用のアノテーションとして、@RequestMappingの合成アノテーション(@GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping)が追加されます。
3 Beanのスコープ(Web専用のスコープ)を指定するアノテーションとして、@Scopeの合成アノテーション(@RequestScope, @SessionScope, @ApplicationScope)が追加されます。
4 RESTful Web Services(REST API)向けのControllerAdviceクラスの作成をサポートするアノテーションとして、@RestControllerAdvice(@ControllerAdvice@ResponseBodyを合成したアノテーション)が追加されます。
5 @ResponseStatusをクラスレベルに付与することができるようになります。
6 HttpSessionで管理しているオブジェクトにアクセスするためのアノテーションとして、@SessionAttributeが追加されます。
7 HttpServletRequestで管理しているオブジェクトにアクセスするためのアノテーションとして、@RequestAttributeが追加されます。
8 Modelから取得したオブジェクトに対して、リクエストパラメータのバインディング有無を制御できるようになります。
9 Spring MVCの例外ハンドラ(HandlerExceptionResolver)でErrorや自作のThrowableをハンドリングできるようになります。 (★6/11 追加)
10 マルチパートのパート部分をコンバートするHttpMessageConverterのデフォルトエンコーディングがUTF-8になります。 (★6/11 追加)
11 静的リソースのContent-Type(Media Type)を決定する際に、ContentNegotiationManagerの設定を利用できるようになります。
12 RestTemplateAsyncRestTemplateにて、DefaultUriTemplateHandlerを介してURI変数値に厳格なURLエンコーディングを適用するか否かを指定できるようになります。
13 AsyncRestTemplateにインタセプター(RestTemplateでいうところのClientHttpRequestInterceptor相当)の仕組みが追加されます。

HEADとOPTIONSに対するリクエストハンドラーが暗黙的に用意される :thumbsup:

Spring 4.3から、HEADとOPTIONSのHandlerメソッドを実装する必要がなくなります!!
HTTPの仕様に準拠したWebアプリケーション(主にRESTful Web Services)を作成する場合は、GETメソッドを提供する際にはHEADも提供する必要があります。また、指定したリソースがサポートしている操作(HTTPメソッド)をOPTIONSメソッド経由で知ることができるようにしておく必要があります。(とはいえ、実際にHEADやOPTIONSメソッドを使うのか???という話はここでは触れないことにします :sweat_smile:

HEADメソッド

Spring 4.3からは、GETメソッド用のHandlerメソッドを実装すると、暗黙的にHEADメソッドも実装したことになります。

@RequestMapping("/todos")
@RestController
public class TodoRestController {

    private final ConcurrentMap<String, Resource> resources = new ConcurrentHashMap<>();

    @RequestMapping(method = RequestMethod.GET) // GETメソッドのみマッピング
    List<Resource> getAll() {
        return resources.values().stream()
                .sorted((a, b) -> a.createdAt.compareTo(b.createdAt))
                .collect(Collectors.toList());

    }
    // ...
}

起動直後にGETメソッドでアクセスすると、空のリスト(JSON)が返却されます。

$ curl -D - http://localhost:8080/todos
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 29 May 2016 14:32:33 GMT

[]

HEADメソッドでアクセスすると、リソースのメタ情報(Content-Typeなど)のみ返却されます :blush:

$ curl -I http://localhost:8080/todos
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/json;charset=UTF-8
Content-Length: 2
Date: Sun, 29 May 2016 14:33:36 GMT

Spring 4.2で同じ実装をすると・・・405 Method Not Allowedになってしまいます :disappointed:

HTTP/1.1 405 Method Not Allowed
Server: Apache-Coyote/1.1
Allow: GET
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 29 May 2016 14:37:42 GMT

Spring 4.2で4.3と同じ挙動にするためには、HEADメソッドを明示的にHandlerメソッドにマッピングする必要があります。

@RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD}) // HEADも加える
List<Resource> getAll() {
    // ...
}

OPTIONSメソッド

Spring 4.3からは、暗黙的にOPTIONSメソッドを実装したことになります。
OPTIONSメソッドでアクセスすると、明示的に実装したGETとSpringが暗黙実装してくれるHEADの2つがAllowヘッダに設定されます :blush:

$ curl -D - -X OPTIONS http://localhost:8080/todos
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Allow: GET,HEAD
Content-Length: 0
Date: Sun, 29 May 2016 14:46:12 GMT

Spring 4.2だと・・・DispatcherServletが扱えるメソッドが全てAllowヘッダに設定されてしまいます。このレスポンスだと、HTTPの仕様に準拠しているとはいえません・・・ :disappointed:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
Content-Length: 0
Date: Sun, 29 May 2016 14:52:10 GMT

Spring 4.2でHTTPの仕様に準拠するためには、DispatcherServletのオプションを変更(dispatchOptionsRequestプロパティをtrueに)した上で、OPTIONSメソッドをハンドリングするためのHandlerメソッドを明示的に実装する必要があります。ここではSpring Boot上での変更方法を紹介します。

src/main/resources/application.properties
spring.mvc.dispatch-options-request=true
@RequestMapping(method = RequestMethod.OPTIONS) // OPTIONSを明示的にハンドリングする
HttpHeaders options() {
    HttpHeaders headers = new HttpHeaders();
    Set<HttpMethod> allows = new LinkedHashSet<>();
    allows.add(HttpMethod.GET);
    allows.add(HttpMethod.HEAD);
    headers.setAllow(allows);
    return headers;
}

@RequestMappingの合成アノテーションが追加される :thumbsup:

Spring 4.3から、リクエストマッピング用のアノテーションとして、@RequestMappingの合成アノテーション(@GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping)が追加されます。ここでは、GETメソッドのマッピング例を紹介します。

〜4.2のマッピング例
@RequestMapping(method = RequestMethod.GET)
List<Resource> getAll() {
    return resources.values().stream()
            .sorted((a, b) -> a.createdAt.compareTo(b.createdAt))
            .collect(Collectors.toList());
}
4.3〜のマッピング例
@GetMapping // 合成アノテーションを利用
List<Resource> getAll() {
    return resources.values().stream()
            .sorted((a, b) -> a.createdAt.compareTo(b.createdAt))
            .collect(Collectors.toList());
}

イメージ的には、JAX-RSの@GET, @POSTと同じような切り口ですね。だた、SpringはHTTPメソッド以外の属性(リクエストパラメータ、リクエストヘッダなど)を使ってマッピングルールを表現できるので、JAX-RSのアノテーションとは別物です。

Web専用のBeanスコープを指定する合成アノテーションが追加される :thumbsup:

Spring 4.3から、Web環境専用のBeanスコープ(request, session, application)であることを指定する合成アノテーションが追加されます。ここでは、session(セッションスコープ)のアノテーションの使用例を紹介します。

〜4.2の指定例
@Scope(WebApplicationContext.SCOPE_SESSION)
@Component
public class Cart implements Serializable {
    private static final long serialVersionUID = 9202491228533102493L;
    // ...
}
4.3〜の指定例
@SessionScope // 合成アノテーションを利用
@Component
public class Cart implements Serializable {
    private static final long serialVersionUID = 9202491228533102493L;
    // ...
}

@RestControllerAdviceが追加される :thumbsup:

Spring 4.3から、RESTful Web Services(REST API)向けのControllerAdviceクラスの作成をサポートするアノテーションとして、@RestControllerAdviceが追加されます。@RestControllerAdviceは、@ControllerAdvice@ResponseBodyを合成したアノテーションで、基本的には@ControllerAdviceと同じ機能を提供しており、@ExceptionHandlerメソッドで@ResponseBodyの指定を省くことができます。

〜4.2の実装例
@ControllerAdvice
public class ApiGlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(ApiGlobalExceptionHandler.class);

    @ExceptionHandler
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody // 明示的に指定する必要がある
    ApiError handleException(Exception e) {
        logger.error("System error occurred.", e);
        ApiError error = new ApiError();
        error.setCode("SYSTEM_ERROR");
        return error;
    }

}
4.3〜の実装例
@RestControllerAdvice // @ControllerAdviceの代わりに@RestControllerAdviceを指定
public class ApiGlobalExceptionHandler {

    @ExceptionHandler
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    // @ResponseBody 省略できる
    ApiError handleException(Exception e) {
        // ...
    }

}

@ResponseStatusをクラスレベルに付与できる :thumbsup:

@ResponseStatusをクラスレベルに付与することで、応答するレスポンスコード(デフォルトのレスポンスコード)を各メソッドで共有することができます。ControllerクラスとControllerAdviceクラスで利用できますが、@ExceptionHandlerを集約したControllerAdviceクラスで利用するのが有効的だと思います。

以下は、@ExceptionHandlerメソッドで@ResponseStatusの指定を省略した時のデフォルト値を「500(Internal Server Error)」にしています。なお、Spring MVCのデフォルトは「200(OK)」です。例外が発生しているのに「200(OK)」というのは適切ではないので、デフォルト値を「500(Internal Server Error)」にしておき、必要に応じて各メソッドで上書きするのがよいでしょう。

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // デフォルトを「500(Internal Server Error)」に設定する
@RestControllerAdvice
public class ApiGlobalExceptionHandler {

    @ExceptionHandler
    ApiError handleException(Exception e) {
        // ...
    }

}

@SessionAttributeを使用してセッションスコープのオブジェクトにアクセスできる :thumbsup:

HttpSessionで管理しているオブジェクトにアクセスしたい場合は、HttpSessionのAPIを直接使わずに@SessionAttributeを使うことができます。

〜4.2の実装例
@RequestMapping("/users")
@Controller
public class UserController {
    // ...
    @PostMapping(path = "create")
    String create(@Validated Form form, BindingResult result,
                  HttpSession session) {
        ClientInfo clientInfo = ClientInfo.class.cast(session.getAttribute("clientInfo")); // 引数で受け取ったHttpSessionのメソッドを直接利用する
        // ...
        return "redirect:/users/create?complete";
    }
    // ...
}

Spring 4.2までは、Servlet APIに依存する実装になってしまいます。HandlerMethodArgumentResolverの実装クラスを作成すればServlet APIに依存しない実装にすることもできますが、やや大げさな作りになってしまいます。Spring 4.3だと、上記コードを以下のように書き換えることができます。

4.3〜の実装例
@RequestMapping("/users")
@Controller
public class UserController {
    // ...
    @PostMapping(path = "create")
    String create(@Validated Form form, BindingResult result,
                  @SessionAttribute ClientInfo clientInfo) { // 引数として直接受け取れる
        // ...
        return "redirect:/users/create?complete";
    }
    // ...
}

デフォルトの動作では、HttpSessionに指定したオブジェクトが格納されていない場合は、ServletRequestBindingExceptionが発生して400(Bad Request)になります。セッションに該当するオブジェクトが格納されていないことがアプリケーションの作り上あり得る場合は、required属性をfalseにするか、java.util.Optionalを利用してください。

required=falseの指定例
@PostMapping(path = "create")
String create(@Validated Form form, BindingResult result,
              @SessionAttribute(required = false) ClientInfo clientInfo) { // required = falseを指定
    // ...
    return "redirect:/users/create?complete";
}
java.util.Optionalの利用例
@PostMapping(path = "create")
String create(@Validated Form form, BindingResult result,
              @SessionAttribute Optional<ClientInfo> clientInfo) { // Optionalとして受け取る
    // ...
    return "redirect:/users/create?complete";
}

なお、HttpSessionからオブジェクトを取得する際の属性名は、デフォルトでは引数の名前になりますが、これはデバッグ情報(JDKの「-g」オプション)またはパラメータのメタ情報(JDK 8から追加された「-parameters」オプション)がコンパイル後のクラスに含まれている必要があります。これらの情報をクラスに含めない場合は、name属性(or value属性)に属性名を明示的に指定してください。

name属性の指定例
@PostMapping(path = "create")
String create(@Validated Form form, BindingResult result,
              @SessionAttribute("clientInfo") Optional<ClientInfo> clientInfo) { // 明示的に属性名を指定する
    // ...
    return "redirect:/users/create?complete";
}

@RequestAttributeを使用してリクエストスコープのオブジェクトにアクセスできる :thumbsup:

HttpServletRequestで管理しているオブジェクトにアクセスしたい場合は、HttpServletRequestのAPIを直接使わずに@RequestAttributeを使うことができます。具体的な使い方は、@SessionAttributeと同じなので、本投稿では割愛します。

リクエストパラメータのバインディング有無を制御できる :thumbsup:

Spring 4.3から、Modelから取得したオブジェクトに対して、リクエストパラメータのバインディング有無を制御できるようになります。Spring MVCでは、Handlerメソッドの引数にフォームクラスなどのJavaBeanを指定すると、JavaBeanのプロパティにリクエストパラメータ値がバインドされる仕組みになっています。この仕組みは非常に便利なのですが、セッションスコープやフラッシュスコープで管理しているオブジェクトに対しては、この仕組みを適用したくないケースがあります。たとえば、入力フォームをセッションスコープで管理していて、確認画面から送信処理を行う際にはリクエストパラメータをバインドさせたくない!!というケースが考えられます。

Spring 4.3では、@ModelAttributebinding属性にfalseを指定することで、リクエストパラメータのバインディングを抑止できます。

〜4.2の実装例
@RequestMapping(path = "create", method = RequestMethod.POST)
String create(ModelMap model) {
    Form form = Form.class.cast(model.get("form")); // 引数でうけとったMapから取得する
    // ...
    return "redirect:/users/create?complete";
}
4.3〜の実装例
@PostMapping(path = "create")
String create(@ModelAttribute(binding = false) Form form) { // binding=falseを指定して引数として直接受け取る
    // ...
    return "redirect:/users/create?complete";
}

Spring MVCの例外ハンドラでErrorやThrowableをハンドリングできる :thumbsup:

Spring 4.3から、Spring MVCの例外ハンドラ(HandlerExceptionResolver)でErrorや自作のThrowableをハンドリングできるようになります。Spring 4.2までは、例外ハンドラは呼び出されず、NestedServletExceptionにラップしてサーブレットコンテナへスローされていました。

特定のErrorをハンドリングする際の実装例
@ExceptionHandler
public String handleStackOverflowError(StackOverflowError e) {
    // ...
}

基本的には、ErrorThrowableはむやみにハンドリングすべきではないと思いますが、アプリケーションの要件によっては利用を検討してもよいかもしれません。
この改善に伴い1点注意が必要なことがあります。それは、すでにjava.lang.Exceptionjavax.servlet.ServletExceptionをハンドリングしていると、Spring 4.3を使うと意図せずErrorThrowableもハンドリングされてしまいます・・・ :scream:

これを回避するには・・・(どうするのがいいのかな・・・ :disappointed_relieved: ) とりあえず以下のようなバイパス的なハンドリング処理を実装すればSpring 4.2と同じ動きに(サーブレットコンテナに通知)することはできるけど・・・・(なんか違う気がしてならない・・・ :sweat_smile:

@ExceptionHandler
public void handleNestedServletException(NestedServletException e) throws NestedServletException {
    throw e;
}

マルチパートのパート部分をコンバートするHttpMessageConverterのデフォルトエンコーディングがUTF-8になる :thumbsup:

Spring 4.3から、マルチパートのパート部分をコンバートするHttpMessageConverterのデフォルトエンコーディングがUTF-8になります。これは、RestTemplateを使ってサーバーへマルチパートデータを送信する場合や、ResponseEntity@ResponseBodyを使ってマルチパートデータをクライアントへ応答する際に適用されます。新規に作るアプリケーションの場合は、UTF-8を利用するケースが多いだろうから、この対応は地味にうれしいですね :laughing:

ここでは、マルチパートを使ってテキストファイルとフォームデータを送る例を見てみましょう。

サーバー側の実装例
@RequestMapping("/upload")
@RestController
public class UploadRestController {
    @RequestMapping(method = RequestMethod.POST)
    String upload(MultipartFile file, String text) throws IOException {
        System.out.println(StreamUtils.copyToString(file.getInputStream(), StandardCharsets.UTF_8));
        System.out.println(text);
        return file.getOriginalFilename();
    }
}
クライアント側の実装例(JUnit)

@Test
public void upload() {

    RestTemplate restTemplate = new RestTemplate();
    // デバッグ用にリクエストボディをコンソールに出力するインタセプタを差し込む
    restTemplate.setInterceptors(Collections.singletonList((request, body, execution) -> {
        System.out.println(new String(body));
        return execution.execute(request, body);
    }));

    // ファイルとテキストデータをマルチパートデータとしてセットアップ
    MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
    form.add("file", new FileSystemResource("./a.txt"));
    form.add("text", "あああ");

    // マルチパートリクエストを作成
    RequestEntity<MultiValueMap<String, Object>> requestEntity =
            RequestEntity.post(URI.create("http://localhost:8080/upload"))
                    .contentType(MediaType.MULTIPART_FORM_DATA)
                    .body(form);

    // マルチパートリクエストを送信
    ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);
    System.out.println(responseEntity.getBody());
}

JUnitを実行すること、コンソールに以下のような結果が出力されます。

コンソール(クライアント)
--_U91C0cWNkMO5HsYdnhF29Vz1Tj7dh0e0nQrAzM0
Content-Disposition: form-data; name="file"; filename="a.txt"
Content-Type: text/plain
Content-Length: 12

ああああ
--_U91C0cWNkMO5HsYdnhF29Vz1Tj7dh0e0nQrAzM0
Content-Disposition: form-data; name="text"
Content-Type: text/plain;charset=UTF-8
Content-Length: 9

あああ
--_U91C0cWNkMO5HsYdnhF29Vz1Tj7dh0e0nQrAzM0--
a.txt

ここでポイントになるのが、フォームデータのtextのContent-TypeのcharsetがUTF-8になっている点です。

4.3〜
--_U91C0cWNkMO5HsYdnhF29Vz1Tj7dh0e0nQrAzM0
Content-Disposition: form-data; name="text"
Content-Type: text/plain;charset=UTF-8
Content-Length: 9

あああ

Spring 4.2だと、この部分がISO-8859-1になるため、日本語などのマルチバイト文字がデフォルトのまま使うと文字化けしてしまいます。もちろん任意のエンコーディングを指定できるようになっていますが、コンフィギュレーションが面倒なんだよね・・・ :sweat_smile:

〜4.2
--JPpi8b-TOpRe3PyEB7vcjpjID072o6wxXAZy7
Content-Disposition: form-data; name="text"
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 3

???

では、UTF-8以外のエンコーディングにする必要がある場合はどうすればよいのでしょうか? ここでは、日本のシステムだとまだまだ現役!?の「Windows-31J」に変更してみます。

エンコーディングを変更する際の実装例
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().stream()
        .filter(c -> c instanceof FormHttpMessageConverter)
        .forEach(c -> ((FormHttpMessageConverter) c).setCharset(Charset.forName("Windows-31J")));
エンコーディングをWindows-31Jにした場合のテキスト部
--oym_khz0wPMOlL5rD3WZ70mNapWvjiaMXs
Content-Disposition: form-data; name="text"
Content-Type: text/plain;charset=windows-31j
Content-Length: 6

あああ

Content-Lengthも6バイトなので、Window-31Jになりましたね :wink:

静的リソースのContent-typeをContentNegotiationManager経由で指定できる :thumbsup:

Spring 4.3から、静的リソースのContent-Type(Media Type)を決定する際に、ContentNegotiationManagerの設定を利用できるようになります。まず、デフォルト設定のままで静的リソースにアクセスしています。

src/main/resources/static/test.xml
<test>
    <code>TEST</code>
</test>
$ curl -D - http://localhost:8080/test.xml
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Last-Modified: Sun, 29 May 2016 18:42:38 GMT
Accept-Ranges: bytes
Content-Type: application/xml
Content-Length: 36
Date: Sun, 29 May 2016 18:42:54 GMT

<test>
    <code>TEST</code>
</test>

次にContentNegotiationManagerの設定を変更(拡張子がxmlのメディアタイプをtext/xmlに設定)してから静的リソースにアクセスします。

ContentNegotiationManagerの設定変更例
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.mediaType("xml", MediaType.TEXT_XML); // XMLのメディアタイプを明示的に指定
    }
}
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Last-Modified: Sun, 29 May 2016 18:42:38 GMT
Accept-Ranges: bytes
Content-Type: text/xml
Content-Length: 36
Date: Sun, 29 May 2016 18:45:21 GMT

<test>
    <code>TEST</code>
</test>

RestTemplateAsyncRestTemplateにてURI変数値に厳格なURLエンコーディングを適用できる :thumbsup:

Spring 4.3から、RestTemplateAsyncRestTemplateに設定するDefaultUriTemplateHandlerを介して、URI変数値に厳格なURLエンコーディングを適用することができます。デフォルトは、厳格なエンコーディングは行いません。
厳格なエンコーディングを行うようにすると、「RFC 3986のセクション2」で定義されている「unreserved」以外の文字もURLエンコーディングされます。

ここでは、「;」を例に、厳格なURLエンコーディングの有無でどのような違いがでるのか説明します。

サーバ側の実装例
@GetMapping(path = "/users/{username}")
@ResponseBody
String get(@PathVariable String username) {
    return username;  // パス変数からユーザー名を取得し、取得したユーザー名を返却する
}

まず、厳格なURLエンコーディングを行わないでアクセスしてみます。

厳格なURLエンコーディングを行わない場合の実装例
RestTemplate restTemplate = new RestTemplate();
String body = restTemplate.getForObject("http://localhost:8080/users/{username}"
        , String.class, "Kazuki;Shimizu");
System.out.println(body);
コンソール
Kazuki

残念なことに、「;」以降の文字が欠落してしまいました。厳格なURLエンコーディングを行わないと、「http://localhost:8080/users/Kazuki;Shimizu」というURLでサーバー側にアクセスするため、「;」以降が欠落しました。

次に、厳格なURLエンコーディングを行ってアクセスしてみます。

厳格なURLエンコーディングを行う場合の実装例
RestTemplate restTemplate = new RestTemplate();
DefaultUriTemplateHandler handler = new DefaultUriTemplateHandler();
handler.setStrictEncoding(true); // 厳格なURLエンコーディングを有効化する
restTemplate.setUriTemplateHandler(handler);
String body = restTemplate.getForObject("http://localhost:8080/users/{username}"
        , String.class, "Kazuki;Shimizu");
System.out.println(body);
コンソール
Kazuki;Shimizu

厳格なURLエンコーディングを行うと、「http://localhost:8080/users/Kazuki%3BShimizu」というURLでサーバー側にアクセスするため、「;」以降の文字は欠落しません。

AsyncRestTemplateにインタセプターを組み込める :thumbsup:

Spring 4.3から、AsyncRestTemplateにインタセプター(RestTemplateでいうところのClientHttpRequestInterceptor相当)の仕組みが追加されます。個人的には・・・待望の・・・といった感じがあります :smile:

ここでは、リクエストとレスポンスをログ出力するインタセプターを作成し、AsyncRestTemplateに適用してみます。

インタセプターの実装例
public class LoggingInterceptor implements AsyncClientHttpRequestInterceptor {
    @Override
    public ListenableFuture<ClientHttpResponse> intercept(HttpRequest request, byte[] body, AsyncClientHttpRequestExecution execution) throws IOException {

        // リクエスト前に行う処理を実装する
        if (logger.isDebugEnabled()) {
            logger.debug("Request Header : {}", request.getHeaders());
            logger.debug("Request Body   : {}", new String(body, StandardCharsets.UTF_8));
        }

        // 後続処理(別のAsyncClientHttpRequestInterceptorまたは非同期通信処理)を呼び出す
        ListenableFuture<ClientHttpResponse> future = execution.executeAsync(request, body);

        // レスポンス後に行う処理をコールバック関数としてListenableFutureに追加する
        if (logger.isDebugEnabled()) {
            future.addCallback(
                    response -> {
                        try {
                            logger.debug("Response Status : {}", response.getStatusCode());
                            logger.debug("Response Header : {}", response.getHeaders());
                        } catch (IOException e) {
                            throw new UncheckedIOException(e);
                        }
                    },
                    e -> logger.debug("Error!!", e)
            );
        }
        return future;
    }
}
インタセプターの適用例
AsyncRestTemplate asyncRestTemplate = new AsyncRestTemplate();
asyncRestTemplate.setInterceptors(Collections.singletonList(new LoggingInterceptor())); // インタセプターを適用する
ListenableFuture<ResponseEntity<String>> future = asyncRestTemplate.getForEntity("http://localhost:8080/users/{username}"
        , String.class, "kazuki43zoo");
future.addCallback(
        entity -> System.out.println(entity.getBody()),
        Throwable::printStackTrace);
コンソール
06:37:56.706 [main] DEBUG org.springframework.web.client.AsyncRestTemplate - Created asynchronous GET request for "http://localhost:8080/users/kazuki43zoo"
06:37:56.713 [main] DEBUG org.springframework.web.client.RestTemplate - Setting request Accept header to [text/plain, application/json, application/*+json, */*]
06:37:56.715 [main] DEBUG com.example.RestTemplateTests - Request Header : {Accept=[text/plain, application/json, application/*+json, */*], Content-Length=[0]}
06:37:56.717 [main] DEBUG com.example.RestTemplateTests - Request Body   : 
06:37:59.755 [SimpleAsyncTaskExecutor-1] DEBUG com.example.RestTemplateTests - Response Status : 200
06:37:59.755 [SimpleAsyncTaskExecutor-1] DEBUG com.example.RestTemplateTests - Response Header : {Server=[Apache-Coyote/1.1], Content-Type=[text/plain;charset=UTF-8], Content-Length=[11], Date=[Sun, 29 May 2016 21:37:59 GMT]}
06:37:59.756 [SimpleAsyncTaskExecutor-1] DEBUG org.springframework.web.client.AsyncRestTemplate - Async GET request for "http://localhost:8080/users/kazuki43zoo" resulted in 200 (OK)
06:37:59.757 [SimpleAsyncTaskExecutor-1] DEBUG org.springframework.web.client.RestTemplate - Reading [java.lang.String] as "text/plain;charset=UTF-8" using [org.springframework.http.converter.StringHttpMessageConverter@245d5499]
kazuki43zoo

まとめ

今回は、Web関連の主な変更点を紹介しました。インパクトのある新機能はないものの、新しいアノテーションの追加や既存アノテーションの挙動の改善などが行われており、アノテーション駆動開発を手助けする機能が強化されています。また、個人的にはAsyncRestTemplateにインタセプターが導入されたのはGood Newsでした!! :laughing: 次回は、「Web Socket関連の主な変更点」を紹介する予定です。

参考サイト

補足

Spring 4.3 GAに伴い変更点を追加 (2016/6/11)

ついに4.3がGAになり、そのタイミングで主な変更点に追加されたトピックスを反映しました。(「★6/11追加」でマークしてあります)

54
64
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
54
64

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?