Edited at

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

More than 1 year has passed since last update.


やりたいこと

前回の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)
.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出してマージしてます。