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

# やりたいこと

前回の[SpringでバージョニングされたAPIのエンドポイントを定義する(1)](http://qiita.com/uich/items/538fcaa779e49ecc45ac)では、

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

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

# 前回のコード

## コントローラ

```java: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)`の実装をやめて、こちらの実装に切り替えました。

```java: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`のバージョンをサポートするというふうに変更してみます。

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

 // ・・・

}
```

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

```java: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`となるコードになっています。
よって、コントローラ側をこんな風に修正しました。

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

 // ・・・

}
```

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

# まとめ

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

[GitHub](https://github.com/uich/demo-api-versioning)に[PR](https://github.com/uich/demo-api-versioning/pull/1)出してマージしてます。