4
5

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 1 year has passed since last update.

SpringでバージョニングされたAPIのエンドポイントを定義する(2)

Last updated at Posted at 2017-03-14

やりたいこと

前回のSpringでバージョニングされたAPIのエンドポイントを定義する(1)では、

  • クラスに@ApiVersionが付与できない
  • RequestMappingInfoの生成が雑

という問題点がありました。
今回はこれを解消します。

前回のコード

コントローラ

DemoController.java
@RestController
public class DemoController {

  @GetMapping("/items/{id}")
  @ApiVersion("1.0")
  public Item1 getItem(@PathVariable Integer id) {
    return Item1.builder()
      .id(id)
      .name("バッグ")
      .build();
  }

  @GetMapping("/items/{id}")
  @ApiVersion(greaterThan = "1.0")
  public Item2 getItem(@PathVariable Long id) {
    return Item2.builder()
      .id(id)
      .name("バッグ")
      .price(10.9)
      .build();
  }

  @GetMapping("/users/{id}")
  @ApiVersion(supported = { "1.0", "3.1" })
  public User getUser(@PathVariable Long id) {
    return User.builder()
      .id(id)
      .name("Kenny")
      .build();
  }

}

複合的なRequestMappingInfoの生成

コントローラにもメソッドにも@ApiVersionを付与するので複合的なRequestMappingInfoが生成されなければいけません。
これを自前で実装するのは手間なので、もう一度RequestMappingHandlerMappingクラスを読み直しました。#getMappingForMethod(Method, Class)から追っていくと、もっとオーバーライドするのにふさわしいメソッドがありました。

#createRequestMappingInfo(RequestMapping, RequestCondition)です。

#getMappingForMethod(Method, Class)の実装をやめて、こちらの実装に切り替えました。

VersionedRequestMappingHandlerMapping.java

@Override
protected RequestMappingInfo createRequestMappingInfo(RequestMapping requestMapping,
  RequestCondition<?> customCondition) {
  if (customCondition instanceof ApiVersionRequestCondition) {
    return RequestMappingInfo.paths(Arrays.stream(requestMapping.path())
      // RequestMappingから得られる全てのパスパターンに
      // バージョン文字列のパターンを付与する
      .map(path -> "/{version:\\d+\\.\\d+}/" + StringUtils.removeStart(path, "/"))
      .toArray(String[]::new))
      .methods(requestMapping.method())
      .params(requestMapping.params())
      .headers(requestMapping.headers())
      .consumes(requestMapping.consumes())
      .produces(requestMapping.produces())
      .mappingName(requestMapping.name())
      .customCondition(customCondition)
      .options(getBuilderConfiguration())
      .build();
  }

  return super.createRequestMappingInfo(requestMapping, customCondition);
}

これで自前で空っぽのRequestMappingInfoを作ってオリジナルと繋げて・・・みたいなことはしなくて済むのでコード的にはスッキリしました。

コントローラに@ApiVersionを仕込む

今回は仮に、全てのEndpointで0.5のバージョンをサポートするというふうに変更してみます。

DemoController.java
@RestController
+ @ApiVersion("0.5")
public class DemoController {

 // ・・・

}

よし、これでOK。
と思ったが、ダメでした。
クラス側に付与した@ApiVersionが考慮されないので、Spring側のコードを見てみます。

RequestMappingHandlerMapping.java
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
  RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
  RequestCondition<?> condition = (element instanceof Class ?
      getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
  return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}

ここであるように、AnnotatedElementがクラスになるのですが、@RequestMappingが付与されていないとそもそもnullとなるコードになっています。
よって、コントローラ側をこんな風に修正しました。

DemoController.java
@RestController
+ @RequestMapping
@ApiVersion("0.5")
public class DemoController {

 // ・・・

}

これで全てのEndpointでバージョン0.5もマッピングされるようになりました。

まとめ

  • もうちょっとちゃんとコード読む

GitHubPR出してマージしてます。

4
5
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
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?